implemented so many things...
This commit is contained in:
1 parent
fd843a5425
commit
bce64fc8f6
24 files changed
+734
-59
No files matched your search
@@ -16,3 +16,4 @@ full-stack
|
||||
fixtures
|
||||
node_modules
|
||||
figwheel_server.log
|
||||
.rebel_readline_history
|
||||
@@ -1,4 +1,5 @@
|
||||
FROM clojure
|
||||
MAINTAINER Didier Amyot <didier.amyot@gmail.com>
|
||||
|
||||
# Set the working directory to /app
|
||||
WORKDIR /app
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
FROM openjdk:11-jdk
|
||||
MAINTAINER Didier Amyot <didier.amyot@gmail.com>
|
||||
|
||||
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"]
|
||||
@@ -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
|
||||
+32
-22
@@ -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"
|
||||
+6
@@ -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))
|
||||
+9
-4
@@ -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"
|
||||
|
||||
+36
-4
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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])
|
||||
@@ -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/<!! finished-threads)]
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
(defn -main [config exchange-apis]
|
||||
(let [webserver-config (:api-server config)
|
||||
api-routes (create-routes-for-exchange exchange-apis)]
|
||||
(println "Running server with bellow config")
|
||||
(println webserver-config)
|
||||
(println "Running skirmish.exchange-gateway.core server with bellow config\n" webserver-config)
|
||||
(run-server (api api-routes) webserver-config)))
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@
|
||||
[compojure.handler :refer [api]]
|
||||
[compojure.core :refer :all]
|
||||
[skirmish.order] ;; for spec side effect
|
||||
[skirmish.rest.util :refer [build-response build-response-body]]
|
||||
[skirmish.rest.util :refer [build-response build-response-body get-body]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[kinsky.client]
|
||||
[slingshot.slingshot :refer [try+ throw+]]
|
||||
[clojure.core.async :refer [chan go go-loop close! <! >!! 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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)))
|
||||
@@ -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)}))
|
||||
: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))
|
||||
@@ -10,4 +10,21 @@
|
||||
|
||||
(defn block-thread []
|
||||
"Block the current thread until chuck norris gets weaker."
|
||||
(async/<!! (async/chan)))
|
||||
(async/<!! (async/chan)))
|
||||
|
||||
(defn- get-overlapping-for-specific-event [event sorted-events]
|
||||
(let [event-end (:calendar.event/end event)]
|
||||
(take-while #(< (:calendar.event/start %) event-end) sorted-events)))
|
||||
|
||||
|
||||
(defn get-overlapping-events [events]
|
||||
(let [sorted-events (sort-by :calendar.event/start events)]
|
||||
(loop [events sorted-events ret []]
|
||||
(if (seq events)
|
||||
(let [first-event (first events)
|
||||
rest-events (rest events)
|
||||
overlapping-events (get-overlapping-for-specific-event first-event rest-events)
|
||||
overlapping-pairs (map vector (repeat first-event) overlapping-events)
|
||||
new-ret (if (seq overlapping-pairs) (into ret overlapping-pairs) ret)]
|
||||
(recur rest-events new-ret))
|
||||
ret))))
|
||||
@@ -1,4 +1,50 @@
|
||||
(ns skirmish.wui.components.create-order-page)
|
||||
(ns skirmish.wui.components.create-order-page
|
||||
(:require [reagent.core :refer [atom]]))
|
||||
|
||||
(defn set-value-in-atom [atm key evt]
|
||||
(swap! atm #(assoc % key (-> 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"])
|
||||
(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)]]]])))
|
||||
|
||||
@@ -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"])
|
||||
[:div
|
||||
[:section.section
|
||||
[:div.container
|
||||
[:h1.title "lorem ipsum"]
|
||||
(for [sentence lorem-ipsum]
|
||||
[:p sentence])]]])
|
||||
@@ -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"])
|
||||
(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)]))
|
||||
@@ -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)]))
|
||||
@@ -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
|
||||
|
||||
@@ -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]])))
|
||||
|
||||
;;
|
||||
Reference in new issue
Block a user