|
|
|
@@ -7,6 +7,7 @@
|
|
|
|
|
(:import
|
|
|
|
|
[java.net URI]
|
|
|
|
|
[java.net.http HttpClient HttpRequest HttpResponse$BodyHandlers]
|
|
|
|
|
[java.time Duration Instant]
|
|
|
|
|
[org.postgresql.util PGobject]))
|
|
|
|
|
|
|
|
|
|
(defn- ->jsonb
|
|
|
|
@@ -29,6 +30,58 @@
|
|
|
|
|
body (.body resp)]
|
|
|
|
|
body))
|
|
|
|
|
|
|
|
|
|
(defn- post-quote
|
|
|
|
|
"POST /v1/currency-exchange-quotes. Returns parsed response map or nil."
|
|
|
|
|
[^HttpClient client ^String api-key ^String eur-amount]
|
|
|
|
|
(let [body-str (json/write-str {:sell "EUR" :buy "BTC"
|
|
|
|
|
:amount {:amount eur-amount :currency "EUR"}})
|
|
|
|
|
request (-> (HttpRequest/newBuilder)
|
|
|
|
|
(.uri (URI. "https://api.strike.me/v1/currency-exchange-quotes"))
|
|
|
|
|
(.header "Accept" "application/json")
|
|
|
|
|
(.header "Content-Type" "application/json")
|
|
|
|
|
(.header "Authorization" (str "Bearer " api-key))
|
|
|
|
|
(.POST (java.net.http.HttpRequest$BodyPublishers/ofString body-str))
|
|
|
|
|
(.build))
|
|
|
|
|
resp (.send client request (HttpResponse$BodyHandlers/ofString))
|
|
|
|
|
status (.statusCode resp)
|
|
|
|
|
body (.body resp)]
|
|
|
|
|
(if (= 200 status)
|
|
|
|
|
(json/read-str body :key-fn keyword)
|
|
|
|
|
(do (log/warn "Strike quote HTTP" status body)
|
|
|
|
|
nil))))
|
|
|
|
|
|
|
|
|
|
(def ^:private fee-max-age (Duration/ofHours 24))
|
|
|
|
|
|
|
|
|
|
(defn- refresh-fee!
|
|
|
|
|
"Fetch fee for sats-eur-amount and cache it. Returns the fee BigDecimal or nil."
|
|
|
|
|
[client api-key sats-eur-amount fee-cache]
|
|
|
|
|
(when-let [parsed (post-quote client api-key sats-eur-amount)]
|
|
|
|
|
(let [fee (bigdec (get-in parsed [:fee :amount]))]
|
|
|
|
|
(log/info "Strike fee for" sats-eur-amount "EUR:" fee "EUR")
|
|
|
|
|
(reset! fee-cache {:fee fee :fetched-at (Instant/now)})
|
|
|
|
|
fee)))
|
|
|
|
|
|
|
|
|
|
(defn- cached-fee
|
|
|
|
|
"Return cached fee if fresh, otherwise refresh. Returns BigDecimal or nil."
|
|
|
|
|
[client api-key sats-eur-amount fee-cache]
|
|
|
|
|
(let [{:keys [fee fetched-at]} @fee-cache]
|
|
|
|
|
(if (and fee fetched-at
|
|
|
|
|
(.isBefore (Instant/now) (.plus ^Instant fetched-at fee-max-age)))
|
|
|
|
|
fee
|
|
|
|
|
(refresh-fee! client api-key sats-eur-amount fee-cache))))
|
|
|
|
|
|
|
|
|
|
(defn- fetch-quote-sats
|
|
|
|
|
"Get sats for a total EUR amount (including fee). Uses cached fee to calculate
|
|
|
|
|
pre-fee amount, then quotes that. Returns sats as long or nil."
|
|
|
|
|
[client api-key sats-eur-amount fee-cache]
|
|
|
|
|
(let [total (bigdec sats-eur-amount)
|
|
|
|
|
fee (cached-fee client api-key sats-eur-amount fee-cache)
|
|
|
|
|
pre-fee (if fee (.toPlainString (.subtract total fee)) sats-eur-amount)]
|
|
|
|
|
(when-let [parsed (post-quote client api-key pre-fee)]
|
|
|
|
|
(let [btc-amt (get-in parsed [:target :amount])]
|
|
|
|
|
(when btc-amt
|
|
|
|
|
(long (* (bigdec btc-amt) 100000000)))))))
|
|
|
|
|
|
|
|
|
|
(defn- format-rates
|
|
|
|
|
"Build a compact log string from the parsed rates array."
|
|
|
|
|
[rates]
|
|
|
|
@@ -47,36 +100,49 @@
|
|
|
|
|
|
|
|
|
|
(defn- poll!
|
|
|
|
|
"Fetch rates and insert into the database. Updates latest-atom."
|
|
|
|
|
[client api-key query-fn latest-atom]
|
|
|
|
|
[client api-key query-fn latest-atom sats-eur-amount fee-cache]
|
|
|
|
|
(let [body (fetch-rates client api-key)
|
|
|
|
|
rates (json/read-str body :key-fn keyword)]
|
|
|
|
|
(query-fn :insert-strike-price! {:rates (->jsonb body)})
|
|
|
|
|
(reset! latest-atom (rates->map rates))
|
|
|
|
|
rates (json/read-str body :key-fn keyword)
|
|
|
|
|
rmap (rates->map rates)
|
|
|
|
|
quote-sats (when sats-eur-amount
|
|
|
|
|
(try
|
|
|
|
|
(let [sats (fetch-quote-sats client api-key sats-eur-amount fee-cache)]
|
|
|
|
|
(when sats
|
|
|
|
|
(log/info "Strike quote:" sats-eur-amount "EUR ->" sats "sats"))
|
|
|
|
|
sats)
|
|
|
|
|
(catch Exception e
|
|
|
|
|
(log/error e "Strike quote fetch failed")
|
|
|
|
|
nil)))
|
|
|
|
|
rmap (if quote-sats (assoc rmap "quote-sats" quote-sats) rmap)]
|
|
|
|
|
(query-fn :insert-strike-price! {:rates (->jsonb body)
|
|
|
|
|
:quote-sats quote-sats})
|
|
|
|
|
(reset! latest-atom rmap)
|
|
|
|
|
(log/info "Strike:" (format-rates rates))))
|
|
|
|
|
|
|
|
|
|
(defn- start-poll-loop!
|
|
|
|
|
"Fetch immediately, then poll every 10 seconds."
|
|
|
|
|
[client api-key query-fn running? latest-atom]
|
|
|
|
|
[client api-key query-fn running? latest-atom sats-eur-amount fee-cache]
|
|
|
|
|
(future
|
|
|
|
|
(try
|
|
|
|
|
(poll! client api-key query-fn latest-atom)
|
|
|
|
|
(poll! client api-key query-fn latest-atom sats-eur-amount fee-cache)
|
|
|
|
|
(catch Exception e
|
|
|
|
|
(log/error e "Strike initial fetch failed")))
|
|
|
|
|
(while @running?
|
|
|
|
|
(Thread/sleep 10000)
|
|
|
|
|
(when @running?
|
|
|
|
|
(try
|
|
|
|
|
(poll! client api-key query-fn latest-atom)
|
|
|
|
|
(poll! client api-key query-fn latest-atom sats-eur-amount fee-cache)
|
|
|
|
|
(catch Exception e
|
|
|
|
|
(log/error e "Strike poll failed")))))))
|
|
|
|
|
|
|
|
|
|
(defmethod ig/init-key :strike/ticker
|
|
|
|
|
[_ {:keys [query-fn api-key]}]
|
|
|
|
|
[_ {:keys [query-fn api-key sats-eur-amount]}]
|
|
|
|
|
(log/info "Starting Strike ticker poller")
|
|
|
|
|
(let [client (HttpClient/newHttpClient)
|
|
|
|
|
running? (atom true)
|
|
|
|
|
latest-rates (atom nil)
|
|
|
|
|
fut (start-poll-loop! client api-key query-fn running? latest-rates)]
|
|
|
|
|
fee-cache (atom nil)
|
|
|
|
|
fut (start-poll-loop! client api-key query-fn running? latest-rates sats-eur-amount fee-cache)]
|
|
|
|
|
{:running? running?
|
|
|
|
|
:future fut
|
|
|
|
|
:latest-rates latest-rates}))
|
|
|
|
|