[wallet] use status-go wallet service

- remove use of etherscan and subscriptions
- replace by status-go
This commit is contained in:
yenda 2019-07-08 18:11:07 +02:00
parent c3c4a5170a
commit 562773fa1b
No known key found for this signature in database
GPG Key ID: 0095623C0069DCE6
13 changed files with 159 additions and 496 deletions

View File

@ -0,0 +1,8 @@
(ns status-im.ethereum.encode
(:require [status-im.js-dependencies :as dependencies]))
(defn utils [] (dependencies/web3-utils))
(defn uint
[x]
(.numberToHex (utils) x))

View File

@ -35,7 +35,8 @@
"status_joinPublicChat" {}
"status_chats" {}
"status_startOneOnOneChat" {}
"status_removeChat" {}})
"status_removeChat" {}
"wallet_getTransfers" {}})
(defn call
[{:keys [method params on-success on-error]}]

View File

@ -4,6 +4,7 @@
[status-im.ethereum.core :as ethereum]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.ethereum.tokens :as tokens]
[status-im.wallet.db :as wallet]
[status-im.ethereum.transactions.core :as transactions]
[status-im.utils.fx :as fx]
[taoensso.timbre :as log]))
@ -22,82 +23,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] :as cofx} {:keys [number transactions] :as block}]
(when number
(let [{:keys [:wallet/all-tokens :ethereum/current-block]} db
chain (ethereum/chain-keyword db)
chain-tokens (into {} (map (juxt :address identity)
(tokens/tokens-for all-tokens chain)))
wallet-address (ethereum/current-address db)
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)}}
(transactions/check-watched-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 new-token-transaction-filter
[{:keys [chain-tokens from to] :as args}]
(json-rpc/call
{:method "eth_newFilter"
:params [{:fromBlock "latest"
:toBlock "latest"
:topics [constants/event-transfer-hash from to]}]
:on-success (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
[]
(json-rpc/call
{:method "eth_newBlockFilter"
:on-success
(fn [[block-hash]]
(json-rpc/call
{:method "eth_getBlockByHash"
:params [block-hash true]
:on-success
(fn [block]
(re-frame/dispatch [:ethereum.signal/new-block block]))}))}))
(re-frame/reg-fx
:ethereum.subscriptions/new-block
new-block-filter)
(fx/defn initialize
[{:keys [db] :as cofx}]
[{:keys [db] :as cofx} historical? block-number accounts]
(let [{:keys [:wallet/all-tokens]} db
chain (ethereum/chain-keyword db)
chain-tokens (into {} (map (juxt :address identity)
(tokens/tokens-for all-tokens chain)))
normalized-address (ethereum/current-address db)
padded-address (transactions/add-padding normalized-address)]
{:ethereum.subscriptions/new-block nil
:ethereum.subscriptions/token-transactions
{:chain-tokens chain-tokens
:address padded-address}}))
(tokens/tokens-for all-tokens chain)))]
(fx/merge cofx
(cond-> {}
(not historical?)
(assoc :db (assoc db [:ethereum/current-block] block-number))
(not-empty accounts)
(assoc ::transactions/get-transfers {:chain-tokens chain-tokens
:from-block block-number}))
(transactions/check-watched-transactions))))
(fx/defn reorg
[{:keys [db] :as cofx} block-number accounts]
(let [{:keys [:wallet/all-tokens]} db
chain (ethereum/chain-keyword db)
chain-tokens (into {} (map (juxt :address identity)
(tokens/tokens-for all-tokens chain)))]
{:db (update-in db [:wallet :transactions]
wallet/remove-transactions-since-block block-number)
::transactions/get-transfers {:chain-tokens chain-tokens
:from-block block-number}}))
(fx/defn new-wallet-event
[{:keys [db] :as cofx} {:keys [type blockNumber accounts] :as event}]
(case type
"newblock" (new-block cofx false blockNumber accounts)
"history" (new-block cofx true blockNumber accounts)
"reorg" (reorg cofx blockNumber accounts)
(log/warn ::unknown-wallet-event :type type :event event)))

View File

