feature #3976 - getting token transfer events for erc20 tx history

Signed-off-by: Andrey Shovkoplyas <motor4ik@gmail.com>
This commit is contained in:
Goran Jovic 2018-04-24 11:24:13 +02:00 committed by Andrey Shovkoplyas
parent db2f8c1515
commit e74bc81ce9
No known key found for this signature in database
GPG Key ID: EAAB7C8622D860A4
6 changed files with 201 additions and 41 deletions

View File

@ -111,5 +111,9 @@
(def ^:const web3-send-transaction "eth_sendTransaction") (def ^:const web3-send-transaction "eth_sendTransaction")
(def ^:const web3-personal-sign "personal_sign") (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])+$") (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])+$")

View File

@ -65,11 +65,18 @@
(reg-fx (reg-fx
:get-transactions :get-transactions
(fn [{:keys [network account-id success-event error-event]}] (fn [{:keys [web3 network account-id token-addresses success-event error-event]}]
(transactions/get-transactions network (transactions/get-transactions network
account-id account-id
#(re-frame/dispatch [success-event %]) #(re-frame/dispatch [success-event %])
#(re-frame/dispatch [error-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 ;; TODO(oskarth): At some point we want to get list of relevant assets to get prices for
(reg-fx (reg-fx
@ -121,21 +128,42 @@
(handlers/register-handler-fx (handlers/register-handler-fx
:update-transactions :update-transactions
(fn [{{:keys [network network-status] :as db} :db} _] (fn [{{:keys [network network-status web3] :as db} :db} _]
(when (not= network-status :offline) (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]) {:get-transactions {:account-id (get-in db [:account/account :address])
:token-addresses token-addresses
:network network :network network
:web3 web3
:success-event :update-transactions-success :success-event :update-transactions-success
:error-event :update-transactions-fail} :error-event :update-transactions-fail}
:db (-> db :db (-> db
(clear-error-message :transactions-update) (clear-error-message :transactions-update)
(assoc-in [:wallet :transactions-loading?] true))}))) (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 (handlers/register-handler-db
:update-transactions-success :update-transactions-success
(fn [db [_ transactions]] (fn [db [_ transactions]]
(-> db (-> db
(update-in [:wallet :transactions] merge transactions) (update-in [:wallet :transactions] #(merge-with dedupe-transactions % transactions))
(assoc-in [:wallet :transactions-loading?] false)))) (assoc-in [:wallet :transactions-loading?] false))))
(handlers/register-handler-db (handlers/register-handler-db

View File

@ -128,8 +128,8 @@
{:keys [gas-used gas-price hash timestamp type] :as transaction} (get transactions current-transaction)] {:keys [gas-used gas-price hash timestamp type] :as transaction} (get transactions current-transaction)]
(when transaction (when transaction
(merge transaction (merge transaction
{:gas-price-eth (money/wei->str :eth gas-price) {:gas-price-eth (if gas-price (money/wei->str :eth gas-price) "-")
:gas-price-gwei (money/wei->str :gwei gas-price) :gas-price-gwei (if gas-price (money/wei->str :gwei gas-price) "-")
:date (datetime/timestamp->long-date timestamp)} :date (datetime/timestamp->long-date timestamp)}
(if (= type :unsigned) (if (= type :unsigned)
{:block (i18n/label :not-applicable) {:block (i18n/label :not-applicable)

View File

@ -64,7 +64,7 @@
(:postponed :pending) (transaction-icon :icons/arrow-right components.styles/color-gray4-transparent components.styles/color-gray7) (:postponed :pending) (transaction-icon :icons/arrow-right components.styles/color-gray4-transparent components.styles/color-gray7)
(throw (str "Unknown transaction type: " k)))) (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 (let [[label contact address
contact-accessibility-label contact-accessibility-label
address-accessibility-label] (if (inbound? type) address-accessibility-label] (if (inbound? type)
@ -84,7 +84,7 @@
(->> value (money/wei-> :eth) money/to-fixed str)] (->> value (money/wei-> :eth) money/to-fixed str)]
" " " "
[react/text {:accessibility-label :currency-text} [react/text {:accessibility-label :currency-text}
(clojure.string/upper-case (name :eth))]] (clojure.string/upper-case (name symbol))]]
[react/text {:style styles/tx-time} [react/text {:style styles/tx-time}
time-formatted]] time-formatted]]
[react/view {:style styles/address-row} [react/view {:style styles/address-row}
@ -220,25 +220,21 @@
:unsigned-transactions unsigned-list :unsigned-transactions unsigned-list
react/view)]])) react/view)]]))
(defn- pretty-print-asset [symbol amount & [with-currency?]] (defn- pretty-print-asset [symbol amount token]
(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 amount
(if with-currency? (if (= :ETH symbol)
(money/wei->str token amount) (->> amount (money/wei-> :eth) money/to-fixed str)
(->> amount (money/wei-> token) 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}
[react/view {:style styles/details-header-icon} [react/view {:style styles/details-header-icon}
[list/item-icon (transaction-type->icon type)]] [list/item-icon (transaction-type->icon type)]]
[react/view {:style styles/details-header-infos} [react/view {:style styles/details-header-infos}
[react/text {:style styles/details-header-value} [react/text {:style styles/details-header-value}
[react/text {:accessibility-label :amount-text} [react/text {:accessibility-label :amount-text}
(pretty-print-asset symbol value)] (pretty-print-asset symbol value token)]
" " " "
[react/text {:accessibility-label :currency-text} [react/text {:accessibility-label :currency-text}
(clojure.string/upper-case (name symbol))]] (clojure.string/upper-case (name symbol))]]
@ -271,7 +267,7 @@
[react/text {:style styles/details-item-label} (i18n/label label)] [react/text {:style styles/details-item-label} (i18n/label label)]
[react/view {:style styles/details-item-value-wrapper} [react/view {:style styles/details-item-value-wrapper}
[react/text (merge {:style styles/details-item-value} props) [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) [react/text (merge {:style styles/details-item-extra-value} extra-props)
(str extra-value)]]]))) (str extra-value)]]])))

View File

@ -2,7 +2,8 @@
(:require [clojure.string :as string] (:require [clojure.string :as string]
[status-im.js-dependencies :as dependencies] [status-im.js-dependencies :as dependencies]
[status-im.utils.ethereum.tokens :as tokens] [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 ;; 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 default-transaction-gas
;; TODO(jeluard) Rely on estimateGas call ;; TODO(jeluard) Rely on estimateGas call
(.times default-transaction-gas 5))) (.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))))))

View File

@ -15,7 +15,11 @@
=> 29166666 => 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])) (:refer-clojure :exclude [name symbol]))
(defn name [web3 contract cb] (defn name [web3 contract cb]
@ -58,3 +62,102 @@
(ethereum/call web3 (ethereum/call web3
(ethereum/call-params contract "allowance(address,address)" (ethereum/normalized-address owner-address) (ethereum/normalized-address spender-address)) (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)))