[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/merge cofx
|
||||
(models.wallet/initialize-tokens)
|
||||
(transactions/initialize)
|
||||
(ethereum.subscriptions/initialize)
|
||||
(models.wallet/update-wallet)
|
||||
(transactions/start-sync)))
|
||||
(models.wallet/update-wallet)))
|
||||
|
||||
(fx/defn user-login [{:keys [db] :as cofx} create-database?]
|
||||
(let [{:keys [address password]} (accounts.db/credentials cofx)]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
(ns status-im.accounts.logout.core
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[status-im.chaos-mode.core :as chaos-mode]
|
||||
[status-im.ethereum.transactions.core :as transactions]
|
||||
[status-im.i18n :as i18n]
|
||||
[status-im.init.core :as init]
|
||||
[status-im.node.core :as node]
|
||||
|
@ -13,7 +12,6 @@
|
|||
(fx/merge cofx
|
||||
{:keychain/clear-user-password (get-in db [:account/account :address])
|
||||
:dev-server/stop nil}
|
||||
(transactions/stop-sync)
|
||||
(transport/stop-whisper
|
||||
#(re-frame/dispatch [:accounts.logout/filters-removed]))
|
||||
(chaos-mode/stop-checking)))
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
(:require [clojure.string :as string]
|
||||
[re-frame.core :as re-frame]
|
||||
[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.utils.ethereum.core :as ethereum]
|
||||
[status-im.utils.ethereum.tokens :as tokens]
|
||||
|
@ -10,57 +10,6 @@
|
|||
[status-im.utils.types :as types]
|
||||
[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
|
||||
[cofx {:keys [subscription_id data] :as event}]
|
||||
(if-let [handler (get-in cofx [:db :ethereum/subscriptions subscription_id])]
|
||||
|
@ -75,9 +24,37 @@
|
|||
[{:keys [db]} 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
|
||||
[{:keys [db]} block-number]
|
||||
{:db (assoc-in db [:ethereum/current-block] block-number)})
|
||||
[{:keys [db] :as cofx} {:keys [number transactions] :as block}]
|
||||
(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
|
||||
[filter params callback]
|
||||
|
@ -98,139 +75,33 @@
|
|||
result
|
||||
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
|
||||
[{:keys [chain-tokens from to] :as args}]
|
||||
(subscribe-signal
|
||||
"eth_newFilter"
|
||||
[{:fromBlock "latest"
|
||||
:toBlock "latest"
|
||||
:address (keys chain-tokens)
|
||||
: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
|
||||
[]
|
||||
(subscribe-signal
|
||||
"eth_newBlockFilter" []
|
||||
(fn [[block-hash]]
|
||||
(get-block-by-hash
|
||||
(transactions/get-block-by-hash
|
||||
block-hash
|
||||
(fn [block]
|
||||
(when-let [block-number (:number 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/dispatch [:ethereum.signal/new-block block]))))))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:ethereum.subscriptions/new-block
|
||||
|
@ -242,7 +113,9 @@
|
|||
chain (ethereum/network->chain-keyword (get-in account [:networks network]))
|
||||
chain-tokens (into {} (map (juxt :address identity)
|
||||
(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/token-transactions {:chain-tokens chain-tokens
|
||||
:ethereum.subscriptions/token-transactions
|
||||
{:chain-tokens chain-tokens
|
||||
:address padded-address}}))
|
||||
|
|
|
@ -1,252 +1,200 @@
|
|||
(ns status-im.ethereum.transactions.core
|
||||
(:require [clojure.set :as set]
|
||||
[clojure.string :as string]
|
||||
(:require [clojure.string :as string]
|
||||
[re-frame.core :as re-frame]
|
||||
re-frame.db
|
||||
[status-im.utils.async :as async-util]
|
||||
[status-im.constants :as constants]
|
||||
[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.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]))
|
||||
|
||||
(def sync-interval-ms 15000)
|
||||
(def sync-timeout-ms 20000)
|
||||
(def confirmations-count-threshold 12)
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; etherscan transactions
|
||||
;; --------------------------------------------------------------------------
|
||||
(defn get-block-by-hash
|
||||
[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-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)))))
|
||||
(defn get-transaction-receipt [transaction-hash callback]
|
||||
(status/call-private-rpc
|
||||
(types/clj->json {:jsonrpc "2.0"
|
||||
:id 1
|
||||
:method "eth_getTransactionReceipt"
|
||||
: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))))))
|
||||
|
||||
(def etherscan-api-key "DMSI4UAAKUBVGCDMVP3H2STAMSAUV7BYFI")
|
||||
(defn add-padding [address]
|
||||
{:pre [(string? address)]}
|
||||
(str "0x000000000000000000000000" (subs address 2)))
|
||||
|
||||
(defn- get-api-network-subdomain [chain]
|
||||
(case chain
|
||||
(:testnet) "api-ropsten"
|
||||
(:mainnet) "api"
|
||||
(:rinkeby) "api-rinkeby"))
|
||||
(defn- remove-padding [topic]
|
||||
{:pre [(string? topic)]}
|
||||
(str "0x" (subs topic 26)))
|
||||
|
||||
(defn- get-transaction-url
|
||||
([chain account] (get-transaction-url chain account false))
|
||||
([chain account chaos-mode?]
|
||||
{:pre [(keyword? chain) (string? account)]
|
||||
: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")))))
|
||||
(def default-erc20-token
|
||||
{:symbol :ERC20
|
||||
:decimals 18
|
||||
:name "ERC20"})
|
||||
|
||||
(defn- format-transaction [account
|
||||
{:keys [value timeStamp blockNumber hash from to
|
||||
gas gasPrice gasUsed nonce input isError]}]
|
||||
(let [inbound? (= (str "0x" account) to)
|
||||
error? (= "1" isError)]
|
||||
{:value value
|
||||
;; timestamp is in seconds, we convert it in ms
|
||||
:timestamp (str timeStamp "000")
|
||||
(defn- parse-token-transfer
|
||||
[chain-tokens direction transfer]
|
||||
(let [{:keys [blockHash transactionHash topics data address]} transfer
|
||||
[_ from to] topics
|
||||
{:keys [nft? symbol] :as token} (get chain-tokens address
|
||||
default-erc20-token)]
|
||||
(when-not nft?
|
||||
(cond-> {:hash transactionHash
|
||||
:symbol symbol
|
||||
:from (remove-padding from)
|
||||
:to (remove-padding to)
|
||||
:value (ethereum/hex->bignumber data)
|
||||
:type direction
|
||||
:token token
|
||||
: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}
|
||||
(= :inbound direction)
|
||||
(assoc :block-hash blockHash)))))
|
||||
|
||||
(defn enrich-transaction-from-new-block
|
||||
[chain-tokens
|
||||
{:keys [number timestamp]}
|
||||
{:keys [transfer direction hash gasPrice value gas from input nonce to] :as transaction}]
|
||||
(get-transaction-receipt
|
||||
hash
|
||||
(fn [{:keys [gasUsed logs] :as receipt}]
|
||||
(let [[event _ _] (:topics (first logs))
|
||||
transfer (= constants/event-transfer-hash event)]
|
||||
(re-frame/dispatch
|
||||
[:ethereum.transactions/new
|
||||
(merge {:block (str number)
|
||||
:timestamp (str (* timestamp 1000))
|
||||
:gas-used (str (decode/uint gasUsed))
|
||||
:gas-price (str (decode/uint gasPrice))
|
||||
:gas-limit (str (decode/uint gas))
|
||||
:nonce (str (decode/uint nonce))
|
||||
:data input}
|
||||
(if transfer
|
||||
(parse-token-transfer chain-tokens
|
||||
:outbound
|
||||
(first logs))
|
||||
;; this is not a ERC20 token transaction
|
||||
{:hash hash
|
||||
: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}))
|
||||
:type direction
|
||||
:value (str (decode/uint value))}))])))))
|
||||
|
||||
(defn- format-transactions-response [response account]
|
||||
(let [{:keys [result]} (types/json->clj response)]
|
||||
(cond-> {}
|
||||
(vector? result)
|
||||
(into (comp
|
||||
(map (partial format-transaction account))
|
||||
(map (juxt :hash identity)))
|
||||
result))))
|
||||
(re-frame/reg-fx
|
||||
:ethereum.transactions/enrich-transactions-from-new-blocks
|
||||
(fn [{:keys [chain-tokens block transactions]}]
|
||||
(doseq [transaction transactions]
|
||||
(enrich-transaction-from-new-block chain-tokens
|
||||
block
|
||||
transaction))))
|
||||
|
||||
(defn- etherscan-transactions
|
||||
([chain account on-success on-error]
|
||||
(etherscan-transactions chain account on-success on-error false))
|
||||
([chain account on-success on-error chaos-mode?]
|
||||
(if (etherscan-supported? chain)
|
||||
(let [url (get-transaction-url chain account chaos-mode?)]
|
||||
(log/debug "HTTP GET" url)
|
||||
(http/get url
|
||||
#(on-success (format-transactions-response % account))
|
||||
on-error))
|
||||
(log/info "Etherscan not supported for " chain))))
|
||||
(defn inbound-token-transfer-handler
|
||||
"The handler gets a list of inbound token transfer events and parses each
|
||||
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))))])))))))))))))
|
||||
|
||||
(defn- get-transactions [{:keys [web3 chain chain-tokens account-address
|
||||
success-fn error-fn chaos-mode?]}]
|
||||
(log/debug "Syncing transactions data..")
|
||||
(etherscan-transactions chain
|
||||
account-address
|
||||
success-fn
|
||||
error-fn
|
||||
chaos-mode?))
|
||||
;; -----------------------------------------------
|
||||
;; transactions api
|
||||
;; -----------------------------------------------
|
||||
|
||||
;; -----------------------------------------------------------------------------
|
||||
;; Helpers functions that help determine if a background sync should execute
|
||||
;; -----------------------------------------------------------------------------
|
||||
(fx/defn new
|
||||
[{:keys [db]} {:keys [hash] :as transaction}]
|
||||
{:db (assoc-in db [:wallet :transactions hash] transaction)})
|
||||
|
||||
(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
|
||||
(fx/defn handle-history
|
||||
[{:keys [db]} transactions]
|
||||
{:db (update-in 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?}))
|
||||
#(merge transactions %))})
|
||||
|
||||
(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?))))))
|
||||
(fx/defn handle-token-history
|
||||
[{:keys [db]} transactions]
|
||||
{:db (update-in db
|
||||
[:wallet :transactions]
|
||||
merge transactions)})
|
||||
|
||||
;; 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
|
||||
::sync-transactions-now
|
||||
(fn [db] (sync-now! db)))
|
||||
|
||||
(re-frame/reg-fx
|
||||
::start-sync-transactions
|
||||
(fn [db] (start-sync! db)))
|
||||
|
||||
(fx/defn start-sync [{:keys [db]}]
|
||||
{::start-sync-transactions
|
||||
(select-keys db [:network-status :account/account :wallet/all-tokens
|
||||
:app-state :network :web3])})
|
||||
|
||||
(re-frame/reg-fx
|
||||
::stop-sync-transactions
|
||||
#(when @polling-executor
|
||||
(async-util/async-periodic-stop! @polling-executor)))
|
||||
|
||||
(fx/defn stop-sync [_]
|
||||
{::stop-sync-transactions nil})
|
||||
(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.core :as contact]
|
||||
[status-im.ethereum.subscriptions :as ethereum.subscriptions]
|
||||
[status-im.ethereum.transactions.core :as ethereum.transactions]
|
||||
[status-im.extensions.core :as extensions]
|
||||
[status-im.extensions.registry :as extensions.registry]
|
||||
[status-im.fleet.core :as fleet]
|
||||
|
@ -2120,10 +2121,26 @@
|
|||
|
||||
(handlers/register-handler-fx
|
||||
:ethereum.signal/new-block
|
||||
(fn [cofx [_ block-number]]
|
||||
(ethereum.subscriptions/new-block cofx block-number)))
|
||||
(fn [cofx [_ block]]
|
||||
(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
|
||||
: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]]
|
||||
(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.fleet.core :as fleet]
|
||||
[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.ui.components.bottom-bar.styles :as tabs.styles]
|
||||
[status-im.ui.components.toolbar.styles :as toolbar.styles]
|
||||
|
@ -1133,7 +1135,7 @@
|
|||
:hash (i18n/label :not-applicable)}
|
||||
{:cost (when gas-used
|
||||
(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
|
||||
:wallet.transactions.details/confirmations
|
||||
|
@ -1141,7 +1143,7 @@
|
|||
:<- [:wallet.transactions/transaction-details]
|
||||
(fn [[current-block {:keys [block]}]]
|
||||
(if (and current-block block)
|
||||
(- current-block block)
|
||||
(inc (- current-block block))
|
||||
0)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
(ns status-im.ui.screens.wallet.events
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[status-im.ethereum.transactions.core :as transactions]
|
||||
[status-im.i18n :as i18n]
|
||||
[status-im.models.wallet :as models]
|
||||
[status-im.ui.screens.navigation :as navigation]
|
||||
|
@ -143,13 +142,6 @@
|
|||
(navigation/navigate-back)
|
||||
(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
|
||||
:update-balance-success
|
||||
(fn [{:keys [db]} [_ balance]]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
(ns status-im.ui.screens.wallet.navigation
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[status-im.constants :as constants]
|
||||
[status-im.ui.screens.navigation :as navigation]
|
||||
[status-im.utils.ethereum.core :as ethereum]
|
||||
[status-im.constants :as constants]
|
||||
|
@ -25,11 +26,6 @@
|
|||
500)
|
||||
(assoc-in db [:wallet :current-tab] 0))
|
||||
|
||||
(defmethod navigation/preload-data! :transactions-history
|
||||
[db _]
|
||||
(re-frame/dispatch [:update-transactions])
|
||||
db)
|
||||
|
||||
(def transaction-send-default
|
||||
(let [symbol :ETH]
|
||||
{:gas (ethereum/estimate-gas symbol)
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
(ns status-im.ui.screens.wallet.transactions.views
|
||||
(:require-macros [status-im.utils.views :refer [defview letsubs]])
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[status-im.i18n :as i18n]
|
||||
[status-im.ui.components.colors :as colors]
|
||||
[status-im.ui.components.list.views :as list]
|
||||
[status-im.ui.components.react :as react]
|
||||
[status-im.ui.components.status-bar.view :as status-bar]
|
||||
[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.view :as toolbar]
|
||||
[status-im.ui.components.status-bar.view :as status-bar]
|
||||
[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.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?]
|
||||
(cond->
|
||||
|
@ -111,7 +109,6 @@
|
|||
:render-fn #(render-transaction % network all-tokens hide-details?)
|
||||
:empty-component [react/i18n-text {:style styles/empty-text
|
||||
:key :transactions-history-empty}]
|
||||
:on-refresh #(re-frame/dispatch [:update-transactions])
|
||||
:refreshing false}]]))
|
||||
|
||||
;; Filter history
|
||||
|
@ -263,4 +260,3 @@
|
|||
[details-confirmations confirmations confirmations-progress type]
|
||||
[react/view {:style styles/details-separator}]
|
||||
[details-list transaction]]]))
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
:body body}))))))
|
||||
(.catch (or on-error
|
||||
(fn [error]
|
||||
(utils/show-popup "Error" (str error))))))))
|
||||
(utils/show-popup "Error" url (str error))))))))
|
||||
|
||||
(defn post
|
||||
"Performs an HTTP POST request"
|
||||
|
@ -72,7 +72,7 @@
|
|||
(.catch (fn [error]
|
||||
(if on-error
|
||||
(on-error {:response-body error})
|
||||
(utils/show-popup "Error" (str error))))))))
|
||||
(utils/show-popup "Error" url (str error))))))))
|
||||
|
||||
(defn raw-get
|
||||
"Performs an HTTP GET request and returns raw results :status :headers :body."
|
||||
|
@ -94,7 +94,7 @@
|
|||
:body body}))))))
|
||||
(.catch (or on-error
|
||||
(fn [error]
|
||||
(utils/show-popup "Error" (str error))))))))
|
||||
(utils/show-popup "Error" url (str error))))))))
|
||||
|
||||
(defn get
|
||||
"Performs an HTTP GET request"
|
||||
|
@ -129,7 +129,7 @@
|
|||
:else false)))
|
||||
(.catch (or on-error
|
||||
(fn [error]
|
||||
(utils/show-popup "Error" (str error))))))))
|
||||
(utils/show-popup "Error" url (str error))))))))
|
||||
|
||||
(defn normalize-url [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 :web3/get-syncing))
|
||||
(is (contains? efx :get-tokens-balance))
|
||||
(is (contains? efx :get-prices))
|
||||
(is (contains? efx :status-im.ethereum.transactions.core/start-sync-transactions))))))
|
||||
(is (contains? efx :get-prices))))))
|
||||
|
||||
(deftest login-failed
|
||||
(testing
|
||||
|
|
|
@ -1,154 +1,9 @@
|
|||
(ns status-im.test.wallet.transactions
|
||||
(:require [cljs.test :refer-macros [deftest is]]
|
||||
[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]))
|
||||
|
||||
(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]
|
||||
(let [uri' (goog-uri/parse uri)
|
||||
accum (atom {})]
|
||||
|
@ -183,7 +38,7 @@
|
|||
:sort "desc",
|
||||
:apikey "DMSI4UAAKUBVGCDMVP3H2STAMSAUV7BYFI",
|
||||
:q "json"}}
|
||||
(uri-query-data (transactions/get-transaction-url :mainnet "asdfasdf"))))
|
||||
(uri-query-data (transactions/get-transaction-url :mainnet "0xasdfasdf"))))
|
||||
(is (= {:scheme "https",
|
||||
:domain "api-rinkeby.etherscan.io",
|
||||
:path "/api",
|
||||
|
@ -196,28 +51,29 @@
|
|||
:sort "desc",
|
||||
:apikey "DMSI4UAAKUBVGCDMVP3H2STAMSAUV7BYFI",
|
||||
:q "json"}}
|
||||
(uri-query-data (transactions/get-transaction-url :rinkeby "asdfasdfg"))))
|
||||
(let [uri (-> (transactions/get-transaction-url :testnet "asdfasdfgg")
|
||||
(uri-query-data (transactions/get-transaction-url :rinkeby "0xasdfasdfg"))))
|
||||
(let [uri (-> (transactions/get-transaction-url :testnet "0xasdfasdfgg")
|
||||
uri-query-data)]
|
||||
(is (= "api-ropsten.etherscan.io" (:domain uri)))
|
||||
(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
|
||||
mock-etherscan-error-response
|
||||
mock-etherscan-empty-response)
|
||||
|
||||
(deftest etherscan-transactions
|
||||
(deftest etherscan-history
|
||||
(let [ky-set #{:block :hash :symbol :gas-price :value :gas-limit :type
|
||||
:gas-used :from :timestamp :nonce :to :data}]
|
||||
(with-redefs [http/get (fn [url success-fn error-fn]
|
||||
(success-fn mock-etherscan-success-response))]
|
||||
(let [result (atom nil)]
|
||||
(transactions/etherscan-transactions
|
||||
(transactions/etherscan-history
|
||||
:mainnet
|
||||
"asdfasdf"
|
||||
"0xasdfasdf"
|
||||
#(reset! result %)
|
||||
(fn [er]))
|
||||
(fn [er])
|
||||
false)
|
||||
(doseq [[tx-hash tx-map] @result]
|
||||
(is (string? tx-hash))
|
||||
(is (= tx-hash (:hash tx-map)))
|
||||
|
@ -232,25 +88,25 @@
|
|||
(with-redefs [http/get (fn [url success-fn error-fn]
|
||||
(success-fn mock-etherscan-empty-response))]
|
||||
(let [result (atom nil)]
|
||||
(transactions/etherscan-transactions
|
||||
(transactions/etherscan-history
|
||||
:mainnet
|
||||
"asdfasdf"
|
||||
"0xasdfasdf"
|
||||
#(reset! result %)
|
||||
(fn [er]))
|
||||
(fn [er])
|
||||
false)
|
||||
(is (= {} @result))))
|
||||
|
||||
(with-redefs [http/get (fn [url success-fn error-fn]
|
||||
(success-fn mock-etherscan-error-response))]
|
||||
(let [result (atom nil)]
|
||||
(transactions/etherscan-transactions
|
||||
(transactions/etherscan-history
|
||||
:mainnet
|
||||
"asdfasdf"
|
||||
"0xasdfasdf"
|
||||
#(reset! result %)
|
||||
(fn [er]))
|
||||
(fn [er])
|
||||
false)
|
||||
(is (= {} @result)))))
|
||||
|
||||
#_(run-tests)
|
||||
|
||||
(def mock-etherscan-error-response
|
||||
"{\"status\":\"0\",\"message\":\"NOTOK\",\"result\":\"Error!\"}")
|
||||
|
||||
|
|
Loading…
Reference in New Issue