@ -2,14 +2,27 @@
(:require [re-frame.core :as re-frame]
[status-im.constants :as constants]
[status-im.ethereum.decode :as decode]
[status-im.ethereum.encode :as encode]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.ethereum.transactions.etherscan :as transactions.etherscan]
[status-im.ethereum.core :as ethereum]
[status-im.ethereum.tokens :as tokens]
[status-im.utils.fx :as fx]
[status-im.utils.money :as money]
[status-im.wallet.core :as wallet]))
(def confirmations-count-threshold 12)
(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)))))
(defn add-padding [address]
{:pre [(string? address)]}
(str "0x000000000000000000000000" (subs address 2)))
@ -23,120 +36,61 @@
:decimals 18
:name "ERC20"})
(defn direction
[address to]
(if (= address to)
:inbound
:outbound))
(defn- parse-token-transfer
[chain-tokens direction transfer]
[chain-tokens 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 (money/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)))))
{:hash transactionHash
:symbol symbol
:from (remove-padding from)
:to (remove-padding to)
:value (money/bignumber data)
:type (direction address to)
: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})))
(defn enrich-transaction-from-new-block
[chain-tokens
{:keys [number timestamp]}
{:keys [transfer direction hash gasPrice value gas from input nonce to] :as transaction}]
(json-rpc/call
{:method "eth_getTransactionReceipt"
:params [hash]
:on-success
(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
:from from
:to to
:type direction
:value (str (decode/uint value))}))])))}))
(defn enrich-transfer
[chain-tokens transfer]
(let [{:keys [address blockNumber timestamp type transaction receipt from]} transfer
{:keys [hash gasPrice value gas input nonce to]} transaction
{:keys [gasUsed logs]} receipt
erc20? (= type "erc20")]
(merge {:block (str (decode/uint blockNumber))
:timestamp (* (decode/uint 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 erc20?
(parse-token-transfer chain-tokens (first logs))
;; this is not a ERC20 token transaction
{:hash hash
:symbol :ETH
:from from
:to to
:type (direction address to)
:value (str (decode/uint value))}))))
(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 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]
(json-rpc/call
{:method "eth_getBlockByHash"
:params [block-hash false]
:on-success
(fn [{:keys [timestamp number]}]
(let [timestamp (str (* timestamp 1000))]
(doseq [{:keys [hash] :as transfer} block-transfers]
(json-rpc/call
{:method "eth_getTransactionByHash"
:params [hash]
:on-success
(fn [{:keys [gasPrice gas input nonce]}]
(json-rpc/call
{:method "eth_getTransactionReceipt"
:params [hash]
:on-success
(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 enrich-transfers
[chain-tokens transfers]
(mapv (fn [transfer]
(enrich-transfer chain-tokens transfer))
transfers))
;; -----------------------------------------------
;; transactions api
@ -172,17 +126,24 @@
(let [watched-transactions
(select-keys (get-in db [:wallet :transactions])
(keys (get db :ethereum/watched-transactions)))]
(apply fx/merge cofx
(apply fx/merge
cofx
(map (fn [[_ transaction]]
(check-transaction transaction))
watched-transactions))))
(fx/defn new
[{:keys [db] :as cofx} {:keys [hash] :as transaction}]
(fx/defn add-transfer
[{:keys [db] :as cofx} {:keys [hash] :as transfer}]
(fx/merge cofx
{:db (assoc-in db [:wallet :transactions hash] transaction)}
(check-transaction transaction)
wallet/update-balances))
{:db (assoc-in db [:wallet :transactions hash] transfer)}
(check-transaction transfer)))
(fx/defn new-transfers
{:events [::new-transfers]}
[{:keys [db] :as cofx} transfers]
(let [add-transfers-fx (map add-transfer transfers)]
(apply fx/merge cofx (conj add-transfers-fx
wallet/update-balances))))
(fx/defn handle-history
[{:keys [db] :as cofx} transactions]
@ -198,6 +159,23 @@
[:wallet :transactions]
merge transactions)})
(re-frame/reg-fx
::get-transfers
(fn [{:keys [chain-tokens from-block to-block]
:or {from-block "0"
to-block nil}}]
;; start inbound token transaction subscriptions
;; outbound token transactions are already caught in new blocks filter
(json-rpc/call
{:method "wallet_getTransfers"
:params [(encode/uint from-block) (encode/uint to-block)]
:on-success #(re-frame/dispatch
[::new-transfers (enrich-transfers chain-tokens %)])})))
(fx/defn initialize
[cofx]
(transactions.etherscan/fetch-history cofx))
[{:keys [db] :as cofx}]
(let [{:keys [:wallet/all-tokens]} db
chain (ethereum/chain-keyword db)
chain-tokens (into {} (map (juxt :address identity)
(tokens/tokens-for all-tokens chain)))]
{::get-transfers {:chain-tokens chain-tokens}}))

View File

@ -1,191 +0,0 @@
(ns status-im.ethereum.transactions.etherscan
(:require [re-frame.core :as re-frame]
[status-im.ethereum.core :as ethereum]
[status-im.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 [:multiaccount :wallet/all-tokens]} db
chain (ethereum/chain-keyword db)
chain-tokens (into {} (map (juxt :address identity)
(tokens/tokens-for all-tokens chain)))
chaos-mode? (get-in multiaccount [:settings :chaos-mode?])
normalized-address (ethereum/current-address db)]
#: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?}}))

View File

@ -1835,11 +1835,6 @@
(fn [cofx [_ id handler]]
(ethereum.subscriptions/register-subscription cofx id handler)))
(handlers/register-handler-fx
:ethereum.signal/new-block
(fn [cofx [_ block]]
(ethereum.subscriptions/new-block cofx block)))
;; ethereum transactions events
(handlers/register-handler-fx
:ethereum.transactions.callback/fetch-history-success
@ -1856,11 +1851,6 @@
(fn [cofx [_ transactions]]
(ethereum.transactions/handle-token-history cofx transactions)))
(handlers/register-handler-fx
:ethereum.transactions/new
(fn [cofx [_ transaction]]
(ethereum.transactions/new cofx transaction)))
;; wallet events
(handlers/register-handler-fx

View File

@ -88,8 +88,7 @@
(wallet/initialize-tokens)
(wallet/update-balances)
(wallet/update-prices)
(transactions/initialize)
(ethereum.subscriptions/initialize)))
(transactions/initialize)))
(fx/defn user-login-without-creating-db
{:events [:multiaccounts.login.ui/password-input-submitted]}

View File

@ -120,6 +120,7 @@
:LightClient true
:MinimumPoW 0.001
:EnableNTPSync true}
:WalletConfig {:Enabled true}
:ShhextConfig {:BackupDisabledDataDir (utils.platform/no-backup-directory)
:InstallationID installation-id
:MaxMessageDeliveryAttempts config/max-message-delivery-attempts

View File

@ -83,4 +83,5 @@
"status.chats.did-change" (chat.loading/load-chats-from-rpc cofx)
"whisper.filter.added" (transport.filters/handle-negotiated-filter cofx event)
"messages.new" (transport.message/receive-messages cofx event)
"wallet" (ethereum.subscriptions/new-wallet-event cofx event)
(log/debug "Event " type " not handled" event))))

View File

@ -14,7 +14,6 @@
[status-im.ethereum.stateofus :as stateofus]
[status-im.ethereum.tokens :as tokens]
[status-im.ethereum.transactions.core :as transactions]
[status-im.ethereum.transactions.etherscan :as transactions.etherscan]
[status-im.fleet.core :as fleet]
[status-im.group-chats.db :as group-chats.db]
[status-im.i18n :as i18n]
@ -1166,12 +1165,12 @@
:<- [:contacts/contacts-by-address]
:<- [:ethereum/native-currency]
(fn [[transactions contacts native-currency]]
(reduce-kv (fn [acc hash transaction]
(assoc acc
hash
(enrich-transaction transaction contacts native-currency)))
{}
transactions)))
(reduce (fn [acc [hash transaction]]
(assoc acc
hash
(enrich-transaction transaction contacts native-currency)))
{}
transactions)))
(re-frame/reg-sub
:wallet.transactions/all-filters?
@ -1296,7 +1295,7 @@
(money/wei->str :eth
(money/fee-value gas-used gas-price)
native-currency-text))
:url (transactions.etherscan/get-transaction-details-url
:url (transactions/get-transaction-details-url
chain-keyword
hash)}))))))

View File

@ -230,3 +230,6 @@
(def empty-message-map
(priority-map-keyfn-by :clock-value >))
(def empty-transaction-map
(priority-map-keyfn-by :block #(< (int %1) (int %2))))

View File

@ -3,7 +3,8 @@
[status-im.i18n :as i18n]
status-im.ui.screens.wallet.request.db
status-im.ui.screens.wallet.send.db
[status-im.utils.money :as money]))
[status-im.utils.money :as money]
[status-im.utils.priority-map :refer [empty-transaction-map]]))
(spec/def :wallet.send/recipient string?)
@ -65,10 +66,18 @@
#{:inbound :outbound :pending :failed})
(def default-wallet
{:filters default-wallet-filters})
{:filters default-wallet-filters
:transactions empty-transaction-map})
(defn get-confirmations
[{:keys [block]} current-block]
(if (and current-block block)
(inc (- current-block block))
0))
(defn remove-transactions-since-block
[transactions block]
(into empty-transaction-map
(drop-while (fn [[k v]]
(>= (int (:block v)) block))
transactions)))

File diff suppressed because one or more lines are too long