From e74bc81ce976bf1d43e37414cc0b9dfb83feff36 Mon Sep 17 00:00:00 2001 From: Goran Jovic Date: Tue, 24 Apr 2018 11:24:13 +0200 Subject: [PATCH] feature #3976 - getting token transfer events for erc20 tx history Signed-off-by: Andrey Shovkoplyas --- src/status_im/constants.cljs | 4 + src/status_im/ui/screens/wallet/events.cljs | 70 ++++++++---- .../ui/screens/wallet/transactions/subs.cljs | 4 +- .../ui/screens/wallet/transactions/views.cljs | 26 ++--- src/status_im/utils/ethereum/core.cljs | 31 ++++- src/status_im/utils/ethereum/erc20.cljs | 107 +++++++++++++++++- 6 files changed, 201 insertions(+), 41 deletions(-) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 84c7f75076..1ac3766c7c 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -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])+$") diff --git a/src/status_im/ui/screens/wallet/events.cljs b/src/status_im/ui/screens/wallet/events.cljs index 74992d4d4a..b9a6dd0bd3 100644 --- a/src/status_im/ui/screens/wallet/events.cljs +++ b/src/status_im/ui/screens/wallet/events.cljs @@ -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 diff --git a/src/status_im/ui/screens/wallet/transactions/subs.cljs b/src/status_im/ui/screens/wallet/transactions/subs.cljs index 7d576a528a..42e81299fd 100644 --- a/src/status_im/ui/screens/wallet/transactions/subs.cljs +++ b/src/status_im/ui/screens/wallet/transactions/subs.cljs @@ -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) diff --git a/src/status_im/ui/screens/wallet/transactions/views.cljs b/src/status_im/ui/screens/wallet/transactions/views.cljs index c4be792b4d..88dcd04986 100644 --- a/src/status_im/ui/screens/wallet/transactions/views.cljs +++ b/src/status_im/ui/screens/wallet/transactions/views.cljs @@ -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)]]]))) diff --git a/src/status_im/utils/ethereum/core.cljs b/src/status_im/utils/ethereum/core.cljs index 77a4017752..d28bad61d4 100644 --- a/src/status_im/utils/ethereum/core.cljs +++ b/src/status_im/utils/ethereum/core.cljs @@ -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)))))) diff --git a/src/status_im/utils/ethereum/erc20.cljs b/src/status_im/utils/ethereum/erc20.cljs index aef1499dd0..c6c2c75147 100644 --- a/src/status_im/utils/ethereum/erc20.cljs +++ b/src/status_im/utils/ethereum/erc20.cljs @@ -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)))) \ No newline at end of file + #(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)))