From bce64fc8f6f68b565d92814e696181548b2cd103 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sun, 9 Sep 2018 12:20:12 -0400 Subject: [PATCH] implemented so many things... --- .gitignore | 1 + Dockerfile | 1 + README.md | 3 +- datomic/Dockerfile | 32 +++ datomic/start.sh | 4 + docker-compose.yml | 54 +++--- env/dev/clj/skirmish/wui/middleware.clj | 6 + project.clj | 13 +- resources/config.edn | 40 +++- src/clj/skirmish/bookkeeper/core.clj | 2 +- src/clj/skirmish/broker/core.clj | 5 + src/clj/skirmish/core.clj | 4 +- src/clj/skirmish/exchange_gateway/core.clj | 3 +- .../exchange_gateway/exchange_gateway.clj | 12 +- src/clj/skirmish/ledger/accounts.clj | 94 ++++++++- src/clj/skirmish/ledger/core.clj | 175 ++++++++++++++++- src/clj/skirmish/rest/util.clj | 26 ++- src/clj/skirmish/util.clj | 19 +- .../wui/components/create_order_page.cljs | 50 ++++- .../skirmish/wui/components/home_page.cljs | 16 +- .../wui/components/portfolio_page.cljs | 21 +- src/cljs/skirmish/wui/components/secunit.cljs | 5 + test/clj/skirmish/ledger/test_core.clj | 183 +++++++++++++++++- test/clj/skirmish/test_util.clj | 24 +++ 24 files changed, 734 insertions(+), 59 deletions(-) create mode 100644 datomic/Dockerfile create mode 100644 datomic/start.sh create mode 100644 src/cljs/skirmish/wui/components/secunit.cljs create mode 100644 test/clj/skirmish/test_util.clj diff --git a/.gitignore b/.gitignore index 8ec97ec..03fe787 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ full-stack fixtures node_modules figwheel_server.log +.rebel_readline_history diff --git a/Dockerfile b/Dockerfile index dc3aecd..8db1b87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ FROM clojure +MAINTAINER Didier Amyot # Set the working directory to /app WORKDIR /app diff --git a/README.md b/README.md index 83d8c0f..169c9ec 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,13 @@ file should be self documented. #### Server * [clojure](https://clojure.org/) for programming language * [kafka](http://kafka.apache.org/) for message broker/database +* [datomic-free](https://www.datomic.com/) for transaction needs #### In the browser * [clojure script](https://clojurescript.org/) for programming language * [sparks](https://aftertheflood.com/projects/sparks/) for trivial charts * [metricsgraphicsjs](http://metricsgraphicsjs.org/) for complex charts - +* [dc-js](http://dc-js.github.io/dc.js/) for some charting. ## License diff --git a/datomic/Dockerfile b/datomic/Dockerfile new file mode 100644 index 0000000..762e759 --- /dev/null +++ b/datomic/Dockerfile @@ -0,0 +1,32 @@ +FROM openjdk:11-jdk +MAINTAINER Didier Amyot + +ENV DATOMIC_VERSION 0.9.5656 + +ADD https://my.datomic.com/downloads/free/${DATOMIC_VERSION} /tmp/datomic.zip + +RUN unzip /tmp/datomic.zip && rm /tmp/datomic.zip + +WORKDIR datomic-free-${DATOMIC_VERSION} + +RUN cp config/samples/free-transactor-template.properties transactor.properties + +ADD https://clojars.org/repo/slingshot/slingshot/0.12.2/slingshot-0.12.2.jar /tmp/slingshot-0.12.2.jar +RUN mv /tmp/slingshot-0.12.2.jar lib/ + +RUN sed "s/host=localhost/host=0.0.0.0/" -i transactor.properties + +RUN mkdir /data +RUN sed "s/# data-dir=data/data-dir=\/data/" -i transactor.properties +VOLUME /data + +RUN mkdir /log +RUN sed "s/# log-dir=log/log-dir=\/log/" -i transactor.properties +VOLUME /log + +ADD start.sh ./ +RUN chmod +x start.sh + +EXPOSE 4334 4335 4336 + +CMD ["./start.sh"] diff --git a/datomic/start.sh b/datomic/start.sh new file mode 100644 index 0000000..69a7f8e --- /dev/null +++ b/datomic/start.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +sed "/host=0.0.0.0/a alt-host=${ALT_HOST:-127.0.0.1}" -i transactor.properties +exec bin/transactor transactor.properties diff --git a/docker-compose.yml b/docker-compose.yml index 6602f56..01e4935 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ version: '2' services: zoo1: image: zookeeper:3.4.9 - restart: unless-stopped + restart: "unless-stopped" hostname: zoo1 ports: - "2181:2181" @@ -150,26 +150,36 @@ services: depends_on: - zoo1 -# skirmish - - skirmish-api: - image: skirmish - command: ["lein", "run", "api-server"] - depends_on: - - kafka-topics-ui # basically, we need all kafka - environment: - PORT: 1602 # https://en.wikipedia.org/wiki/Euronext_Amsterdam - KAFKA_BOOTSTRAP_SERVERS: "kafka1:9092" +# datomic + datomic-transactors: + build: "datomic/" + restart: "unless-stopped" ports: - - "1602:1602" + - "4334:4334" + - "4335:4335" + - "4336:4336" - skirmish-exchange-core: - image: skirmish - command: ["lein", "run", "exchange-core"] - depends_on: - - skirmish-api # basically, we need all kafka - environment: - PORT: 1602 # https://en.wikipedia.org/wiki/Euronext_Amsterdam - KAFKA_BOOTSTRAP_SERVERS: "kafka1:9092" - ports: - - "1602:1602" + +# skirmish itself + +# skirmish-api: +# build: . +# command: ["lein", "run", "api-server"] +# depends_on: +# - kafka-topics-ui # basically, we need all kafka +# environment: +# PORT: 1602 # https://en.wikipedia.org/wiki/Euronext_Amsterdam +# KAFKA_BOOTSTRAP_SERVERS: "kafka1:9092" +# ports: +# - "1602:1602" +# +# skirmish-exchange-core: +# build: . +# command: ["lein", "run", "exchange-core"] +# depends_on: +# - skirmish-api # basically, we need all kafka +# environment: +# PORT: 1602 # https://en.wikipedia.org/wiki/Euronext_Amsterdam +# KAFKA_BOOTSTRAP_SERVERS: "kafka1:9092" +# ports: +# - "1602:1602" diff --git a/env/dev/clj/skirmish/wui/middleware.clj b/env/dev/clj/skirmish/wui/middleware.clj index d932f17..753b1a6 100644 --- a/env/dev/clj/skirmish/wui/middleware.clj +++ b/env/dev/clj/skirmish/wui/middleware.clj @@ -3,8 +3,14 @@ [prone.middleware :refer [wrap-exceptions]] [ring.middleware.reload :refer [wrap-reload]])) +(defn custom-dev-middleware [handler] + (fn [request] + (let [response (handler request)] + response))) + (defn wrap-middleware [handler] (-> handler (wrap-defaults site-defaults) + custom-dev-middleware wrap-exceptions wrap-reload)) diff --git a/project.clj b/project.clj index 270a83f..b10c008 100644 --- a/project.clj +++ b/project.clj @@ -4,9 +4,15 @@ :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} + + :resource-paths ["resources" + "target/cljsbuild"] + :dependencies [[org.clojure/clojure "1.9.0"] [org.clojure/core.async "0.4.474"] [org.clojure/math.combinatorics "0.1.4"] + [com.datomic/datomic-free "0.9.5697"] + [com.google.guava/guava "22.0"] ;; server [aero "1.1.3"] @@ -15,8 +21,8 @@ [com.taoensso/nippy "2.14.0"] [ring-cors "0.1.12"] [javax.servlet/javax.servlet-api "3.1.0"] - [org.apache.kafka/kafka-streams "1.1.0"] - [org.apache.kafka/kafka-clients "1.1.0"] + + ;; backends [spootnik/kinsky "0.1.22"] ;;utils @@ -64,7 +70,6 @@ [:cljsbuild :builds :app :compiler :output-to]] :source-paths ["src/clj" "src/cljc"] - :resource-paths ["resources" "target/cljsbuild"] :minify-assets {:assets @@ -94,7 +99,7 @@ :devcards {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"] - :figwheel {:devcards true} + :figwheel {:devcards false} :compiler {:main "skirmish.wui.cards" :asset-path "js/devcards_out" :output-to "target/cljsbuild/public/js/app_devcards.js" diff --git a/resources/config.edn b/resources/config.edn index 2fcb00e..0be01c2 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -5,6 +5,9 @@ ;; If set to true, orchestra will instrument all the spec'ed functions :instrument-spec true + :datomic + {:transactor-url "datomic:free://localhost:4334/skirmish"} + ;; all the kafka configuration goes bellow. :kafka {:bootstrap.servers #or [#env KAFKA_BOOTSTRAP_SERVERS "localhost:9092"] @@ -24,15 +27,44 @@ :max-body 524288 ;; 512kb, small but enough :max-ws 524288} + :ledger-server + {:port #long #or [#env LEDGER_SERVER_PORT 1604] + :ip "0.0.0.0" + :max-body 524288 ;; 512kb, small but enough + :max-ws 524288} + :wui {:port #long #or [#env WUI_PORT 3000]} ;; list of tradable securities :securities - [{:sec-id :foo :name "the foo company" :description "Sell names to developer."} - {:sec-id :bar :name "bar international" :description "Steal company."} - {:sec-id :toto :name "La compagnie des toto" :description "Serait trop long pour ecrire ici."} - {:sec-id :tata :name "Unicorn startup" :description "ambition: conquerir le monde!"}] + [{:sec-id :dapm + :currency :dapm + :symbol "ლ" + :name "didier amyot paper money" + :interest-rate 1.1 ;; 10% per day + :emitter "emitter publick key" + :description "The default currency."} + + {:sec-id :foo + :currency :dapm + :name "the foo company" + :description "Sell names to developer."} + + {:sec-id :bar + :currency :dapm + :name "bar international" + :description "Steal company."} + + {:sec-id :toto + :currency :dapm + :name "La compagnie des toto" + :description "Serait trop long pour ecrire ici."} + + {:sec-id :tata + :currency :dapm + :name "Unicorn startup" + :description "ambition: conquerir le monde!"}] ;; stupid parameters about benchmarking tools :benchmark { diff --git a/src/clj/skirmish/bookkeeper/core.clj b/src/clj/skirmish/bookkeeper/core.clj index c009c0c..015b95f 100644 --- a/src/clj/skirmish/bookkeeper/core.clj +++ b/src/clj/skirmish/bookkeeper/core.clj @@ -9,7 +9,7 @@ (defn -main [config exchange-apis] (let [bookkeeper-config (:api-bookkeeper config) api-routes (create-routes-for-bookkeeper exchange-apis config)] - (println "Running server with bellow config") + (println "Running skirmish.bookkeeper.core server with bellow config\n" bookkeeper-config) (println bookkeeper-config) (run-server (-> api-routes api (wrap-cors :access-control-allow-origin #"http://localhost:3449" diff --git a/src/clj/skirmish/broker/core.clj b/src/clj/skirmish/broker/core.clj index cd7871c..9ec1691 100644 --- a/src/clj/skirmish/broker/core.clj +++ b/src/clj/skirmish/broker/core.clj @@ -1,4 +1,9 @@ (ns skirmish.broker.core) +(def portfolio {:dapm {:amount 10} + :foo {:amount 5 :long-exposure 10 :short-exposure 7} + :bar {:amount 100 :long-exposure 10 :short-exposure 7}}) + + (defn -main [config exchange-apis]) \ No newline at end of file diff --git a/src/clj/skirmish/core.clj b/src/clj/skirmish/core.clj index 4ce9828..f4e8edd 100644 --- a/src/clj/skirmish/core.clj +++ b/src/clj/skirmish/core.clj @@ -5,6 +5,7 @@ [skirmish.wui.server] [skirmish.bookkeeper.core] [skirmish.exchange-gateway.core] + [skirmish.ledger.core] [skirmish.middleware.core :refer [create-exchange-apis-for]] [aero.core :refer (read-config)] [orchestra.spec.test :as st] @@ -16,11 +17,12 @@ {:api-server skirmish.exchange-gateway.core/-main :exchange-core skirmish.exchange.core/-main ;; should be skirmish.exchange.core/api-main :api-bookkeeper skirmish.bookkeeper.core/-main + :ledger skirmish.ledger.core/-main :wui skirmish.wui.server/-main}) (defn run-all-targets-in-separate-threads [target-fns config exchange-apis] (println "running multiple targets. This is okay in dev but horrible in production") - (println target-fns) + (println (clojure.string/join "\n" target-fns)) (let [threads (map #(async/thread (% config exchange-apis)) target-fns) finished-threads (async/merge threads)] (loop [finished-thread (async/!! sub]] - [clojure.edn :as edn])) + [clojure.core.async :refer [chan go go-loop close! !! sub]])) (def order-id (atom -1)) @@ -28,13 +27,6 @@ :book.order/entry-timestamp now}))) -(defn get-body [body] - (try+ - (-> body clojure.java.io/reader slurp edn/read-string) - (catch Exception e - (throw+ {:error-type :4XX :status 400 :body {:error "Invalid body format. Body must be edn format."}})))) - - (defn get-exchange-api [sec-id exchange-apis] (if (contains? exchange-apis sec-id) (get exchange-apis sec-id) diff --git a/src/clj/skirmish/ledger/accounts.clj b/src/clj/skirmish/ledger/accounts.clj index 375b47a..91f780c 100644 --- a/src/clj/skirmish/ledger/accounts.clj +++ b/src/clj/skirmish/ledger/accounts.clj @@ -1 +1,93 @@ -(ns skirmish.ledger.accounts) +(ns skirmish.ledger.accounts + (:require [datomic.api :as d] + [slingshot.slingshot :refer [throw+]])) + +(def db-uri "datomic:free://localhost:4334/skirmish") + +(def create-securities + [ + {:db/ident :skirmish.ledger/create-security + :db/doc "Create a security in the system." + :db/fn (d/function {:lang "clojure" + :requires ['[slingshot.slingshot]] + :params '[db sec-id interest-rate] + :code '(let [security (d/pull db '[:db/id] [:skirmish.security/sec-id sec-id])] + (when (-> security :db/id some?) + (slingshot.slingshot/throw+ {:error-type :4XX :status 400 :body {:error "The security already exists."}})) + [#:skirmish.security{:sec-id sec-id :interest-rate interest-rate}])})}]) + +(def print-securities + [ + {:db/ident :skirmish.ledger/emit-securities + :db/doc "Print securities fresh from the press." + :db/fn (d/function {:lang "clojure" + :requires ['[slingshot.slingshot]] + :params '[db sec-id amount account-public-key] + :code '(let [security (d/pull db '[:db/id] [:skirmish.security/sec-id sec-id]) + account (d/pull db '[:skirmish.ledger/amount :skirmish.security/sec] [:skirmish.ledger/owner-public-key account-public-key]) + base-amount (get account :skirmish.ledger/amount 0) + entity (:db/id account "new-account")] + (when (-> security :db/id nil?) + (slingshot.slingshot/throw+ {:error-type :4XX :status 404 :body {:error "No security with such id."}})) + (when (and (not= (:skirmish.security/sec account) (:db/id security)) (some? (:db/id account))) + (slingshot.slingshot/throw+ {:error-type :4XX :status 400 :body {:error "The security emitted does not match the account security."}})) + [[:db/add entity :skirmish.ledger/amount (+ base-amount amount)] + [:db/add entity :skirmish.ledger/owner-public-key account-public-key] + [:db/add entity :skirmish.ledger/sec (:db/id security)]])})}]) + +;; there are different types of securities... +;; 1. currencies, which pays interest rates in their one sec-id. +;; 2. equities, which do not pay interest rates. +;; 3. bonds, which pays interest rates in another currency. + +(def schema-securities + [ + { + :db/ident :skirmish.security/sec-id + :db/valueType :db.type/keyword + :db/cardinality :db.cardinality/one + :db/unique :db.unique/identity + :db/doc "The sec id."} + { + :db/ident :skirmish.security/interest-rate + :db/valueType :db.type/long + :db/cardinality :db.cardinality/one + :db/doc "The interest-rate payed by the security as a multiple of 0.01% (100 is 1% interest rate)."}]) + +(def ledger-schema + [ + + { + :db/ident :skirmish.ledger/owner-public-key + :db/unique :db.unique/identity + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/doc "The public key of the owner"} + { + :db/ident :skirmish.ledger/amount + :db/index false + :db/valueType :db.type/long + :db/cardinality :db.cardinality/one + :db/doc "The quantity of currency owned by this account."} + { + :db/ident :skirmish.ledger/sec + :db/index false + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one + :db/doc "The sec kept in this account."}]) + + +(def schema (-> [] + (concat schema-securities) + (concat ledger-schema) + (concat create-securities) + (concat print-securities))) + + +(defn create-connection [uri] + (let [new-db? (d/create-database uri) + conn (d/connect uri)] + (when new-db? + @(d/transact conn schema)) + conn)) + diff --git a/src/clj/skirmish/ledger/core.clj b/src/clj/skirmish/ledger/core.clj index f442b15..32853d3 100644 --- a/src/clj/skirmish/ledger/core.clj +++ b/src/clj/skirmish/ledger/core.clj @@ -1 +1,174 @@ -(ns skirmish.ledger.core) +(ns skirmish.ledger.core + (:require [org.httpkit.server :refer [run-server]] + [compojure.handler :refer [api]] + [skirmish.util :refer [block-thread]] + [slingshot.slingshot :refer [try+ throw+]] + [compojure.route :refer [not-found]] + [skirmish.middleware.core :refer [create-exchange-apis-for]] + [skirmish.exchange-gateway.exchange-gateway :refer [create-routes-for-exchange]] + [skirmish.rest.util :refer [build-response get-body]] + [skirmish.ledger.accounts] + [datomic.api :as d] + [compojure.core :refer :all] + [clojure.spec.alpha :as s] + [ring.middleware.cors :refer [wrap-cors]])) + +;; the ledger is the central + +;; an :account-id is the public key of a +;; an :account-secret is an :acount-id private key + +(defn get-account-body [request account sec] + (if (nil? sec) + (build-response request {:status 404}) + (let [sec-url (str "/api/1/securities/" (-> sec :skirmish.security/sec-id name)) + beautify-account { :amount (:skirmish.ledger/amount account) + :sec-id (:skirmish.security/sec-id sec) + :account-public-key (:skirmish.ledger/owner-public-key account)} + ret {:account beautify-account :sec-url sec-url}] + (build-response request ret)))) + + +(defn get-account [conn] + "Returns the value of an account." + (fn [{{account-id :account-id} :route-params :as request}] + (let [db (d/db conn) + account (d/pull db '[*] [:skirmish.ledger/owner-public-key account-id]) + sec-eid (-> account :skirmish.ledger/sec :db/id) + sec (d/entity db sec-eid)] + (get-account-body request account sec)))) + + +(s/def ::transaction (s/cat :type #{:create} + :sec-id keyword? + :amount int? + :account-id string?)) + + +(defn get-transaction [raw-transact] + (if (s/valid? ::transaction raw-transact) + (s/conform ::transaction raw-transact) + (throw+ {:error-type :4XX :status 400 :body {:error "Invalid transaction format."}}))) + + +(s/def ::security (s/cat :sec-id keyword? + :interest-rate nat-int?)) + +(defn get-new-security [raw-securities] + (if (s/valid? ::security raw-securities) + raw-securities + (throw+ {:error-type :4XX :status 400 :body {:error "Invalid security format."}}))) + +(defn get-security-from-db [conn sec-id] + (let [e (d/pull (d/db conn) '[*] [:skirmish.security/sec-id sec-id]) + {interest-rate :skirmish.security/interest-rate id :db/id} e + ret {:sec-id sec-id :interest-rate interest-rate}] + (if (nil? id) + (throw+ {:error-type :4XX :status 404 :body {:error "Security not found."}}) + ret))) + +(defn create-security [conn] + "Create a fresh new security." + (fn [{body :body :as request}] + (try+ + (let [[sec-id interest-rate] (-> body get-body get-new-security)] + @(d/transact conn [[:skirmish.ledger/create-security sec-id interest-rate]]) + (build-response request {:body {:success true} + :headers {"location" (str "/api/1" "/ledger/securities/" (name sec-id))}})) + (catch [:error-type :4XX] e (build-response request e)) + (catch [:error-type :5XX] e (build-response request e)) + (catch Object _ + (println (:throwable &throw-context) "unexpected error"))))) + +(defn get-security [conn] + "Returns the security." + (fn [{{sec-id-str :sec-id} :route-params :as request}] + (try+ + (let [sec-id (keyword sec-id-str)] + (build-response request (get-security-from-db conn sec-id))) + (catch [:error-type :4XX] e (build-response request e)) + (catch [:error-type :5XX] e (build-response request e))))) + + +(defn update-security [conn] + "Update a security with new features." + (fn [request] + (try+ + (catch [:error-type :4XX] e (build-response request e)) + (catch [:error-type :5XX] e (build-response request e))))) + + +(defn get-available-securities-body [securities] + (let [sec-id->url #(str "/api/1/ledger/securities/" (name %)) + to-security-description #(hash-map :sec-id % :url (sec-id->url %)) + xf (comp cat (map to-security-description)) + securities-info (sequence xf securities)];; + {:securities securities-info})) + + +(defn get-available-securities [conn] + (fn [request] + (let [securities (d/q '[:find ?s :where [?e :skirmish.security/sec-id ?s]] (d/db conn))] + (build-response request (get-available-securities-body securities))))) + + +(s/def ::emit-securities (s/cat :sec-id keyword? + :amount nat-int? + :account string?)) + + +(defn get-emit-securities-request [[amount account] sec-id] + (if (s/valid? ::emit-securities [sec-id amount account]) + [:skirmish.ledger/emit-securities sec-id amount account] + (throw+ {:error-type :4XX :status 400 :body {:error "Invalid emit request."}}))) + + +(defn emit-securities [conn] + (fn [{{sec-id-str :sec-id} :route-params body :body :as request}] + (try+ + (let [emit-request (-> body get-body (get-emit-securities-request (keyword sec-id-str)))] + @(d/transact conn [emit-request]) + (build-response request {})) + (catch [:error-type :4XX] e (build-response request e)) + (catch [:error-type :5XX] e (build-response request e))))) + + +(defn get-available-accounts-body [accounts] + (let [sec-id->url #(str "/api/1/ledger/accounts/" (name %)) + to-security-description #(hash-map :account-public-key % :url (sec-id->url %)) + xf (comp cat (map to-security-description)) + securities-info (sequence xf accounts)];; + {:accounts securities-info})) + + +(defn get-available-accounts [conn] + (fn [request] + (let [securities (d/q '[:find ?pk :where [?e :skirmish.ledger/owner-public-key ?pk]] (d/db conn))] + (build-response request (get-available-accounts-body securities))))) + +(defn get-api-description [request] + (let [body [{:url "ledger/accounts" :name "accounts"} + {:url "ledger/securities" :name "securities"}]] + (build-response request body))) + +(defn create-routes-for-ledger [securities conn] + (routes + (context "/api/1" [] + (GET "/" get-api-description) + (GET "/ledger/accounts" [] (get-available-accounts conn)) + (GET "/ledger/accounts/:account-id" [] (get-account conn)) + (GET "/ledger/securities" [] (get-available-securities conn)) ;; done + (POST "/ledger/securities" [] (create-security conn)) ;; done + (GET "/ledger/securities/:sec-id" [] (get-security conn)) ;; done + (PUT "/ledger/securities/:sec-id" [] (update-security conn)) ;; not done + (POST "/ledger/securities/:sec-id" [] (emit-securities conn))) ;; done + (not-found "not found"))) + + +(defn -main [config exchange-apis] + (let [ledger-config (:ledger-server config) + conn (skirmish.ledger.accounts/create-connection (-> config :datomic :transactor-url)) + api-routes (create-routes-for-ledger (:securities config) conn)] + + (println "Running skirmish.ledger.core server with bellow config\n" ledger-config) + (run-server (api api-routes) ledger-config))) diff --git a/src/clj/skirmish/rest/util.clj b/src/clj/skirmish/rest/util.clj index 49b56d2..9068141 100644 --- a/src/clj/skirmish/rest/util.clj +++ b/src/clj/skirmish/rest/util.clj @@ -1,5 +1,7 @@ (ns skirmish.rest.util (:require [cheshire.core :refer [generate-string]] + [slingshot.slingshot :refer [try+ throw+]] + [clojure.edn :as edn] [clojure.string :refer [split starts-with? ends-with?]])) ;; Access-Control-Allow-Origin @@ -40,8 +42,24 @@ (let [[_ serializer] (get-best-serializer request)] (serializer body))) -(defn build-response [request {status :status body :body :as response}] - (let [[content-type serializer] (get-best-serializer request)] +(defn build-response [request {status :status body :body headers :headers :as response}] + (let [[content-type serializer] (get-best-serializer request) + ret-headers (if (some? headers) headers {})] {:body (serializer (if (nil? body) response body)) - :headers {"Content-Type" content-type} - :status (get-status status request)})) \ No newline at end of file + :headers (into {"Content-Type" content-type} ret-headers) + :status (get-status status request)})) + +(defn get-body [body] + (try+ + (-> body clojure.java.io/reader slurp edn/read-string) + (catch Exception e + (throw+ {:error-type :4XX :status 400 :body {:error "Invalid body format. Body must be edn format."}})))) + +(defn string->stream + ([s] (string->stream s "UTF-8")) + ([s encoding] + (-> s + (.getBytes encoding) + (java.io.ByteArrayInputStream.)))) + +(def edn->stream (comp string->stream str)) \ No newline at end of file diff --git a/src/clj/skirmish/util.clj b/src/clj/skirmish/util.clj index 995a47c..a6dba89 100644 --- a/src/clj/skirmish/util.clj +++ b/src/clj/skirmish/util.clj @@ -10,4 +10,21 @@ (defn block-thread [] "Block the current thread until chuck norris gets weaker." - (async/ evt .-target .-value)))) + + + +(defn select-type [order] + [:div.control + [:div.select + [:select {:on-change (partial set-value-in-atom order :type)} + [:option {:value :buy} "Buy"] + [:option {:value :sell} "Sell"]]]]) + +(defn select-securities [order] + [:div.control + [:div.select + [:select {:on-change (partial set-value-in-atom order :sec-id)} + [:option {:value :nype} ":nype"] + [:option {:value :osko} ":osko"] + [:option {:value :oops} ":oops"]]]]) + +(defn select-quantities [order] + [:div.control + [:input.input {:type "number" + :on-change (partial set-value-in-atom order :quantity)}]]) + +(def action-button + [:div.control + [:button.button.is-primary "Submit"]]) + (defn page [] - [:h1 "create order page"]) \ No newline at end of file + (let [order (atom {})] + (fn [] + [:div + [:div.columns + [:div.column.content + [:div.columns.is-centered + [:column + [:div.field.has-addons + (select-type order) + (select-securities order) + (select-quantities order) + action-button]]]]] + [:div.columns + [:div.column.content + [:pre (str @order)]]]]))) + diff --git a/src/cljs/skirmish/wui/components/home_page.cljs b/src/cljs/skirmish/wui/components/home_page.cljs index 01310ca..95fc149 100644 --- a/src/cljs/skirmish/wui/components/home_page.cljs +++ b/src/cljs/skirmish/wui/components/home_page.cljs @@ -1,4 +1,16 @@ -(ns skirmish.wui.components.home-page) +(ns skirmish.wui.components.home-page + (:require [reagent.core :refer [atom]])) + + +(def lorem-ipsum ["Lorem ipsum dolor amet pickled bushwick next level adaptogen ugh franzen salvia hoodie man bun organic kinfolk. Hammock pickled banjo, artisan single-origin coffee brunch 90's occupy taxidermy. Put a bird on it gentrify offal tilde. Authentic put a bird on it prism semiotics, roof party banh mi typewriter. XOXO schlitz sriracha godard." + "Succulents shoreditch twee, VHS hammock hot chicken distillery. Edison bulb aesthetic pork belly meggings. 3 wolf moon artisan keytar blue bottle. Actually pok pok asymmetrical, banh mi before they sold out PBR&B portland tumblr." + "Gentrify palo santo enamel pin, tumeric jianbing thundercats shaman green juice chambray yr. Kale chips synth meditation chambray thundercats helvetica, cornhole whatever kickstarter ennui pop-up lo-fi godard. Austin unicorn brunch woke hammock. Coloring book kombucha occupy tofu wayfarers."]) + (defn page [state] - [:h1 "Hello world sdjjksd dsklsd"]) \ No newline at end of file + [:div + [:section.section + [:div.container + [:h1.title "lorem ipsum"] + (for [sentence lorem-ipsum] + [:p sentence])]]]) \ No newline at end of file diff --git a/src/cljs/skirmish/wui/components/portfolio_page.cljs b/src/cljs/skirmish/wui/components/portfolio_page.cljs index a48e3a5..c0fa363 100644 --- a/src/cljs/skirmish/wui/components/portfolio_page.cljs +++ b/src/cljs/skirmish/wui/components/portfolio_page.cljs @@ -1,4 +1,21 @@ -(ns skirmish.wui.components.portfolio-page) +(ns skirmish.wui.components.portfolio-page + (:require [reagent.core :refer [atom]] + [skirmish.wui.components.secunit :refer [secunit]])) + +(defn make-portfolio-summary-table [portfolio] + [:table + [:thead + [:tr [:th "Security"] [:th "Amount"] [:th "Face Value"]]] + [:tbody + (for [sec-summary portfolio] + [:tr + [:td (:sec-id sec-summary)] + [:td [secunit (:sec-id sec-summary) (:amount sec-summary)]] + [:td [secunit :dapm (:face-value sec-summary)]]])]]) (defn page [] - [:h1 "portfolio page"]) \ No newline at end of file + (let [porfolio [{:sec-id :foo :amount 100 :face-value 1265} + {:sec-id :toto :amount 100 :face-value 1265} + {:sec-id :foo :amount 100 :face-value 1265}]] + [:div.content + (make-portfolio-summary-table porfolio)])) diff --git a/src/cljs/skirmish/wui/components/secunit.cljs b/src/cljs/skirmish/wui/components/secunit.cljs new file mode 100644 index 0000000..2c9d582 --- /dev/null +++ b/src/cljs/skirmish/wui/components/secunit.cljs @@ -0,0 +1,5 @@ +(ns skirmish.wui.components.secunit) + +(defn secunit [sec-id amount] + (let [href (str "/securities/" sec-id)] + [:a {:href href} (str amount)])) \ No newline at end of file diff --git a/test/clj/skirmish/ledger/test_core.clj b/test/clj/skirmish/ledger/test_core.clj index 3537c45..12141a6 100644 --- a/test/clj/skirmish/ledger/test_core.clj +++ b/test/clj/skirmish/ledger/test_core.clj @@ -1,2 +1,183 @@ (ns skirmish.ledger.test-core - (:require [clojure.test :refer :all])) + (:require [midje.sweet :refer :all] + [datomic.api :as d] + [clojure.edn] + [skirmish.rest.util :refer [build-response]] + [skirmish.ledger.core :refer :all] + [skirmish.rest.util :refer [edn->stream]] + [clojure.spec.alpha :as s] + [skirmish.ledger.accounts :refer [create-connection schema]])) + +(defn spec-check [spec] + #(s/valid? spec %)) + +(defn body [resp] + (-> resp :body clojure.edn/read-string)) + +(s/def ::ok #(<= 200 (:status %) 299)) + +(s/def ::status-404 #(= (:status %) 404)) + +(s/def ::status-400 #(= (:status %) 400)) + + +(defn create-conn [] + (let [mem-url "datomic:mem://skirmish"] + (d/delete-database mem-url) + (create-connection mem-url))) + + + +(defn location? [path] + #(-> % :headers (get "location") (clojure.string/ends-with? path))) + + +(def sec->hash-set #(apply hash-set %)) + + +(defn create-some-securities [conn] + (let [cs (create-security conn) + create-security-1 {:body (edn->stream [:sec-1 100]) :headers {"accept" "application/edn"}} + create-security-2 {:body (edn->stream [:sec-2 200]) :headers {"accept" "application/edn"}} + create-security-3 {:body (edn->stream [:sec-3 300]) :headers {"accept" "application/edn"}} + create-security-4 {:body (edn->stream [:zimbabwean-dollar 1000000]) :headers {"accept" "application/edn"}}] + (->> create-security-1 cs (s/assert ::ok)) + (->> create-security-2 cs (s/assert ::ok)) + (->> create-security-3 cs (s/assert ::ok)) + (->> create-security-4 cs (s/assert ::ok)) + conn)) + + +(defn create-some-accounts [conn] + (let [es (emit-securities conn) + emit-sec-req-1 {:body (edn->stream [100 "1-sec-1"]) :route-params {:sec-id "sec-1"}} + emit-sec-req-2 {:body (edn->stream [200 "2-sec-1"]) :route-params {:sec-id "sec-1"}} + emit-sec-req-3 {:body (edn->stream [1000 "1-sec-2"]) :route-params {:sec-id "sec-2"}} + emit-sec-req-4 {:body (edn->stream [2000 "2-sec-2"]) :route-params {:sec-id "sec-2"}}] + (->> emit-sec-req-1 es (s/assert ::ok)) + (->> emit-sec-req-2 es (s/assert ::ok)) + (->> emit-sec-req-3 es (s/assert ::ok)) + (->> emit-sec-req-4 es (s/assert ::ok)) + conn)) + + +(def create-some-securities-and-accounts (comp create-some-accounts create-some-securities)) + + +(facts "about the securities" + (fact "We should be able to create a security" + (let [conn (create-conn) + security [:new-currency 100] + request {:body (edn->stream security) :headers {"accept" "application/edn"}} + resp ((create-security conn) request)] + resp => (spec-check ::ok) + resp => (location? "/api/1/ledger/securities/new-currency"))) ;; + + (fact "We should not be able to create a security that already exists" + (let [conn (create-conn) + security [:new-currency 100] + request {:body (edn->stream security) :headers {"accept" "application/edn"}} + resp-1 ((create-security conn) request) + resp-2 ((create-security conn) request)] + resp-1 => (spec-check ::ok) + resp-2 => (spec-check ::status-400))) + + (fact "We should be able to update a security when it exists" + :todo => :todo) + + (fact "we should be able to get-security if it does exists" + (let [conn (create-conn) + security [:new-currency 100] + create-request {:body (edn->stream security) :headers {"accept" "application/edn"}} + _ ((create-security conn) create-request) + request {:route-params {:sec-id :new-currency} :headers {"accept" "application/edn"}} + resp ((get-security conn) request)] + resp => (spec-check ::ok) + (body resp) => {:sec-id :new-currency :interest-rate 100})) + + (fact "we should not be able to get-security if it does not exists" + (let [conn (create-conn) + request {:route-params {:sec-id :new-currency} :headers {"accept" "application/edn"}} + resp ((get-security conn) request)] + resp => (spec-check ::status-404) + resp => #(-> % :sec-id nil?))) + + (fact "we should be able to get a list of all securities" + (let [conn (create-conn) + request {:route-params {} :headers {"accept" "application/edn"}} + empty-res ((get-available-securities conn) request) + create-security-1 {:body (edn->stream [:new-currency 100]) :headers {"accept" "application/edn"}} + _ ((create-security conn) create-security-1) + one-res ((get-available-securities conn) request)] + empty-res => (spec-check ::ok) + one-res => (spec-check ::ok) + (body empty-res) => {:securities []} + (body one-res) => {:securities [{:sec-id :new-currency :url "/api/1/ledger/securities/new-currency"}]}))) + + +(facts "about the emitting securities" + (fact "should not work if invalid request" + (let [conn (create-some-securities (create-conn)) + req-1 {:body (edn->stream [10 :not-a-string]) :route-params {:sec-id "sec-1"} :headers {"accept" "application/edn"}} + req-2 {:body (edn->stream [-10 "foo"]) :route-params {:sec-id "sec-1"} :headers {"accept" "application/edn"}} + req-3 {:body (edn->stream [90 "account-id"]) :route-params {:sec-id "invalid-sec-id"} :headers {"accept" "application/edn"}}] + ((emit-securities conn) req-1) => (spec-check ::status-400) + ((emit-securities conn) req-2) => (spec-check ::status-400) + ((emit-securities conn) req-3) => (spec-check ::status-404))) + + (fact "should create new account if targeted account does not exists" + (let [conn (create-some-securities (create-conn)) + get-account-req {:route-params {:account-id "new-account-id"} :headers {"accept" "application/edn"}} + emit-sec-req {:body (edn->stream [90 "new-account-id"]) :route-params {:sec-id "sec-1"} :headers {"accept" "application/edn"}}] + ((get-account conn) get-account-req) => (spec-check ::status-404) + ((emit-securities conn) emit-sec-req) => (spec-check ::ok) + (body ((get-account conn) get-account-req)) => #(-> % :account :amount (= 90)))) + + (fact "should add to account if targeted account exists" + (let [conn (create-some-securities (create-conn)) + get-account-req {:route-params {:account-id "new-account-id"} :headers {"accept" "application/edn"}} + emit-sec-req-1 {:body (edn->stream [90 "new-account-id"]) :route-params {:sec-id "sec-1"} :headers {"accept" "application/edn"}} + emit-sec-req-2 {:body (edn->stream [100 "new-account-id"]) :route-params {:sec-id "sec-1"} :headers {"accept" "application/edn"}}] + ((get-account conn) get-account-req) => (spec-check ::status-404) + ((emit-securities conn) emit-sec-req-1) => (spec-check ::ok) + ((emit-securities conn) emit-sec-req-2) => (spec-check ::ok) + (body ((get-account conn) get-account-req)) => #(-> % :account :amount (= 190))))) + + +(facts "about accounts" + (fact "we should be able to get list of all accounts" + (let [conn (create-some-securities-and-accounts (create-conn)) + req {:route-params {} :headers {"accept" "application/edn"}} + expt [{:account-public-key "1-sec-1" :url "/api/1/ledger/accounts/1-sec-1"} + {:account-public-key "1-sec-2" :url "/api/1/ledger/accounts/1-sec-2"} + {:account-public-key "2-sec-1" :url "/api/1/ledger/accounts/2-sec-1"} + {:account-public-key "2-sec-2" :url "/api/1/ledger/accounts/2-sec-2"}]] + ((get-available-accounts conn) req) => (spec-check ::ok) + (-> ((get-available-accounts conn) req) body :accounts sec->hash-set) => (sec->hash-set expt))) + + (fact "we should be able to get account" + (let [conn (create-some-securities-and-accounts (create-conn)) + req {:route-params {:account-id "2-sec-1"} :headers {"accept" "application/edn"}}] + ((get-account conn) req) => (spec-check ::ok) + (body ((get-account conn) req)) => {:account {:account-public-key "2-sec-1" :amount 200 :sec-id :sec-1} + :sec-url "/api/1/securities/sec-1"})) + + (fact "the interest should be payed when getting the account" + (let [conn (create-some-securities-and-accounts (create-conn)) + req {:route-params {:account-id "2-sec-1"} :headers {"accept" "application/edn"}}] + ((get-account conn) req) => (spec-check ::ok) + (body ((get-account conn) req)) => {:account {:account-public-key "2-sec-1" :amount 200 :sec-id :sec-1} + :sec-url "/api/1/securities/sec-1"})) + + + (fact "we should not get an account if it does not exists" + (let [conn (create-some-securities-and-accounts (create-conn)) + req {:route-params {:account-id "2-sec-10"} :headers {"accept" "application/edn"}}] + ((get-account conn) req) => (spec-check ::status-404)))) + + + +;; TODO +;; 4. transfer sec from one account to the other. +;; 5. account for interest + diff --git a/test/clj/skirmish/test_util.clj b/test/clj/skirmish/test_util.clj new file mode 100644 index 0000000..1ca9ada --- /dev/null +++ b/test/clj/skirmish/test_util.clj @@ -0,0 +1,24 @@ +(ns skirmish.test-util + (:require [midje.sweet :refer :all] + [skirmish.util :refer :all])) + + +;; midje testing +(facts "About events overlapping" + (let [evt-1 #:calendar.event{:start 10 :end 15} + evt-2 #:calendar.event{:start 12 :end 17} + evt-3 #:calendar.event{:start 15 :end 25} + + evt-4 #:calendar.event{:start 30 :end 40} + evt-5 #:calendar.event{:start 33 :end 34}] + + (fact "about edge cases" + (get-overlapping-events nil) => empty? + (get-overlapping-events []) => empty?) + + (fact "about regular situations" + (get-overlapping-events [evt-1 evt-2]) => [[evt-1 evt-2]] + (get-overlapping-events [evt-1 evt-2 evt-3]) => [[evt-1 evt-2] [evt-2 evt-3]] + (get-overlapping-events [evt-4 evt-5]) => [[evt-4 evt-5]]))) + +;; \ No newline at end of file