[feature] use new block signal to get new transactions
- remove the transaction fetching loop entirely to rely only on subscription for live transactions and token transfer updates - fetch token transfers history via etherscan API to lift the 100000 blocks limit on token transfers history - inbound token transfers are catched via a filter on ethlogs - outbound token transfers and other transactions are catched by filtering transaction in current block that have the wallet address as to or from field
This commit is contained in:
parent
f5c18ae7a9
commit
b274ed9fa9
|
@ -85,9 +85,9 @@
|
||||||
(fx/defn initialize-wallet [cofx]
|
(fx/defn initialize-wallet [cofx]
|
||||||
(fx/merge cofx
|
(fx/merge cofx
|
||||||
(models.wallet/initialize-tokens)
|
(models.wallet/initialize-tokens)
|
||||||
|
(transactions/initialize)
|
||||||
(ethereum.subscriptions/initialize)
|
(ethereum.subscriptions/initialize)
|
||||||
(models.wallet/update-wallet)
|
(models.wallet/update-wallet)))
|
||||||
(transactions/start-sync)))
|
|
||||||
|
|
||||||
(fx/defn user-login [{:keys [db] :as cofx} create-database?]
|
(fx/defn user-login [{:keys [db] :as cofx} create-database?]
|
||||||
(let [{:keys [address password]} (accounts.db/credentials cofx)]
|
(let [{:keys [address password]} (accounts.db/credentials cofx)]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
(ns status-im.accounts.logout.core
|
(ns status-im.accounts.logout.core
|
||||||
(:require [re-frame.core :as re-frame]
|
(:require [re-frame.core :as re-frame]
|
||||||
[status-im.chaos-mode.core :as chaos-mode]
|
[status-im.chaos-mode.core :as chaos-mode]
|
||||||
[status-im.ethereum.transactions.core :as transactions]
|
|
||||||
[status-im.i18n :as i18n]
|
[status-im.i18n :as i18n]
|
||||||
[status-im.init.core :as init]
|
[status-im.init.core :as init]
|
||||||
[status-im.node.core :as node]
|
[status-im.node.core :as node]
|
||||||
|
@ -13,7 +12,6 @@
|
||||||
(fx/merge cofx
|
(fx/merge cofx
|
||||||
{:keychain/clear-user-password (get-in db [:account/account :address])
|
{:keychain/clear-user-password (get-in db [:account/account :address])
|
||||||
:dev-server/stop nil}
|
:dev-server/stop nil}
|
||||||
(transactions/stop-sync)
|
|
||||||
(transport/stop-whisper
|
(transport/stop-whisper
|
||||||
#(re-frame/dispatch [:accounts.logout/filters-removed]))
|
#(re-frame/dispatch [:accounts.logout/filters-removed]))
|
||||||
(chaos-mode/stop-checking)))
|
(chaos-mode/stop-checking)))
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
(:require [clojure.string :as string]
|
(:require [clojure.string :as string]
|
||||||
[re-frame.core :as re-frame]
|
[re-frame.core :as re-frame]
|
||||||
[status-im.constants :as constants]
|
[status-im.constants :as constants]
|
||||||
[status-im.ethereum.decode :as decode]
|
[status-im.ethereum.transactions.core :as transactions]
|
||||||
[status-im.native-module.core :as status]
|
[status-im.native-module.core :as status]
|
||||||
[status-im.utils.ethereum.core :as ethereum]
|
[status-im.utils.ethereum.core :as ethereum]
|
||||||
[status-im.utils.ethereum.tokens :as tokens]
|
[status-im.utils.ethereum.tokens :as tokens]
|
||||||
|
@ -10,57 +10,6 @@
|
||||||
[status-im.utils.types :as types]
|
[status-im.utils.types :as types]
|
||||||
[taoensso.timbre :as log]))
|
[taoensso.timbre :as log]))
|
||||||
|
|
||||||
;; NOTE: this is the safe block range that can be
|
|
||||||
;; queried from infura rpc gateway without getting timeouts
|
|
||||||
;; determined experimentally by @goranjovic
|
|
||||||
(def block-query-limit 100000)
|
|
||||||
|
|
||||||
(defn get-latest-block [callback]
|
|
||||||
(status/call-private-rpc
|
|
||||||
(types/json->clj {:jsonrpc "2.0"
|
|
||||||
:id 1
|
|
||||||
:method "eth_blockNumber"
|
|
||||||
:params []})
|
|
||||||
(fn [response]
|
|
||||||
(if (string/blank? response)
|
|
||||||
(log/warn :web3-response-error)
|
|
||||||
(callback (-> (.parse js/JSON response)
|
|
||||||
(js->clj :keywordize-keys true)
|
|
||||||
:result
|
|
||||||
decode/uint))))))
|
|
||||||
|
|
||||||
(defn get-block-by-hash [block-hash callback]
|
|
||||||
(status/call-private-rpc
|
|
||||||
(types/json->clj {:jsonrpc "2.0"
|
|
||||||
:id 1
|
|
||||||
:method "eth_getBlockByHash"
|
|
||||||
:params [block-hash false]})
|
|
||||||
(fn [response]
|
|
||||||
(if (string/blank? response)
|
|
||||||
(log/warn :web3-response-error)
|
|
||||||
(callback (-> (.parse js/JSON response)
|
|
||||||
(js->clj :keywordize-keys true)
|
|
||||||
:result
|
|
||||||
(update :number decode/uint)
|
|
||||||
(update :timestamp decode/uint)))))))
|
|
||||||
|
|
||||||
(defn- get-token-transfer-logs
|
|
||||||
[from-block {:keys [chain-tokens direction from to]} callback]
|
|
||||||
(status/call-private-rpc
|
|
||||||
(types/json->clj {:jsonrpc "2.0"
|
|
||||||
:id 2
|
|
||||||
:method "eth_getLogs"
|
|
||||||
:params
|
|
||||||
[{:address (keys chain-tokens)
|
|
||||||
:fromBlock from-block
|
|
||||||
:topics [constants/event-transfer-hash from to]}]})
|
|
||||||
(fn [response]
|
|
||||||
(if (string/blank? response)
|
|
||||||
(log/warn :web3-response-error)
|
|
||||||
(callback (-> (.parse js/JSON response)
|
|
||||||
(js->clj :keywordize-keys true)
|
|
||||||
:result))))))
|
|
||||||
|
|
||||||
(fx/defn handle-signal
|
(fx/defn handle-signal
|
||||||
[cofx {:keys [subscription_id data] :as event}]
|
[cofx {:keys [subscription_id data] :as event}]
|
||||||
(if-let [handler (get-in cofx [:db :ethereum/subscriptions subscription_id])]
|
(if-let [handler (get-in cofx [:db :ethereum/subscriptions subscription_id])]
|
||||||
|
@ -75,9 +24,37 @@
|
||||||
[{:keys [db]} id handler]
|
[{:keys [db]} id handler]
|
||||||
{:db (assoc-in db [:ethereum/subscriptions id] handler)})
|
{:db (assoc-in db [:ethereum/subscriptions id] handler)})
|
||||||
|
|
||||||
|
(defn keep-user-transactions
|
||||||
|
[wallet-address transactions]
|
||||||
|
(keep (fn [{:keys [to from] :as transaction}]
|
||||||
|
(when-let [direction (cond
|
||||||
|
(= wallet-address to) :inbound
|
||||||
|
(= wallet-address from) :outbound)]
|
||||||
|
(assoc transaction :direction direction)))
|
||||||
|
transactions))
|
||||||
|
|
||||||
(fx/defn new-block
|
(fx/defn new-block
|
||||||
[{:keys [db]} block-number]
|
[{:keys [db] :as cofx} {:keys [number transactions] :as block}]
|
||||||
{:db (assoc-in db [:ethereum/current-block] block-number)})
|
(when number
|
||||||
|
(let [{:keys [:account/account :wallet/all-tokens network
|
||||||
|
:ethereum/current-block]} db
|
||||||
|
chain (ethereum/network->chain-keyword (get-in account [:networks network]))
|
||||||
|
chain-tokens (into {} (map (juxt :address identity)
|
||||||
|
(tokens/tokens-for all-tokens chain)))
|
||||||
|
wallet-address (ethereum/normalized-address (:address account))
|
||||||
|
token-contracts-addresses (into #{} (keys chain-tokens))]
|
||||||
|
(fx/merge cofx
|
||||||
|
{:db (assoc-in db [:ethereum/current-block] number)
|
||||||
|
:ethereum.transactions/enrich-transactions-from-new-blocks
|
||||||
|
{:chain-tokens chain-tokens
|
||||||
|
:block block
|
||||||
|
:transactions (keep-user-transactions wallet-address
|
||||||
|
transactions)}}
|
||||||
|
(when (or (not current-block)
|
||||||
|
(not= number (inc current-block)))
|
||||||
|
;; in case we skipped some blocks or got an uncle, re-fetch history
|
||||||
|
;; from etherscan
|
||||||
|
(transactions/initialize))))))
|
||||||
|
|
||||||
(defn subscribe-signal
|
(defn subscribe-signal
|
||||||
[filter params callback]
|
[filter params callback]
|
||||||
|
@ -98,139 +75,33 @@
|
||||||
result
|
result
|
||||||
callback])))))))
|
callback])))))))
|
||||||
|
|
||||||
(defn- add-padding [address]
|
|
||||||
{:pre [(string? address)]}
|
|
||||||
(str "0x000000000000000000000000" (subs address 2)))
|
|
||||||
|
|
||||||
(defn- remove-padding [topic]
|
|
||||||
{:pre [(string? topic)]}
|
|
||||||
(str "0x" (subs topic 26)))
|
|
||||||
|
|
||||||
(defn- parse-transaction-entries [timestamp chain-tokens direction transfers]
|
|
||||||
{:pre [(integer? timestamp)
|
|
||||||
(map? chain-tokens)
|
|
||||||
(every? (fn [[k v]] (and (string? k) (map? v))) chain-tokens)
|
|
||||||
(keyword? direction)
|
|
||||||
(every? map? transfers)]}
|
|
||||||
(into {}
|
|
||||||
(keep identity
|
|
||||||
(for [transfer transfers]
|
|
||||||
(when-let [token (->> transfer :address (get chain-tokens))]
|
|
||||||
(when-not (:nft? token)
|
|
||||||
[(:transactionHash transfer)
|
|
||||||
{:block (str (-> transfer :blockNumber ethereum/hex->bignumber))
|
|
||||||
:hash (:transactionHash transfer)
|
|
||||||
:symbol (:symbol token)
|
|
||||||
:from (some-> transfer :topics second remove-padding)
|
|
||||||
:to (some-> transfer :topics last remove-padding)
|
|
||||||
:value (-> transfer :data ethereum/hex->bignumber)
|
|
||||||
:type direction
|
|
||||||
:gas-price nil
|
|
||||||
:nonce nil
|
|
||||||
:data nil
|
|
||||||
:gas-limit nil
|
|
||||||
:timestamp (str (* timestamp 1000))
|
|
||||||
:gas-used nil
|
|
||||||
;; NOTE(goranjovic) - metadata on the type of token: contains name, symbol, decimas, address.
|
|
||||||
:token token
|
|
||||||
;; NOTE(goranjovic) - if an event has been emitted, we can say there was no error
|
|
||||||
:error? false
|
|
||||||
;; NOTE(goranjovic) - just a flag we need when we merge this entry with the existing entry in
|
|
||||||
;; the app, e.g. transaction info with gas details, or a previous transfer entry with old
|
|
||||||
;; confirmations count.
|
|
||||||
:transfer true}]))))))
|
|
||||||
|
|
||||||
(letfn [(combine-entries [transaction token-transfer]
|
|
||||||
(merge transaction (select-keys token-transfer [:symbol :from :to :value :type :token :transfer])))
|
|
||||||
(tx-and-transfer? [tx1 tx2]
|
|
||||||
(and (not (:transfer tx1)) (:transfer tx2)))
|
|
||||||
(both-transfer?
|
|
||||||
[tx1 tx2]
|
|
||||||
(and (:transfer tx1) (:transfer tx2)))]
|
|
||||||
(defn- dedupe-transactions [tx1 tx2]
|
|
||||||
(cond (tx-and-transfer? tx1 tx2) (combine-entries tx1 tx2)
|
|
||||||
(tx-and-transfer? tx2 tx1) (combine-entries tx2 tx1)
|
|
||||||
:else tx2)))
|
|
||||||
|
|
||||||
(fx/defn new-transactions
|
|
||||||
[{:keys [db]} transactions]
|
|
||||||
{:db (update-in db
|
|
||||||
[:wallet :transactions]
|
|
||||||
#(merge-with dedupe-transactions % transactions))})
|
|
||||||
|
|
||||||
(defn transactions-handler
|
|
||||||
[{:keys [chain-tokens from to direction]}]
|
|
||||||
(fn [transfers]
|
|
||||||
(let [transfers-by-block (group-by :blockHash transfers)]
|
|
||||||
(doseq [[block-hash block-transfers] transfers-by-block]
|
|
||||||
(get-block-by-hash
|
|
||||||
block-hash
|
|
||||||
(fn [{:keys [timestamp]}]
|
|
||||||
(let [transactions (parse-transaction-entries timestamp
|
|
||||||
chain-tokens
|
|
||||||
direction
|
|
||||||
block-transfers)]
|
|
||||||
(when (not-empty transactions)
|
|
||||||
(re-frame/dispatch [:ethereum.signal/new-transactions
|
|
||||||
transactions])))))))))
|
|
||||||
|
|
||||||
;; Here we are querying event logs for Transfer events.
|
|
||||||
;;
|
|
||||||
;; The parameters are as follows:
|
|
||||||
;; - address - token smart contract address
|
|
||||||
;; - fromBlock - we need to specify it, since default is latest
|
|
||||||
;; - topics[0] - hash code of the Transfer event signature
|
|
||||||
;; - topics[1] - address of token sender with leading zeroes padding up to 32 bytes
|
|
||||||
;; - topics[2] - address of token sender with leading zeroes padding up to 32 bytes
|
|
||||||
(defn new-token-transaction-filter
|
(defn new-token-transaction-filter
|
||||||
[{:keys [chain-tokens from to] :as args}]
|
[{:keys [chain-tokens from to] :as args}]
|
||||||
(subscribe-signal
|
(subscribe-signal
|
||||||
"eth_newFilter"
|
"eth_newFilter"
|
||||||
[{:fromBlock "latest"
|
[{:fromBlock "latest"
|
||||||
:toBlock "latest"
|
:toBlock "latest"
|
||||||
:address (keys chain-tokens)
|
|
||||||
:topics [constants/event-transfer-hash from to]}]
|
:topics [constants/event-transfer-hash from to]}]
|
||||||
(transactions-handler args)))
|
(transactions/inbound-token-transfer-handler chain-tokens)))
|
||||||
|
|
||||||
|
(re-frame/reg-fx
|
||||||
|
:ethereum.subscriptions/token-transactions
|
||||||
|
(fn [{:keys [address] :as args}]
|
||||||
|
;; start inbound token transaction subscriptions
|
||||||
|
;; outbound token transactions are already caught in new blocks filter
|
||||||
|
(new-token-transaction-filter (merge args
|
||||||
|
{:direction :inbound
|
||||||
|
:to address}))))
|
||||||
|
|
||||||
(defn new-block-filter
|
(defn new-block-filter
|
||||||
[]
|
[]
|
||||||
(subscribe-signal
|
(subscribe-signal
|
||||||
"eth_newBlockFilter" []
|
"eth_newBlockFilter" []
|
||||||
(fn [[block-hash]]
|
(fn [[block-hash]]
|
||||||
(get-block-by-hash
|
(transactions/get-block-by-hash
|
||||||
block-hash
|
block-hash
|
||||||
(fn [block]
|
(fn [block]
|
||||||
(when-let [block-number (:number block)]
|
(re-frame/dispatch [:ethereum.signal/new-block block]))))))
|
||||||
(re-frame/dispatch [:ethereum.signal/new-block
|
|
||||||
block-number])))))))
|
|
||||||
|
|
||||||
(defn get-from-block
|
|
||||||
[current-block-number]
|
|
||||||
(-> current-block-number
|
|
||||||
(- block-query-limit)
|
|
||||||
(max 0)
|
|
||||||
ethereum/int->hex))
|
|
||||||
|
|
||||||
(re-frame/reg-fx
|
|
||||||
:ethereum.subscriptions/token-transactions
|
|
||||||
(fn [{:keys [address] :as args}]
|
|
||||||
(let [inbound-args (merge args
|
|
||||||
{:direction :inbound
|
|
||||||
:to address})
|
|
||||||
outbound-args (merge args
|
|
||||||
{:direction :outbound
|
|
||||||
:from address})]
|
|
||||||
;; fetch 2 weeks of history until transactions are persisted
|
|
||||||
(get-latest-block
|
|
||||||
(fn [current-block-number]
|
|
||||||
(let [from-block (get-from-block current-block-number)]
|
|
||||||
(get-token-transfer-logs from-block inbound-args
|
|
||||||
(transactions-handler inbound-args))
|
|
||||||
(get-token-transfer-logs from-block outbound-args
|
|
||||||
(transactions-handler outbound-args)))))
|
|
||||||
;; start inbound and outbound token transaction subscriptions
|
|
||||||
(new-token-transaction-filter inbound-args)
|
|
||||||
(new-token-transaction-filter outbound-args))))
|
|
||||||
|
|
||||||
(re-frame/reg-fx
|
(re-frame/reg-fx
|
||||||
:ethereum.subscriptions/new-block
|
:ethereum.subscriptions/new-block
|
||||||
|
@ -242,7 +113,9 @@
|
||||||
chain (ethereum/network->chain-keyword (get-in account [:networks network]))
|
chain (ethereum/network->chain-keyword (get-in account [:networks network]))
|
||||||
chain-tokens (into {} (map (juxt :address identity)
|
chain-tokens (into {} (map (juxt :address identity)
|
||||||
(tokens/tokens-for all-tokens chain)))
|
(tokens/tokens-for all-tokens chain)))
|
||||||
padded-address (add-padding (ethereum/normalized-address (:address account)))]
|
normalized-address (ethereum/normalized-address (:address account))
|
||||||
|
padded-address (transactions/add-padding normalized-address)]
|
||||||
{:ethereum.subscriptions/new-block nil
|
{:ethereum.subscriptions/new-block nil
|
||||||
:ethereum.subscriptions/token-transactions {:chain-tokens chain-tokens
|
:ethereum.subscriptions/token-transactions
|
||||||
:address padded-address}}))
|
{:chain-tokens chain-tokens
|
||||||
|
:address padded-address}}))
|
||||||
|
|
|
@ -1,252 +1,200 @@
|
||||||
(ns status-im.ethereum.transactions.core
|
(ns status-im.ethereum.transactions.core
|
||||||
(:require [clojure.set :as set]
|
(:require [clojure.string :as string]
|
||||||
[clojure.string :as string]
|
|
||||||
[re-frame.core :as re-frame]
|
[re-frame.core :as re-frame]
|
||||||
re-frame.db
|
[status-im.constants :as constants]
|
||||||
[status-im.utils.async :as async-util]
|
[status-im.ethereum.decode :as decode]
|
||||||
|
[status-im.ethereum.transactions.etherscan :as transactions.etherscan]
|
||||||
|
[status-im.native-module.core :as status]
|
||||||
[status-im.utils.ethereum.core :as ethereum]
|
[status-im.utils.ethereum.core :as ethereum]
|
||||||
[status-im.utils.ethereum.tokens :as tokens]
|
|
||||||
[status-im.utils.fx :as fx]
|
[status-im.utils.fx :as fx]
|
||||||
[status-im.utils.http :as http]
|
|
||||||
[status-im.utils.types :as types]
|
[status-im.utils.types :as types]
|
||||||
[taoensso.timbre :as log]))
|
[taoensso.timbre :as log]))
|
||||||
|
|
||||||
(def sync-interval-ms 15000)
|
|
||||||
(def sync-timeout-ms 20000)
|
|
||||||
(def confirmations-count-threshold 12)
|
(def confirmations-count-threshold 12)
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
(defn get-block-by-hash
|
||||||
;; etherscan transactions
|
[block-hash callback]
|
||||||
;; --------------------------------------------------------------------------
|
(status/call-private-rpc
|
||||||
|
(types/clj->json {:jsonrpc "2.0"
|
||||||
|
:id 1
|
||||||
|
:method "eth_getBlockByHash"
|
||||||
|
:params [block-hash true]})
|
||||||
|
(fn [response]
|
||||||
|
(if (string/blank? response)
|
||||||
|
(log/warn :web3-response-error)
|
||||||
|
(callback (-> (.parse js/JSON response)
|
||||||
|
(js->clj :keywordize-keys true)
|
||||||
|
:result
|
||||||
|
(update :number decode/uint)
|
||||||
|
(update :timestamp decode/uint)))))))
|
||||||
|
|
||||||
(def etherscan-supported? #{:testnet :mainnet :rinkeby})
|
(defn get-transaction-by-hash
|
||||||
|
[transaction-hash callback]
|
||||||
|
(status/call-private-rpc
|
||||||
|
(types/clj->json {:jsonrpc "2.0"
|
||||||
|
:id 1
|
||||||
|
:method "eth_getTransactionByHash"
|
||||||
|
:params [transaction-hash]})
|
||||||
|
(fn [response]
|
||||||
|
(if (string/blank? response)
|
||||||
|
(log/warn :web3-response-error)
|
||||||
|
(callback (-> (.parse js/JSON response)
|
||||||
|
(js->clj :keywordize-keys true)
|
||||||
|
:result))))))
|
||||||
|
|
||||||
(let [network->subdomain {:testnet "ropsten" :rinkeby "rinkeby"}]
|
(defn get-transaction-receipt [transaction-hash callback]
|
||||||
(defn get-transaction-details-url [chain hash]
|
(status/call-private-rpc
|
||||||
{:pre [(keyword? chain) (string? hash)]
|
(types/clj->json {:jsonrpc "2.0"
|
||||||
:post [(or (nil? %) (string? %))]}
|
:id 1
|
||||||
(when (etherscan-supported? chain)
|
:method "eth_getTransactionReceipt"
|
||||||
(let [network-subdomain (when-let [subdomain (network->subdomain chain)]
|
:params [transaction-hash]})
|
||||||
(str subdomain "."))]
|
(fn [response]
|
||||||
(str "https://" network-subdomain "etherscan.io/tx/" hash)))))
|
(if (string/blank? response)
|
||||||
|
(log/warn :web3-response-error)
|
||||||
|
(callback (-> (.parse js/JSON response)
|
||||||
|
(js->clj :keywordize-keys true)
|
||||||
|
:result))))))
|
||||||
|
|
||||||
(def etherscan-api-key "DMSI4UAAKUBVGCDMVP3H2STAMSAUV7BYFI")
|
(defn add-padding [address]
|
||||||
|
{:pre [(string? address)]}
|
||||||
|
(str "0x000000000000000000000000" (subs address 2)))
|
||||||
|
|
||||||
(defn- get-api-network-subdomain [chain]
|
(defn- remove-padding [topic]
|
||||||
(case chain
|
{:pre [(string? topic)]}
|
||||||
(:testnet) "api-ropsten"
|
(str "0x" (subs topic 26)))
|
||||||
(:mainnet) "api"
|
|
||||||
(:rinkeby) "api-rinkeby"))
|
|
||||||
|
|
||||||
(defn- get-transaction-url
|
(def default-erc20-token
|
||||||
([chain account] (get-transaction-url chain account false))
|
{:symbol :ERC20
|
||||||
([chain account chaos-mode?]
|
:decimals 18
|
||||||
{:pre [(keyword? chain) (string? account)]
|
:name "ERC20"})
|
||||||
:post [(string? %)]}
|
|
||||||
(let [network-subdomain (get-api-network-subdomain chain)]
|
|
||||||
(if chaos-mode?
|
|
||||||
"http://httpstat.us/500"
|
|
||||||
(str "https://" network-subdomain
|
|
||||||
".etherscan.io/api?module=account&action=txlist&address=0x"
|
|
||||||
account "&startblock=0&endblock=99999999&sort=desc&apikey=" etherscan-api-key "&q=json")))))
|
|
||||||
|
|
||||||
(defn- format-transaction [account
|
(defn- parse-token-transfer
|
||||||
{:keys [value timeStamp blockNumber hash from to
|
[chain-tokens direction transfer]
|
||||||
gas gasPrice gasUsed nonce input isError]}]
|
(let [{:keys [blockHash transactionHash topics data address]} transfer
|
||||||
(let [inbound? (= (str "0x" account) to)
|
[_ from to] topics
|
||||||
error? (= "1" isError)]
|
{:keys [nft? symbol] :as token} (get chain-tokens address
|
||||||
{:value value
|
default-erc20-token)]
|
||||||
;; timestamp is in seconds, we convert it in ms
|
(when-not nft?
|
||||||
:timestamp (str timeStamp "000")
|
(cond-> {:hash transactionHash
|
||||||
:symbol :ETH
|
:symbol symbol
|
||||||
:type (cond error? :failed
|
:from (remove-padding from)
|
||||||
inbound? :inbound
|
:to (remove-padding to)
|
||||||
:else :outbound)
|
:value (ethereum/hex->bignumber data)
|
||||||
:block blockNumber
|
:type direction
|
||||||
:hash hash
|
:token token
|
||||||
:from from
|
:error? false
|
||||||
:to to
|
;; NOTE(goranjovic) - just a flag we need when we merge this entry
|
||||||
:gas-limit gas
|
;; with the existing entry in the app, e.g. transaction info with
|
||||||
:gas-price gasPrice
|
;; gas details, or a previous transfer entry with old confirmations
|
||||||
:gas-used gasUsed
|
;; count.
|
||||||
:nonce nonce
|
:transfer true}
|
||||||
:data input}))
|
(= :inbound direction)
|
||||||
|
(assoc :block-hash blockHash)))))
|
||||||
|
|
||||||
(defn- format-transactions-response [response account]
|
(defn enrich-transaction-from-new-block
|
||||||
(let [{:keys [result]} (types/json->clj response)]
|
[chain-tokens
|
||||||
(cond-> {}
|
{:keys [number timestamp]}
|
||||||
(vector? result)
|
{:keys [transfer direction hash gasPrice value gas from input nonce to] :as transaction}]
|
||||||
(into (comp
|
(get-transaction-receipt
|
||||||
(map (partial format-transaction account))
|
hash
|
||||||
(map (juxt :hash identity)))
|
(fn [{:keys [gasUsed logs] :as receipt}]
|
||||||
result))))
|
(let [[event _ _] (:topics (first logs))
|
||||||
|
transfer (= constants/event-transfer-hash event)]
|
||||||
(defn- etherscan-transactions
|
(re-frame/dispatch
|
||||||
([chain account on-success on-error]
|
[:ethereum.transactions/new
|
||||||
(etherscan-transactions chain account on-success on-error false))
|
(merge {:block (str number)
|
||||||
([chain account on-success on-error chaos-mode?]
|
:timestamp (str (* timestamp 1000))
|
||||||
(if (etherscan-supported? chain)
|
:gas-used (str (decode/uint gasUsed))
|
||||||
(let [url (get-transaction-url chain account chaos-mode?)]
|
:gas-price (str (decode/uint gasPrice))
|
||||||
(log/debug "HTTP GET" url)
|
:gas-limit (str (decode/uint gas))
|
||||||
(http/get url
|
:nonce (str (decode/uint nonce))
|
||||||
#(on-success (format-transactions-response % account))
|
:data input}
|
||||||
on-error))
|
(if transfer
|
||||||
(log/info "Etherscan not supported for " chain))))
|
(parse-token-transfer chain-tokens
|
||||||
|
:outbound
|
||||||
(defn- get-transactions [{:keys [web3 chain chain-tokens account-address
|
(first logs))
|
||||||
success-fn error-fn chaos-mode?]}]
|
;; this is not a ERC20 token transaction
|
||||||
(log/debug "Syncing transactions data..")
|
{:hash hash
|
||||||
(etherscan-transactions chain
|
:symbol :ETH
|
||||||
account-address
|
:from from
|
||||||
success-fn
|
:to to
|
||||||
error-fn
|
:type direction
|
||||||
chaos-mode?))
|
:value (str (decode/uint value))}))])))))
|
||||||
|
|
||||||
;; -----------------------------------------------------------------------------
|
|
||||||
;; Helpers functions that help determine if a background sync should execute
|
|
||||||
;; -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
(defn- keyed-memoize
|
|
||||||
"Space bounded memoize.
|
|
||||||
|
|
||||||
Takes a key-function that decides the key in the cache for the
|
|
||||||
memoized value. Takes a value function that will extract the value
|
|
||||||
that will invalidate the cache if it changes. And finally the
|
|
||||||
function to memoize.
|
|
||||||
|
|
||||||
Memoize that doesn't grow bigger than the number of keys."
|
|
||||||
[key-fn val-fn f]
|
|
||||||
(let [val-store (atom {})
|
|
||||||
res-store (atom {})]
|
|
||||||
(fn [arg]
|
|
||||||
(let [k (key-fn arg)
|
|
||||||
v (val-fn arg)]
|
|
||||||
(if (not= (get @val-store k) v)
|
|
||||||
(let [res (f arg)]
|
|
||||||
#_(prn "storing!!!!" res)
|
|
||||||
(swap! val-store assoc k v)
|
|
||||||
(swap! res-store assoc k res)
|
|
||||||
res)
|
|
||||||
(get @res-store k))))))
|
|
||||||
|
|
||||||
;; Map[id, chat] -> Set[transaction-id]
|
|
||||||
;; chat may or may not have a :messages Map
|
|
||||||
(let [chat-map-entry->transaction-ids
|
|
||||||
(keyed-memoize key (comp :messages val)
|
|
||||||
(fn [[_ chat]]
|
|
||||||
(some->> (:messages chat)
|
|
||||||
vals
|
|
||||||
(filter #(= "command" (:content-type %)))
|
|
||||||
(keep #(select-keys (get-in % [:content :params]) [:tx-hash :network])))))]
|
|
||||||
(defn- chat-map->transaction-ids [network chat-map]
|
|
||||||
{:pre [(string? network) (every? map? (vals chat-map))]
|
|
||||||
:post [(set? %)]}
|
|
||||||
(let [network (string/replace network "_rpc" "")]
|
|
||||||
(->> chat-map
|
|
||||||
(remove (comp :public? val))
|
|
||||||
(mapcat chat-map-entry->transaction-ids)
|
|
||||||
(filter #(= network (:network %)))
|
|
||||||
(map :tx-hash)
|
|
||||||
set))))
|
|
||||||
|
|
||||||
(letfn [(combine-entries [transaction token-transfer]
|
|
||||||
(merge transaction (select-keys token-transfer [:symbol :from :to :value :type :token :transfer])))
|
|
||||||
(tx-and-transfer? [tx1 tx2]
|
|
||||||
(and (not (:transfer tx1)) (:transfer tx2)))
|
|
||||||
(both-transfer?
|
|
||||||
[tx1 tx2]
|
|
||||||
(and (:transfer tx1) (:transfer tx2)))]
|
|
||||||
(defn- dedupe-transactions [tx1 tx2]
|
|
||||||
(cond (tx-and-transfer? tx1 tx2) (combine-entries tx1 tx2)
|
|
||||||
(tx-and-transfer? tx2 tx1) (combine-entries tx2 tx1)
|
|
||||||
:else tx2)))
|
|
||||||
|
|
||||||
;; ----------------------------------------------------------------------------
|
|
||||||
;; The following Code represents how fetching transactions is
|
|
||||||
;; complected with the rest of the application
|
|
||||||
;; ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
(defonce polling-executor (atom nil))
|
|
||||||
|
|
||||||
(defn transactions-query-helper [web3 all-tokens account-address chain done-fn chaos-mode?]
|
|
||||||
(get-transactions
|
|
||||||
{:account-address account-address
|
|
||||||
:chain chain
|
|
||||||
:chain-tokens (into {} (map (juxt :address identity) (tokens/tokens-for all-tokens chain)))
|
|
||||||
:web3 web3
|
|
||||||
:success-fn (fn [transactions]
|
|
||||||
#_(log/debug "Transactions received: " (pr-str (keys transactions)))
|
|
||||||
(swap! re-frame.db/app-db
|
|
||||||
(fn [app-db]
|
|
||||||
(when (= (get-in app-db [:account/account :address])
|
|
||||||
account-address)
|
|
||||||
(update-in app-db
|
|
||||||
[:wallet :transactions]
|
|
||||||
#(merge-with dedupe-transactions % transactions)))))
|
|
||||||
(done-fn))
|
|
||||||
:error-fn (fn [http-error]
|
|
||||||
(log/debug "Unable to get transactions: " http-error)
|
|
||||||
(done-fn))
|
|
||||||
:chaos-mode? chaos-mode?}))
|
|
||||||
|
|
||||||
(defn- sync-now! [{:keys [network-status :account/account :wallet/all-tokens app-state network web3] :as opts}]
|
|
||||||
(when @polling-executor
|
|
||||||
(let [chain (ethereum/network->chain-keyword (get-in account [:networks network]))
|
|
||||||
account-address (:address account)
|
|
||||||
chaos-mode? (get-in account [:settings :chaos-mode?])]
|
|
||||||
(when (and (not= network-status :offline)
|
|
||||||
(= app-state "active")
|
|
||||||
(not= :custom chain))
|
|
||||||
(async-util/async-periodic-run!
|
|
||||||
@polling-executor
|
|
||||||
#(transactions-query-helper web3 all-tokens account-address chain % chaos-mode?))))))
|
|
||||||
|
|
||||||
;; this function handles background syncing of transactions
|
|
||||||
(defn- background-sync [web3 account-address done-fn]
|
|
||||||
(let [{:keys [network network-status :account/account app-state wallet chats :wallet/all-tokens]} @re-frame.db/app-db
|
|
||||||
chain (ethereum/network->chain-keyword (get-in account [:networks network]))]
|
|
||||||
(assert (and web3 account-address network network-status account app-state wallet chats)
|
|
||||||
"Must have all necessary data to run background transaction sync")
|
|
||||||
(if-not (and (not= network-status :offline)
|
|
||||||
(= app-state "active")
|
|
||||||
(not= :custom chain))
|
|
||||||
(done-fn)
|
|
||||||
(let [chat-transaction-ids (chat-map->transaction-ids network chats)
|
|
||||||
transaction-map (:transactions wallet)
|
|
||||||
transaction-ids (set (keys transaction-map))
|
|
||||||
chaos-mode? (get-in account [:settings :chaos-mode?])]
|
|
||||||
(if-not (not-empty (set/difference chat-transaction-ids transaction-ids))
|
|
||||||
(done-fn)
|
|
||||||
(transactions-query-helper web3 all-tokens account-address chain done-fn chaos-mode?))))))
|
|
||||||
|
|
||||||
(defn- start-sync! [{:keys [:account/account network web3] :as options}]
|
|
||||||
(let [account-address (:address account)]
|
|
||||||
(when @polling-executor
|
|
||||||
(async-util/async-periodic-stop! @polling-executor))
|
|
||||||
(reset! polling-executor
|
|
||||||
(async-util/async-periodic-exec
|
|
||||||
(partial #'background-sync web3 account-address)
|
|
||||||
sync-interval-ms
|
|
||||||
sync-timeout-ms)))
|
|
||||||
(sync-now! options))
|
|
||||||
|
|
||||||
(re-frame/reg-fx
|
(re-frame/reg-fx
|
||||||
::sync-transactions-now
|
:ethereum.transactions/enrich-transactions-from-new-blocks
|
||||||
(fn [db] (sync-now! db)))
|
(fn [{:keys [chain-tokens block transactions]}]
|
||||||
|
(doseq [transaction transactions]
|
||||||
|
(enrich-transaction-from-new-block chain-tokens
|
||||||
|
block
|
||||||
|
transaction))))
|
||||||
|
|
||||||
(re-frame/reg-fx
|
(defn inbound-token-transfer-handler
|
||||||
::start-sync-transactions
|
"The handler gets a list of inbound token transfer events and parses each
|
||||||
(fn [db] (start-sync! db)))
|
transfer. Transfers are grouped by block the following chain of callbacks
|
||||||
|
follows:
|
||||||
|
- get block by hash is called to get the `timestamp` of each block
|
||||||
|
- get transaction by hash is called on each transaction to get the `gasPrice`
|
||||||
|
`gas` used, `input` data and `nonce` of each transaction
|
||||||
|
- get transaction receipt is used to get the `gasUsed`
|
||||||
|
- finally everything is merged into one map that is dispatched in a
|
||||||
|
`ethereum.signal/new-transaction` event for each transfer"
|
||||||
|
[chain-tokens]
|
||||||
|
(fn [transfers]
|
||||||
|
(let [transfers-by-block
|
||||||
|
(group-by :block-hash
|
||||||
|
(keep #(parse-token-transfer
|
||||||
|
chain-tokens
|
||||||
|
:inbound
|
||||||
|
%)
|
||||||
|
transfers))]
|
||||||
|
;; TODO: remove this callback chain by implementing a better status-go api
|
||||||
|
;; This function takes the map of supported tokens as params and returns a
|
||||||
|
;; handler for token transfer events
|
||||||
|
(doseq [[block-hash block-transfers] transfers-by-block]
|
||||||
|
(get-block-by-hash
|
||||||
|
block-hash
|
||||||
|
(fn [{:keys [timestamp number]}]
|
||||||
|
(let [timestamp (str (* timestamp 1000))]
|
||||||
|
(doseq [{:keys [hash] :as transfer} block-transfers]
|
||||||
|
(get-transaction-by-hash
|
||||||
|
hash
|
||||||
|
(fn [{:keys [gasPrice gas input nonce]}]
|
||||||
|
(get-transaction-receipt
|
||||||
|
hash
|
||||||
|
(fn [{:keys [gasUsed]}]
|
||||||
|
(re-frame/dispatch
|
||||||
|
[:ethereum.transactions/new
|
||||||
|
(-> transfer
|
||||||
|
(dissoc :block-hash)
|
||||||
|
(assoc :timestamp timestamp
|
||||||
|
:block (str number)
|
||||||
|
:gas-used (str (decode/uint gasUsed))
|
||||||
|
:gas-price (str (decode/uint gasPrice))
|
||||||
|
:gas-limit (str (decode/uint gas))
|
||||||
|
:data input
|
||||||
|
:nonce (str (decode/uint nonce))))])))))))))))))
|
||||||
|
|
||||||
(fx/defn start-sync [{:keys [db]}]
|
;; -----------------------------------------------
|
||||||
{::start-sync-transactions
|
;; transactions api
|
||||||
(select-keys db [:network-status :account/account :wallet/all-tokens
|
;; -----------------------------------------------
|
||||||
:app-state :network :web3])})
|
|
||||||
|
|
||||||
(re-frame/reg-fx
|
(fx/defn new
|
||||||
::stop-sync-transactions
|
[{:keys [db]} {:keys [hash] :as transaction}]
|
||||||
#(when @polling-executor
|
{:db (assoc-in db [:wallet :transactions hash] transaction)})
|
||||||
(async-util/async-periodic-stop! @polling-executor)))
|
|
||||||
|
|
||||||
(fx/defn stop-sync [_]
|
(fx/defn handle-history
|
||||||
{::stop-sync-transactions nil})
|
[{:keys [db]} transactions]
|
||||||
|
{:db (update-in db
|
||||||
|
[:wallet :transactions]
|
||||||
|
#(merge transactions %))})
|
||||||
|
|
||||||
|
(fx/defn handle-token-history
|
||||||
|
[{:keys [db]} transactions]
|
||||||
|
{:db (update-in db
|
||||||
|
[:wallet :transactions]
|
||||||
|
merge transactions)})
|
||||||
|
|
||||||
|
(fx/defn initialize
|
||||||
|
[cofx]
|
||||||
|
(transactions.etherscan/fetch-history cofx))
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
(ns status-im.ethereum.transactions.etherscan
|
||||||
|
(:require [re-frame.core :as re-frame]
|
||||||
|
[status-im.utils.ethereum.core :as ethereum]
|
||||||
|
[status-im.utils.ethereum.tokens :as tokens]
|
||||||
|
[status-im.utils.fx :as fx]
|
||||||
|
[status-im.utils.http :as http]
|
||||||
|
[status-im.utils.types :as types]
|
||||||
|
[taoensso.timbre :as log]))
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; etherscan transactions
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(def etherscan-supported? #{:testnet :mainnet :rinkeby})
|
||||||
|
|
||||||
|
(let [network->subdomain {:testnet "ropsten" :rinkeby "rinkeby"}]
|
||||||
|
(defn get-transaction-details-url [chain hash]
|
||||||
|
{:pre [(keyword? chain) (string? hash)]
|
||||||
|
:post [(or (nil? %) (string? %))]}
|
||||||
|
(when (etherscan-supported? chain)
|
||||||
|
(let [network-subdomain (when-let [subdomain (network->subdomain chain)]
|
||||||
|
(str subdomain "."))]
|
||||||
|
(str "https://" network-subdomain "etherscan.io/tx/" hash)))))
|
||||||
|
|
||||||
|
(def etherscan-api-key "DMSI4UAAKUBVGCDMVP3H2STAMSAUV7BYFI")
|
||||||
|
|
||||||
|
(defn- get-api-network-subdomain [chain]
|
||||||
|
(case chain
|
||||||
|
(:testnet) "api-ropsten"
|
||||||
|
(:mainnet) "api"
|
||||||
|
(:rinkeby) "api-rinkeby"))
|
||||||
|
|
||||||
|
(defn- get-transaction-url
|
||||||
|
([chain address] (get-transaction-url chain address false))
|
||||||
|
([chain address chaos-mode?]
|
||||||
|
{:pre [(keyword? chain) (string? address)]
|
||||||
|
:post [(string? %)]}
|
||||||
|
(let [network-subdomain (get-api-network-subdomain chain)]
|
||||||
|
(if chaos-mode?
|
||||||
|
"http://httpstat.us/500"
|
||||||
|
(str "https://" network-subdomain
|
||||||
|
".etherscan.io/api?module=account&action=txlist&address=" address
|
||||||
|
"&startblock=0&endblock=99999999&sort=desc&apikey=" etherscan-api-key
|
||||||
|
"&q=json")))))
|
||||||
|
|
||||||
|
(defn- get-token-transaction-url
|
||||||
|
([chain address] (get-token-transaction-url chain address false))
|
||||||
|
([chain address chaos-mode?]
|
||||||
|
{:pre [(keyword? chain) (string? address)]
|
||||||
|
:post [(string? %)]}
|
||||||
|
(let [network-subdomain (get-api-network-subdomain chain)]
|
||||||
|
(if chaos-mode?
|
||||||
|
"http://httpstat.us/500"
|
||||||
|
(str "https://" network-subdomain
|
||||||
|
".etherscan.io/api?module=account&action=tokentx&address=" address
|
||||||
|
"&startblock=0&endblock=999999999&sort=asc&apikey=" etherscan-api-key
|
||||||
|
"&q=json")))))
|
||||||
|
|
||||||
|
(defn- format-transaction
|
||||||
|
[address
|
||||||
|
{:keys [value timeStamp blockNumber hash from to
|
||||||
|
gas gasPrice gasUsed nonce input isError]}]
|
||||||
|
(let [inbound? (= address to)
|
||||||
|
error? (= "1" isError)]
|
||||||
|
{:value value
|
||||||
|
;; timestamp is in seconds, we convert it in ms
|
||||||
|
:timestamp (str timeStamp "000")
|
||||||
|
:symbol :ETH
|
||||||
|
:type (cond error? :failed
|
||||||
|
inbound? :inbound
|
||||||
|
:else :outbound)
|
||||||
|
:block blockNumber
|
||||||
|
:hash hash
|
||||||
|
:from from
|
||||||
|
:to to
|
||||||
|
:gas-limit gas
|
||||||
|
:gas-price gasPrice
|
||||||
|
:gas-used gasUsed
|
||||||
|
:nonce nonce
|
||||||
|
:data input}))
|
||||||
|
|
||||||
|
(defn- format-token-transaction
|
||||||
|
[address
|
||||||
|
chain-tokens
|
||||||
|
{:keys [contractAddress blockHash hash tokenDecimal gasPrice value
|
||||||
|
gas tokenName timeStamp transactionIndex tokenSymbol
|
||||||
|
confirmations blockNumber from gasUsed input nonce
|
||||||
|
cumulativeGasUsed to]}]
|
||||||
|
(let [inbound? (= address to)
|
||||||
|
token (get chain-tokens contractAddress
|
||||||
|
{:name tokenName
|
||||||
|
:symbol tokenSymbol
|
||||||
|
:decimals tokenDecimal
|
||||||
|
:address contractAddress})]
|
||||||
|
{:value value
|
||||||
|
;; timestamp is in seconds, we convert it in ms
|
||||||
|
:timestamp (str timeStamp "000")
|
||||||
|
:symbol (keyword tokenSymbol)
|
||||||
|
:type (if inbound?
|
||||||
|
:inbound
|
||||||
|
:outbound)
|
||||||
|
:block blockNumber
|
||||||
|
:hash hash
|
||||||
|
:from from
|
||||||
|
:to to
|
||||||
|
:gas-limit gas
|
||||||
|
:gas-price gasPrice
|
||||||
|
:gas-used gasUsed
|
||||||
|
:nonce nonce
|
||||||
|
:data input
|
||||||
|
:error? false
|
||||||
|
:transfer true
|
||||||
|
:token token}))
|
||||||
|
|
||||||
|
(defn- format-transactions-response [response format-fn]
|
||||||
|
(let [{:keys [result]} (types/json->clj response)]
|
||||||
|
(cond-> {}
|
||||||
|
(vector? result)
|
||||||
|
(into (comp
|
||||||
|
(map format-fn)
|
||||||
|
(map (juxt :hash identity)))
|
||||||
|
result))))
|
||||||
|
|
||||||
|
(defn- etherscan-history
|
||||||
|
[chain address on-success on-error chaos-mode?]
|
||||||
|
(if (etherscan-supported? chain)
|
||||||
|
(let [url (get-transaction-url chain address chaos-mode?)]
|
||||||
|
(log/debug :etherscan-transactions :url url)
|
||||||
|
(http/get url
|
||||||
|
#(on-success (format-transactions-response
|
||||||
|
%
|
||||||
|
(partial format-transaction address)))
|
||||||
|
on-error))
|
||||||
|
(log/info "Etherscan not supported for " chain)))
|
||||||
|
|
||||||
|
(defn- etherscan-token-history
|
||||||
|
[chain address chain-tokens on-success on-error chaos-mode?]
|
||||||
|
(if (etherscan-supported? chain)
|
||||||
|
(let [token-url (get-token-transaction-url chain address chaos-mode?)]
|
||||||
|
(log/debug :etherscan-token-transactions :token-url token-url)
|
||||||
|
(http/get token-url
|
||||||
|
#(on-success (format-transactions-response
|
||||||
|
%
|
||||||
|
(partial format-token-transaction address chain-tokens)))
|
||||||
|
on-error))
|
||||||
|
(log/info "Etherscan not supported for " chain)))
|
||||||
|
|
||||||
|
(re-frame/reg-fx
|
||||||
|
:ethereum.transactions.etherscan/fetch-history
|
||||||
|
(fn [{:keys [chain address on-success on-error chaos-mode?]}]
|
||||||
|
(etherscan-history chain address on-success on-error chaos-mode?)))
|
||||||
|
|
||||||
|
(re-frame/reg-fx
|
||||||
|
:ethereum.transactions.etherscan/fetch-token-history
|
||||||
|
(fn [{:keys [chain chain-tokens address on-success on-error chaos-mode?]}]
|
||||||
|
(etherscan-token-history chain address chain-tokens on-success on-error chaos-mode?)))
|
||||||
|
|
||||||
|
;; -----------------------------------------------
|
||||||
|
;; chain transactions
|
||||||
|
;; -----------------------------------------------
|
||||||
|
|
||||||
|
(fx/defn fetch-history
|
||||||
|
[{:keys [db] :as cofx}]
|
||||||
|
(let [{:keys [:account/account :wallet/all-tokens network]} db
|
||||||
|
chain (ethereum/network->chain-keyword
|
||||||
|
(get-in account [:networks network]))
|
||||||
|
chain-tokens (into {} (map (juxt :address identity)
|
||||||
|
(tokens/tokens-for all-tokens chain)))
|
||||||
|
chaos-mode? (get-in account [:settings :chaos-mode?])
|
||||||
|
normalized-address (ethereum/normalized-address (:address account))]
|
||||||
|
#:ethereum.transactions.etherscan
|
||||||
|
{:fetch-history
|
||||||
|
{:chain chain
|
||||||
|
:address normalized-address
|
||||||
|
:on-success
|
||||||
|
#(re-frame/dispatch
|
||||||
|
[:ethereum.transactions.callback/fetch-history-success %])
|
||||||
|
:on-error
|
||||||
|
#(re-frame/dispatch
|
||||||
|
[:ethereum.transactions.callback/etherscan-error %])
|
||||||
|
:chaos-mode? chaos-mode?}
|
||||||
|
:fetch-token-history
|
||||||
|
{:chain chain
|
||||||
|
:chain-tokens chain-tokens
|
||||||
|
:address normalized-address
|
||||||
|
:on-success
|
||||||
|
#(re-frame/dispatch
|
||||||
|
[:ethereum.transactions.callback/fetch-token-history-success %])
|
||||||
|
:on-error
|
||||||
|
#(re-frame/dispatch
|
||||||
|
[:ethereum.transactions.callback/etherscan-error %])
|
||||||
|
:chaos-mode? chaos-mode?}}))
|
|
@ -21,6 +21,7 @@
|
||||||
[status-im.contact.block :as contact.block]
|
[status-im.contact.block :as contact.block]
|
||||||
[status-im.contact.core :as contact]
|
[status-im.contact.core :as contact]
|
||||||
[status-im.ethereum.subscriptions :as ethereum.subscriptions]
|
[status-im.ethereum.subscriptions :as ethereum.subscriptions]
|
||||||
|
[status-im.ethereum.transactions.core :as ethereum.transactions]
|
||||||
[status-im.extensions.core :as extensions]
|
[status-im.extensions.core :as extensions]
|
||||||
[status-im.extensions.registry :as extensions.registry]
|
[status-im.extensions.registry :as extensions.registry]
|
||||||
[status-im.fleet.core :as fleet]
|
[status-im.fleet.core :as fleet]
|
||||||
|
@ -2120,10 +2121,26 @@
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
:ethereum.signal/new-block
|
:ethereum.signal/new-block
|
||||||
(fn [cofx [_ block-number]]
|
(fn [cofx [_ block]]
|
||||||
(ethereum.subscriptions/new-block cofx block-number)))
|
(ethereum.subscriptions/new-block cofx block)))
|
||||||
|
|
||||||
|
;; ethereum transactions events
|
||||||
|
(handlers/register-handler-fx
|
||||||
|
:ethereum.transactions.callback/fetch-history-success
|
||||||
|
(fn [cofx [_ transactions]]
|
||||||
|
(ethereum.transactions/handle-history cofx transactions)))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
:ethereum.signal/new-transactions
|
:ethereum.transactions.callback/etherscan-error
|
||||||
|
(fn [cofx [event error]]
|
||||||
|
(log/info event error)))
|
||||||
|
|
||||||
|
(handlers/register-handler-fx
|
||||||
|
:ethereum.transactions.callback/fetch-token-history-success
|
||||||
(fn [cofx [_ transactions]]
|
(fn [cofx [_ transactions]]
|
||||||
(ethereum.subscriptions/new-transactions cofx transactions)))
|
(ethereum.transactions/handle-token-history cofx transactions)))
|
||||||
|
|
||||||
|
(handlers/register-handler-fx
|
||||||
|
:ethereum.transactions/new
|
||||||
|
(fn [cofx [_ transaction]]
|
||||||
|
(ethereum.transactions/new cofx transaction)))
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
[status-im.ethereum.transactions.core :as transactions]
|
[status-im.ethereum.transactions.core :as transactions]
|
||||||
[status-im.fleet.core :as fleet]
|
[status-im.fleet.core :as fleet]
|
||||||
[status-im.i18n :as i18n]
|
[status-im.i18n :as i18n]
|
||||||
|
[status-im.ethereum.transactions.core :as transactions]
|
||||||
|
[status-im.ethereum.transactions.etherscan :as transactions.etherscan]
|
||||||
[status-im.models.wallet :as models.wallet]
|
[status-im.models.wallet :as models.wallet]
|
||||||
[status-im.ui.components.bottom-bar.styles :as tabs.styles]
|
[status-im.ui.components.bottom-bar.styles :as tabs.styles]
|
||||||
[status-im.ui.components.toolbar.styles :as toolbar.styles]
|
[status-im.ui.components.toolbar.styles :as toolbar.styles]
|
||||||
|
@ -1133,7 +1135,7 @@
|
||||||
:hash (i18n/label :not-applicable)}
|
:hash (i18n/label :not-applicable)}
|
||||||
{:cost (when gas-used
|
{:cost (when gas-used
|
||||||
(money/wei->str :eth (money/fee-value gas-used gas-price) display-unit))
|
(money/wei->str :eth (money/fee-value gas-used gas-price) display-unit))
|
||||||
:url (transactions/get-transaction-details-url chain hash)}))))))
|
:url (transactions.etherscan/get-transaction-details-url chain hash)}))))))
|
||||||
|
|
||||||
(re-frame/reg-sub
|
(re-frame/reg-sub
|
||||||
:wallet.transactions.details/confirmations
|
:wallet.transactions.details/confirmations
|
||||||
|
@ -1141,7 +1143,7 @@
|
||||||
:<- [:wallet.transactions/transaction-details]
|
:<- [:wallet.transactions/transaction-details]
|
||||||
(fn [[current-block {:keys [block]}]]
|
(fn [[current-block {:keys [block]}]]
|
||||||
(if (and current-block block)
|
(if (and current-block block)
|
||||||
(- current-block block)
|
(inc (- current-block block))
|
||||||
0)))
|
0)))
|
||||||
|
|
||||||
(re-frame/reg-sub
|
(re-frame/reg-sub
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
(ns status-im.ui.screens.wallet.events
|
(ns status-im.ui.screens.wallet.events
|
||||||
(:require [re-frame.core :as re-frame]
|
(:require [re-frame.core :as re-frame]
|
||||||
[status-im.ethereum.transactions.core :as transactions]
|
|
||||||
[status-im.i18n :as i18n]
|
[status-im.i18n :as i18n]
|
||||||
[status-im.models.wallet :as models]
|
[status-im.models.wallet :as models]
|
||||||
[status-im.ui.screens.navigation :as navigation]
|
[status-im.ui.screens.navigation :as navigation]
|
||||||
|
@ -143,13 +142,6 @@
|
||||||
(navigation/navigate-back)
|
(navigation/navigate-back)
|
||||||
(models/update-wallet))))
|
(models/update-wallet))))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
|
||||||
:update-transactions
|
|
||||||
(fn [{:keys [db]} _]
|
|
||||||
{::transactions/sync-transactions-now
|
|
||||||
(select-keys db [:network-status :account/account :wallet/all-tokens
|
|
||||||
:app-state :network :web3])}))
|
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
:update-balance-success
|
:update-balance-success
|
||||||
(fn [{:keys [db]} [_ balance]]
|
(fn [{:keys [db]} [_ balance]]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
(ns status-im.ui.screens.wallet.navigation
|
(ns status-im.ui.screens.wallet.navigation
|
||||||
(:require [re-frame.core :as re-frame]
|
(:require [re-frame.core :as re-frame]
|
||||||
|
[status-im.constants :as constants]
|
||||||
[status-im.ui.screens.navigation :as navigation]
|
[status-im.ui.screens.navigation :as navigation]
|
||||||
[status-im.utils.ethereum.core :as ethereum]
|
[status-im.utils.ethereum.core :as ethereum]
|
||||||
[status-im.constants :as constants]
|
[status-im.constants :as constants]
|
||||||
|
@ -25,11 +26,6 @@
|
||||||
500)
|
500)
|
||||||
(assoc-in db [:wallet :current-tab] 0))
|
(assoc-in db [:wallet :current-tab] 0))
|
||||||
|
|
||||||
(defmethod navigation/preload-data! :transactions-history
|
|
||||||
[db _]
|
|
||||||
(re-frame/dispatch [:update-transactions])
|
|
||||||
db)
|
|
||||||
|
|
||||||
(def transaction-send-default
|
(def transaction-send-default
|
||||||
(let [symbol :ETH]
|
(let [symbol :ETH]
|
||||||
{:gas (ethereum/estimate-gas symbol)
|
{:gas (ethereum/estimate-gas symbol)
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
(ns status-im.ui.screens.wallet.transactions.views
|
(ns status-im.ui.screens.wallet.transactions.views
|
||||||
(:require-macros [status-im.utils.views :refer [defview letsubs]])
|
|
||||||
(:require [re-frame.core :as re-frame]
|
(:require [re-frame.core :as re-frame]
|
||||||
[status-im.i18n :as i18n]
|
[status-im.i18n :as i18n]
|
||||||
|
[status-im.ui.components.colors :as colors]
|
||||||
[status-im.ui.components.list.views :as list]
|
[status-im.ui.components.list.views :as list]
|
||||||
[status-im.ui.components.react :as react]
|
[status-im.ui.components.react :as react]
|
||||||
[status-im.ui.components.status-bar.view :as status-bar]
|
[status-im.ui.components.status-bar.view :as status-bar]
|
||||||
[status-im.ui.components.styles :as components.styles]
|
[status-im.ui.components.styles :as components.styles]
|
||||||
[status-im.ui.components.colors :as colors]
|
|
||||||
[status-im.ui.components.toolbar.actions :as actions]
|
[status-im.ui.components.toolbar.actions :as actions]
|
||||||
[status-im.ui.components.toolbar.view :as toolbar]
|
[status-im.ui.components.toolbar.view :as toolbar]
|
||||||
[status-im.ui.components.status-bar.view :as status-bar]
|
|
||||||
[status-im.ui.screens.wallet.transactions.styles :as styles]
|
[status-im.ui.screens.wallet.transactions.styles :as styles]
|
||||||
[status-im.utils.money :as money]
|
|
||||||
[status-im.utils.ethereum.tokens :as tokens]
|
|
||||||
[status-im.utils.ethereum.core :as ethereum]
|
|
||||||
[status-im.ui.screens.wallet.utils :as wallet.utils]
|
[status-im.ui.screens.wallet.utils :as wallet.utils]
|
||||||
[status-im.utils.utils :as utils]))
|
[status-im.utils.ethereum.core :as ethereum]
|
||||||
|
[status-im.utils.ethereum.tokens :as tokens]
|
||||||
|
[status-im.utils.money :as money])
|
||||||
|
(:require-macros [status-im.utils.views :refer [defview letsubs]]))
|
||||||
|
|
||||||
(defn history-action [filter?]
|
(defn history-action [filter?]
|
||||||
(cond->
|
(cond->
|
||||||
|
@ -111,7 +109,6 @@
|
||||||
:render-fn #(render-transaction % network all-tokens hide-details?)
|
:render-fn #(render-transaction % network all-tokens hide-details?)
|
||||||
:empty-component [react/i18n-text {:style styles/empty-text
|
:empty-component [react/i18n-text {:style styles/empty-text
|
||||||
:key :transactions-history-empty}]
|
:key :transactions-history-empty}]
|
||||||
:on-refresh #(re-frame/dispatch [:update-transactions])
|
|
||||||
:refreshing false}]]))
|
:refreshing false}]]))
|
||||||
|
|
||||||
;; Filter history
|
;; Filter history
|
||||||
|
@ -263,4 +260,3 @@
|
||||||
[details-confirmations confirmations confirmations-progress type]
|
[details-confirmations confirmations confirmations-progress type]
|
||||||
[react/view {:style styles/details-separator}]
|
[react/view {:style styles/details-separator}]
|
||||||
[details-list transaction]]]))
|
[details-list transaction]]]))
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
:body body}))))))
|
:body body}))))))
|
||||||
(.catch (or on-error
|
(.catch (or on-error
|
||||||
(fn [error]
|
(fn [error]
|
||||||
(utils/show-popup "Error" (str error))))))))
|
(utils/show-popup "Error" url (str error))))))))
|
||||||
|
|
||||||
(defn post
|
(defn post
|
||||||
"Performs an HTTP POST request"
|
"Performs an HTTP POST request"
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
(.catch (fn [error]
|
(.catch (fn [error]
|
||||||
(if on-error
|
(if on-error
|
||||||
(on-error {:response-body error})
|
(on-error {:response-body error})
|
||||||
(utils/show-popup "Error" (str error))))))))
|
(utils/show-popup "Error" url (str error))))))))
|
||||||
|
|
||||||
(defn raw-get
|
(defn raw-get
|
||||||
"Performs an HTTP GET request and returns raw results :status :headers :body."
|
"Performs an HTTP GET request and returns raw results :status :headers :body."
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
:body body}))))))
|
:body body}))))))
|
||||||
(.catch (or on-error
|
(.catch (or on-error
|
||||||
(fn [error]
|
(fn [error]
|
||||||
(utils/show-popup "Error" (str error))))))))
|
(utils/show-popup "Error" url (str error))))))))
|
||||||
|
|
||||||
(defn get
|
(defn get
|
||||||
"Performs an HTTP GET request"
|
"Performs an HTTP GET request"
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
:else false)))
|
:else false)))
|
||||||
(.catch (or on-error
|
(.catch (or on-error
|
||||||
(fn [error]
|
(fn [error]
|
||||||
(utils/show-popup "Error" (str error))))))))
|
(utils/show-popup "Error" url (str error))))))))
|
||||||
|
|
||||||
(defn normalize-url [url]
|
(defn normalize-url [url]
|
||||||
(str (when (and (string? url) (not (re-find #"^[a-zA-Z-_]+:/" url))) "http://") url))
|
(str (when (and (string? url) (not (re-find #"^[a-zA-Z-_]+:/" url))) "http://") url))
|
||||||
|
|
|
@ -204,8 +204,7 @@
|
||||||
(is (contains? efx :get-balance))
|
(is (contains? efx :get-balance))
|
||||||
(is (contains? efx :web3/get-syncing))
|
(is (contains? efx :web3/get-syncing))
|
||||||
(is (contains? efx :get-tokens-balance))
|
(is (contains? efx :get-tokens-balance))
|
||||||
(is (contains? efx :get-prices))
|
(is (contains? efx :get-prices))))))
|
||||||
(is (contains? efx :status-im.ethereum.transactions.core/start-sync-transactions))))))
|
|
||||||
|
|
||||||
(deftest login-failed
|
(deftest login-failed
|
||||||
(testing
|
(testing
|
||||||
|
|
|
@ -1,154 +1,9 @@
|
||||||
(ns status-im.test.wallet.transactions
|
(ns status-im.test.wallet.transactions
|
||||||
(:require [cljs.test :refer-macros [deftest is]]
|
(:require [cljs.test :refer-macros [deftest is]]
|
||||||
[goog.Uri :as goog-uri]
|
[goog.Uri :as goog-uri]
|
||||||
[status-im.ethereum.transactions.core :as transactions]
|
[status-im.ethereum.transactions.etherscan :as transactions]
|
||||||
[status-im.utils.http :as http]))
|
[status-im.utils.http :as http]))
|
||||||
|
|
||||||
(deftest chat-map->transaction-ids
|
|
||||||
(is (= #{} (transactions/chat-map->transaction-ids "testnet_rpc" {})))
|
|
||||||
(is (= #{"a" "b" "c" "d"}
|
|
||||||
(transactions/chat-map->transaction-ids
|
|
||||||
"testnet_rpc"
|
|
||||||
{:a {:messages {1 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "a"
|
|
||||||
:network "testnet"}}}}}
|
|
||||||
:b {:messages {1 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "b"
|
|
||||||
:network "testnet"}}}}}
|
|
||||||
:c {:messages {1 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "c"
|
|
||||||
:network "testnet"}}}
|
|
||||||
2 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "d"
|
|
||||||
:network "testnet"}}}}}})))
|
|
||||||
|
|
||||||
(is (= #{"a" "b" "c" "d" "e"}
|
|
||||||
(transactions/chat-map->transaction-ids
|
|
||||||
"testnet"
|
|
||||||
{:aa {:messages {1 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "a"
|
|
||||||
:network "testnet"}}}}}
|
|
||||||
:bb {:messages {1 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "b"
|
|
||||||
:network "testnet"}}}}}
|
|
||||||
:cc {:messages {1 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "c"
|
|
||||||
:network "testnet"}}}
|
|
||||||
2 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "d"
|
|
||||||
:network "testnet"}}}
|
|
||||||
3 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "e"
|
|
||||||
:network "testnet"}}}}}})))
|
|
||||||
(is (= #{"b"}
|
|
||||||
(transactions/chat-map->transaction-ids
|
|
||||||
"testnet_rpc"
|
|
||||||
{:aa {:public? true
|
|
||||||
:messages {1 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "a"
|
|
||||||
:network "testnet"}}}}}
|
|
||||||
:bb {:messages {1 {:content-type "command"
|
|
||||||
:content {:params {:tx-hash "b"
|
|
||||||
:network "testnet"}}}}}
|
|
||||||
:cc {:messages {1 {:content {:params {:tx-hash "c"
|
|
||||||
:network "testnet"}}}
|
|
||||||
2 {:content-type "command"}}}}))))
|
|
||||||
|
|
||||||
;; The following tests are fantastic for developing the async-periodic-exec
|
|
||||||
;; but dismal for CI because of their probablistic nature
|
|
||||||
#_(deftest async-periodic-exec
|
|
||||||
(testing "work-fn is executed and can be stopeed"
|
|
||||||
(let [executor (atom nil)
|
|
||||||
state (atom 0)]
|
|
||||||
(reset! executor
|
|
||||||
(transactions/async-periodic-exec
|
|
||||||
(fn [done-fn]
|
|
||||||
(swap! state inc)
|
|
||||||
(done-fn))
|
|
||||||
100
|
|
||||||
500))
|
|
||||||
(async test-done
|
|
||||||
(js/setTimeout
|
|
||||||
(fn []
|
|
||||||
(is (> 6 @state 2))
|
|
||||||
(transactions/async-periodic-stop! @executor)
|
|
||||||
(let [st @state]
|
|
||||||
(js/setTimeout
|
|
||||||
#(do
|
|
||||||
(is (= st @state))
|
|
||||||
(is (closed? @executor))
|
|
||||||
(test-done))
|
|
||||||
500)))
|
|
||||||
500)))))
|
|
||||||
|
|
||||||
#_(deftest async-periodic-exec-error-in-job
|
|
||||||
(testing "error thrown in job is caught and loop continues"
|
|
||||||
(let [executor (atom nil)
|
|
||||||
state (atom 0)]
|
|
||||||
(reset! executor
|
|
||||||
(transactions/async-periodic-exec
|
|
||||||
(fn [done-fn]
|
|
||||||
(swap! state inc)
|
|
||||||
(throw (ex-info "Throwing this on purpose in error-in-job test" {})))
|
|
||||||
10
|
|
||||||
100))
|
|
||||||
(async test-done
|
|
||||||
(js/setTimeout
|
|
||||||
(fn []
|
|
||||||
(is (> @state 1))
|
|
||||||
(transactions/async-periodic-stop! @executor)
|
|
||||||
(let [st @state]
|
|
||||||
(js/setTimeout
|
|
||||||
#(do
|
|
||||||
(is (= st @state))
|
|
||||||
(is (closed? @executor))
|
|
||||||
(test-done))
|
|
||||||
500)))
|
|
||||||
1000)))))
|
|
||||||
|
|
||||||
#_(deftest async-periodic-exec-job-takes-longer
|
|
||||||
(testing "job takes longer than expected, executor timeout but task side-effects are still applied"
|
|
||||||
(let [executor (atom nil)
|
|
||||||
state (atom 0)]
|
|
||||||
(reset! executor
|
|
||||||
(transactions/async-periodic-exec
|
|
||||||
(fn [done-fn] (js/setTimeout #(swap! state inc) 100))
|
|
||||||
10
|
|
||||||
1))
|
|
||||||
(async test-done
|
|
||||||
(js/setTimeout
|
|
||||||
(fn []
|
|
||||||
(transactions/async-periodic-stop! @executor)
|
|
||||||
(js/setTimeout
|
|
||||||
#(do (is (< 3 @state))
|
|
||||||
(test-done))
|
|
||||||
500))
|
|
||||||
500)))))
|
|
||||||
|
|
||||||
#_(deftest async-periodic-exec-stop-early
|
|
||||||
(testing "stopping early prevents any executions"
|
|
||||||
(let [executor (atom nil)
|
|
||||||
state (atom 0)]
|
|
||||||
(reset! executor
|
|
||||||
(transactions/async-periodic-exec
|
|
||||||
(fn [done-fn]
|
|
||||||
(swap! state inc)
|
|
||||||
(done-fn))
|
|
||||||
100
|
|
||||||
100))
|
|
||||||
(async test-done
|
|
||||||
(js/setTimeout
|
|
||||||
(fn []
|
|
||||||
(is (zero? @state))
|
|
||||||
(transactions/async-periodic-stop! @executor)
|
|
||||||
(let [st @state]
|
|
||||||
(js/setTimeout
|
|
||||||
(fn []
|
|
||||||
(is (zero? @state))
|
|
||||||
(test-done))
|
|
||||||
500)))
|
|
||||||
50)))))
|
|
||||||
|
|
||||||
(defn- uri-query-data [uri]
|
(defn- uri-query-data [uri]
|
||||||
(let [uri' (goog-uri/parse uri)
|
(let [uri' (goog-uri/parse uri)
|
||||||
accum (atom {})]
|
accum (atom {})]
|
||||||
|
@ -183,7 +38,7 @@
|
||||||
:sort "desc",
|
:sort "desc",
|
||||||
:apikey "DMSI4UAAKUBVGCDMVP3H2STAMSAUV7BYFI",
|
:apikey "DMSI4UAAKUBVGCDMVP3H2STAMSAUV7BYFI",
|
||||||
:q "json"}}
|
:q "json"}}
|
||||||
(uri-query-data (transactions/get-transaction-url :mainnet "asdfasdf"))))
|
(uri-query-data (transactions/get-transaction-url :mainnet "0xasdfasdf"))))
|
||||||
(is (= {:scheme "https",
|
(is (= {:scheme "https",
|
||||||
:domain "api-rinkeby.etherscan.io",
|
:domain "api-rinkeby.etherscan.io",
|
||||||
:path "/api",
|
:path "/api",
|
||||||
|
@ -196,28 +51,29 @@
|
||||||
:sort "desc",
|
:sort "desc",
|
||||||
:apikey "DMSI4UAAKUBVGCDMVP3H2STAMSAUV7BYFI",
|
:apikey "DMSI4UAAKUBVGCDMVP3H2STAMSAUV7BYFI",
|
||||||
:q "json"}}
|
:q "json"}}
|
||||||
(uri-query-data (transactions/get-transaction-url :rinkeby "asdfasdfg"))))
|
(uri-query-data (transactions/get-transaction-url :rinkeby "0xasdfasdfg"))))
|
||||||
(let [uri (-> (transactions/get-transaction-url :testnet "asdfasdfgg")
|
(let [uri (-> (transactions/get-transaction-url :testnet "0xasdfasdfgg")
|
||||||
uri-query-data)]
|
uri-query-data)]
|
||||||
(is (= "api-ropsten.etherscan.io" (:domain uri)))
|
(is (= "api-ropsten.etherscan.io" (:domain uri)))
|
||||||
(is (= "0xasdfasdfgg" (-> uri :query :address))))
|
(is (= "0xasdfasdfgg" (-> uri :query :address))))
|
||||||
(is (thrown? js/Error (transactions/get-transaction-url nil "asdfasdfg"))))
|
(is (thrown? js/Error (transactions/get-transaction-url nil "0xasdfasdfg"))))
|
||||||
|
|
||||||
(declare mock-etherscan-success-response
|
(declare mock-etherscan-success-response
|
||||||
mock-etherscan-error-response
|
mock-etherscan-error-response
|
||||||
mock-etherscan-empty-response)
|
mock-etherscan-empty-response)
|
||||||
|
|
||||||
(deftest etherscan-transactions
|
(deftest etherscan-history
|
||||||
(let [ky-set #{:block :hash :symbol :gas-price :value :gas-limit :type
|
(let [ky-set #{:block :hash :symbol :gas-price :value :gas-limit :type
|
||||||
:gas-used :from :timestamp :nonce :to :data}]
|
:gas-used :from :timestamp :nonce :to :data}]
|
||||||
(with-redefs [http/get (fn [url success-fn error-fn]
|
(with-redefs [http/get (fn [url success-fn error-fn]
|
||||||
(success-fn mock-etherscan-success-response))]
|
(success-fn mock-etherscan-success-response))]
|
||||||
(let [result (atom nil)]
|
(let [result (atom nil)]
|
||||||
(transactions/etherscan-transactions
|
(transactions/etherscan-history
|
||||||
:mainnet
|
:mainnet
|
||||||
"asdfasdf"
|
"0xasdfasdf"
|
||||||
#(reset! result %)
|
#(reset! result %)
|
||||||
(fn [er]))
|
(fn [er])
|
||||||
|
false)
|
||||||
(doseq [[tx-hash tx-map] @result]
|
(doseq [[tx-hash tx-map] @result]
|
||||||
(is (string? tx-hash))
|
(is (string? tx-hash))
|
||||||
(is (= tx-hash (:hash tx-map)))
|
(is (= tx-hash (:hash tx-map)))
|
||||||
|
@ -232,25 +88,25 @@
|
||||||
(with-redefs [http/get (fn [url success-fn error-fn]
|
(with-redefs [http/get (fn [url success-fn error-fn]
|
||||||
(success-fn mock-etherscan-empty-response))]
|
(success-fn mock-etherscan-empty-response))]
|
||||||
(let [result (atom nil)]
|
(let [result (atom nil)]
|
||||||
(transactions/etherscan-transactions
|
(transactions/etherscan-history
|
||||||
:mainnet
|
:mainnet
|
||||||
"asdfasdf"
|
"0xasdfasdf"
|
||||||
#(reset! result %)
|
#(reset! result %)
|
||||||
(fn [er]))
|
(fn [er])
|
||||||
|
false)
|
||||||
(is (= {} @result))))
|
(is (= {} @result))))
|
||||||
|
|
||||||
(with-redefs [http/get (fn [url success-fn error-fn]
|
(with-redefs [http/get (fn [url success-fn error-fn]
|
||||||
(success-fn mock-etherscan-error-response))]
|
(success-fn mock-etherscan-error-response))]
|
||||||
(let [result (atom nil)]
|
(let [result (atom nil)]
|
||||||
(transactions/etherscan-transactions
|
(transactions/etherscan-history
|
||||||
:mainnet
|
:mainnet
|
||||||
"asdfasdf"
|
"0xasdfasdf"
|
||||||
#(reset! result %)
|
#(reset! result %)
|
||||||
(fn [er]))
|
(fn [er])
|
||||||
|
false)
|
||||||
(is (= {} @result)))))
|
(is (= {} @result)))))
|
||||||
|
|
||||||
#_(run-tests)
|
|
||||||
|
|
||||||
(def mock-etherscan-error-response
|
(def mock-etherscan-error-response
|
||||||
"{\"status\":\"0\",\"message\":\"NOTOK\",\"result\":\"Error!\"}")
|
"{\"status\":\"0\",\"message\":\"NOTOK\",\"result\":\"Error!\"}")
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue