feature #3976 - getting token transfer events for erc20 tx history
Signed-off-by: Andrey Shovkoplyas <motor4ik@gmail.com>
This commit is contained in:
parent
db2f8c1515
commit
e74bc81ce9
|
@ -111,5 +111,9 @@
|
|||
|
||||
(def ^:const web3-send-transaction "eth_sendTransaction")
|
||||
(def ^:const web3-personal-sign "personal_sign")
|
||||
(def ^:const web3-get-logs "eth_getLogs")
|
||||
|
||||
(def ^:const event-transfer-hash
|
||||
(ethereum/sha3 "Transfer(address,address,uint256)"))
|
||||
|
||||
(def regx-emoji #"^((?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC69\uDC6E\uDC70-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD26\uDD30-\uDD39\uDD3D\uDD3E\uDDD1-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])?|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDEEB\uDEEC\uDEF4-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267B\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])\uFE0F|[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF])+$")
|
||||
|
|
|
@ -64,12 +64,19 @@
|
|||
:on-error #(re-frame/dispatch [error-event %])})))))
|
||||
|
||||
(reg-fx
|
||||
:get-transactions
|
||||
(fn [{:keys [network account-id success-event error-event]}]
|
||||
(transactions/get-transactions network
|
||||
account-id
|
||||
#(re-frame/dispatch [success-event %])
|
||||
#(re-frame/dispatch [error-event %]))))
|
||||
:get-transactions
|
||||
(fn [{:keys [web3 network account-id token-addresses success-event error-event]}]
|
||||
(transactions/get-transactions network
|
||||
account-id
|
||||
#(re-frame/dispatch [success-event %])
|
||||
#(re-frame/dispatch [error-event %]))
|
||||
(doseq [direction [:inbound :outbound]]
|
||||
(erc20/get-token-transactions web3
|
||||
network
|
||||
token-addresses
|
||||
direction
|
||||
account-id
|
||||
#(re-frame/dispatch [success-event %])))))
|
||||
|
||||
;; TODO(oskarth): At some point we want to get list of relevant assets to get prices for
|
||||
(reg-fx
|
||||
|
@ -120,23 +127,44 @@
|
|||
(assoc :prices-loading? true))}))))
|
||||
|
||||
(handlers/register-handler-fx
|
||||
:update-transactions
|
||||
(fn [{{:keys [network network-status] :as db} :db} _]
|
||||
(when (not= network-status :offline)
|
||||
{:get-transactions {:account-id (get-in db [:account/account :address])
|
||||
:network network
|
||||
:success-event :update-transactions-success
|
||||
:error-event :update-transactions-fail}
|
||||
:db (-> db
|
||||
(clear-error-message :transactions-update)
|
||||
(assoc-in [:wallet :transactions-loading?] true))})))
|
||||
:update-transactions
|
||||
(fn [{{:keys [network network-status web3] :as db} :db} _]
|
||||
(when (not= network-status :offline)
|
||||
(let [chain (ethereum/network->chain-keyword network)
|
||||
all-tokens (tokens/tokens-for chain)
|
||||
token-addresses (map :address all-tokens)]
|
||||
{:get-transactions {:account-id (get-in db [:account/account :address])
|
||||
:token-addresses token-addresses
|
||||
:network network
|
||||
:web3 web3
|
||||
:success-event :update-transactions-success
|
||||
:error-event :update-transactions-fail}
|
||||
:db (-> db
|
||||
(clear-error-message :transactions-update)
|
||||
(assoc-in [:wallet :transactions-loading?] true))}))))
|
||||
|
||||
(defn combine-entries [transaction token-transfer]
|
||||
(merge transaction (select-keys token-transfer
|
||||
(if (= :ETH (:symbol transaction))
|
||||
[:symbol :from :to :value :type :token]
|
||||
[:confirmations]))))
|
||||
|
||||
(defn- tx-and-transfer?
|
||||
"A helper function that checks if first argument is a transaction and the second argument a token transfer object."
|
||||
[tx1 tx2]
|
||||
(and (not (: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))
|
||||
|
||||
(handlers/register-handler-db
|
||||
:update-transactions-success
|
||||
(fn [db [_ transactions]]
|
||||
(-> db
|
||||
(update-in [:wallet :transactions] merge transactions)
|
||||
(assoc-in [:wallet :transactions-loading?] false))))
|
||||
:update-transactions-success
|
||||
(fn [db [_ transactions]]
|
||||
(-> db
|
||||
(update-in [:wallet :transactions] #(merge-with dedupe-transactions % transactions))
|
||||
(assoc-in [:wallet :transactions-loading?] false))))
|
||||
|
||||
(handlers/register-handler-db
|
||||
:update-transactions-fail
|
||||
|
|
|
@ -128,8 +128,8 @@
|
|||
{:keys [gas-used gas-price hash timestamp type] :as transaction} (get transactions current-transaction)]
|
||||
(when transaction
|
||||
(merge transaction
|
||||
{:gas-price-eth (money/wei->str :eth gas-price)
|
||||
:gas-price-gwei (money/wei->str :gwei gas-price)
|
||||
{:gas-price-eth (if gas-price (money/wei->str :eth gas-price) "-")
|
||||
:gas-price-gwei (if gas-price (money/wei->str :gwei gas-price) "-")
|
||||
:date (datetime/timestamp->long-date timestamp)}
|
||||
(if (= type :unsigned)
|
||||
{:block (i18n/label :not-applicable)
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
(:postponed :pending) (transaction-icon :icons/arrow-right components.styles/color-gray4-transparent components.styles/color-gray7)
|
||||
(throw (str "Unknown transaction type: " k))))
|
||||
|
||||
(defn render-transaction [{:keys [hash from-contact to-contact to from type value time-formatted] :as transaction}]
|
||||
(defn render-transaction [{:keys [hash from-contact to-contact to from type value time-formatted symbol] :as transaction}]
|
||||
(let [[label contact address
|
||||
contact-accessibility-label
|
||||
address-accessibility-label] (if (inbound? type)
|
||||
|
@ -84,7 +84,7 @@
|
|||
(->> value (money/wei-> :eth) money/to-fixed str)]
|
||||
" "
|
||||
[react/text {:accessibility-label :currency-text}
|
||||
(clojure.string/upper-case (name :eth))]]
|
||||
(clojure.string/upper-case (name symbol))]]
|
||||
[react/text {:style styles/tx-time}
|
||||
time-formatted]]
|
||||
[react/view {:style styles/address-row}
|
||||
|
@ -220,25 +220,21 @@
|
|||
:unsigned-transactions unsigned-list
|
||||
react/view)]]))
|
||||
|
||||
(defn- pretty-print-asset [symbol amount & [with-currency?]]
|
||||
(let [token (case symbol
|
||||
;; TODO (jeluard) Format tokens amount once tokens history is supported
|
||||
:ETH :eth
|
||||
(throw (str "Unknown asset symbol: " symbol)))]
|
||||
(if amount
|
||||
(if with-currency?
|
||||
(money/wei->str token amount)
|
||||
(->> amount (money/wei-> token) money/to-fixed str))
|
||||
"...")))
|
||||
(defn- pretty-print-asset [symbol amount token]
|
||||
(if amount
|
||||
(if (= :ETH symbol)
|
||||
(->> amount (money/wei-> :eth) money/to-fixed str)
|
||||
(-> amount (money/token->unit (:decimals token)) money/to-fixed str))
|
||||
"..."))
|
||||
|
||||
(defn details-header [{:keys [value date type symbol]}]
|
||||
(defn details-header [{:keys [value date type symbol token]}]
|
||||
[react/view {:style styles/details-header}
|
||||
[react/view {:style styles/details-header-icon}
|
||||
[list/item-icon (transaction-type->icon type)]]
|
||||
[react/view {:style styles/details-header-infos}
|
||||
[react/text {:style styles/details-header-value}
|
||||
[react/text {:accessibility-label :amount-text}
|
||||
(pretty-print-asset symbol value)]
|
||||
(pretty-print-asset symbol value token)]
|
||||
" "
|
||||
[react/text {:accessibility-label :currency-text}
|
||||
(clojure.string/upper-case (name symbol))]]
|
||||
|
@ -271,7 +267,7 @@
|
|||
[react/text {:style styles/details-item-label} (i18n/label label)]
|
||||
[react/view {:style styles/details-item-value-wrapper}
|
||||
[react/text (merge {:style styles/details-item-value} props)
|
||||
(str value)]
|
||||
(str (or value "-"))]
|
||||
[react/text (merge {:style styles/details-item-extra-value} extra-props)
|
||||
(str extra-value)]]])))
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
(:require [clojure.string :as string]
|
||||
[status-im.js-dependencies :as dependencies]
|
||||
[status-im.utils.ethereum.tokens :as tokens]
|
||||
[status-im.utils.money :as money]))
|
||||
[status-im.utils.money :as money]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
;; IDs standardized in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#list-of-chain-ids
|
||||
|
||||
|
@ -95,3 +96,31 @@
|
|||
default-transaction-gas
|
||||
;; TODO(jeluard) Rely on estimateGas call
|
||||
(.times default-transaction-gas 5)))
|
||||
|
||||
(defn handle-error [error]
|
||||
(log/info (.stringify js/JSON error)))
|
||||
|
||||
(defn get-block-number [web3 cb]
|
||||
(.getBlockNumber (.-eth web3)
|
||||
(fn [error result]
|
||||
(if (seq error)
|
||||
(handle-error error)
|
||||
(cb result)))))
|
||||
|
||||
(defn get-block-info [web3 number cb]
|
||||
(.getBlock (.-eth web3) number (fn [error result]
|
||||
(if (seq error)
|
||||
(handle-error error)
|
||||
(cb (js->clj result :keywordize-keys true))))))
|
||||
|
||||
(defn get-transaction [web3 number cb]
|
||||
(.getTransaction (.-eth web3) number (fn [error result]
|
||||
(if (seq error)
|
||||
(handle-error error)
|
||||
(cb (js->clj result :keywordize-keys true))))))
|
||||
|
||||
(defn get-transaction-receipt [web3 number cb]
|
||||
(.getTransactionReceipt (.-eth web3) number (fn [error result]
|
||||
(if (seq error)
|
||||
(handle-error error)
|
||||
(cb (js->clj result :keywordize-keys true))))))
|
||||
|
|
|
@ -15,7 +15,11 @@
|
|||
|
||||
=> 29166666
|
||||
"
|
||||
(:require [status-im.utils.ethereum.core :as ethereum])
|
||||
(:require [status-im.utils.ethereum.core :as ethereum]
|
||||
[status-im.native-module.core :as status]
|
||||
[status-im.utils.ethereum.tokens :as tokens]
|
||||
[status-im.constants :as constants]
|
||||
[status-im.utils.datetime :as datetime])
|
||||
(:refer-clojure :exclude [name symbol]))
|
||||
|
||||
(defn name [web3 contract cb]
|
||||
|
@ -57,4 +61,103 @@
|
|||
(defn allowance [web3 contract owner-address spender-address cb]
|
||||
(ethereum/call web3
|
||||
(ethereum/call-params contract "allowance(address,address)" (ethereum/normalized-address owner-address) (ethereum/normalized-address spender-address))
|
||||
#(cb %1 (ethereum/hex->bignumber %2))))
|
||||
#(cb %1 (ethereum/hex->bignumber %2))))
|
||||
|
||||
(defn- parse-json [s]
|
||||
(try
|
||||
(let [res (-> s
|
||||
js/JSON.parse
|
||||
(js->clj :keywordize-keys true))]
|
||||
(if (= (:error res) "")
|
||||
{:result true}
|
||||
res))
|
||||
(catch :default e
|
||||
{:error (.-message e)})))
|
||||
|
||||
(defn- add-padding [address]
|
||||
(when address
|
||||
(str "0x000000000000000000000000" (subs address 2))))
|
||||
|
||||
(defn- remove-padding [topic]
|
||||
(if topic
|
||||
(str "0x" (subs topic 26))))
|
||||
|
||||
(defn- parse-transaction-entry [block-number chain direction entries]
|
||||
(into {}
|
||||
(for [entry entries]
|
||||
(let [token (->> entry :address (tokens/address->token chain))]
|
||||
[(:transactionHash entry)
|
||||
{:block (-> entry :blockNumber ethereum/hex->int str)
|
||||
:hash (:transactionHash entry)
|
||||
:symbol (:symbol token)
|
||||
:from (-> entry :topics second remove-padding)
|
||||
:to (-> entry :topics last remove-padding)
|
||||
:value (-> entry :data ethereum/hex->bignumber)
|
||||
:type direction
|
||||
|
||||
:confirmations (str (- block-number (-> entry :blockNumber ethereum/hex->int)))
|
||||
|
||||
:gas-price nil
|
||||
:nonce nil
|
||||
:data nil
|
||||
|
||||
:gas-limit nil
|
||||
;; NOTE(goranjovic) - timestamp is mocked to the current time so that the transaction is shown at the
|
||||
;; top of transaction history list between the moment when transfer event was detected and actual
|
||||
;; timestamp was retrieved from block info.
|
||||
:timestamp (str (datetime/timestamp))
|
||||
|
||||
:gas-used nil
|
||||
|
||||
;; NOTE(goranjovic) - metadata on the type of token in question: contains name, symbol, decimas, address.
|
||||
:token token
|
||||
|
||||
;; 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- response-handler [block-number chain direction error-fn success-fn]
|
||||
(fn handle-response
|
||||
([response]
|
||||
(let [{:keys [error result]} (parse-json response)]
|
||||
(handle-response error result)))
|
||||
([error result]
|
||||
(if error
|
||||
(error-fn error)
|
||||
(success-fn (parse-transaction-entry block-number chain direction result))))))
|
||||
|
||||
;;
|
||||
;; 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 get-token-transfer-logs
|
||||
;; NOTE(goranjovic): here we cannot use web3 since events don't work with infura
|
||||
[block-number network contracts direction address cb]
|
||||
(let [chain (ethereum/network->chain-keyword network)
|
||||
[from to] (if (= :inbound direction)
|
||||
[nil (ethereum/normalized-address address)]
|
||||
[(ethereum/normalized-address address) nil])
|
||||
args {:jsonrpc "2.0"
|
||||
:id 2
|
||||
:method constants/web3-get-logs
|
||||
:params [{:address contracts
|
||||
:fromBlock "0x0"
|
||||
:topics [constants/event-transfer-hash
|
||||
(add-padding from)
|
||||
(add-padding to)]}]}
|
||||
payload (.stringify js/JSON (clj->js args))]
|
||||
(status/call-web3-private payload
|
||||
(response-handler block-number chain direction ethereum/handle-error cb))))
|
||||
|
||||
(defn get-token-transactions
|
||||
[web3 network contracts direction address cb]
|
||||
(ethereum/get-block-number web3
|
||||
#(get-token-transfer-logs % network contracts direction address cb)))
|
||||
|
|
Loading…
Reference in New Issue