From f5c18ae7a9cf395fbdbcd83c425ca0c58f780b5e Mon Sep 17 00:00:00 2001 From: yenda Date: Thu, 9 May 2019 21:51:41 +0200 Subject: [PATCH 1/8] [feature] use subscriptions for tokens - removes fetching of last 100000 blocks of token transfers from the wallet pull loop - fetches the last 100000 blocks of token transfers at startup - replaces pulling by subscriptions to ethlogs for token transfers --- STATUS_GO_SHA256 | 2 +- STATUS_GO_VERSION | 2 +- src/status_im/accounts/login/core.cljs | 2 +- src/status_im/accounts/logout/core.cljs | 10 +- src/status_im/ethereum/subscriptions.cljs | 203 ++++++++++++++++-- .../transactions/core.cljs} | 186 +--------------- src/status_im/events.cljs | 30 +-- src/status_im/init/core.cljs | 25 +-- src/status_im/signals/core.cljs | 1 + src/status_im/subs.cljs | 2 +- src/status_im/ui/screens/wallet/events.cljs | 12 +- .../ui/screens/wallet/send/events.cljs | 9 +- test/cljs/status_im/test/sign_in/flow.cljs | 8 +- .../status_im/test/wallet/transactions.cljs | 23 +- 14 files changed, 249 insertions(+), 266 deletions(-) rename src/status_im/{models/transactions.cljs => ethereum/transactions/core.cljs} (54%) diff --git a/STATUS_GO_SHA256 b/STATUS_GO_SHA256 index 8f0d9840a85..9484c62978c 100644 --- a/STATUS_GO_SHA256 +++ b/STATUS_GO_SHA256 @@ -1,3 +1,3 @@ ## DO NOT EDIT THIS FILE BY HAND. USE `scripts/update-status-go.sh ` instead -0cj202bj2rwfrw327gibj8hj8i94ciyp3hkq2hck9l6711qlhpnb +0049i6znvl45hc651bqyzwgmzlv0fp40maggfjsrv13q5avd0g6d diff --git a/STATUS_GO_VERSION b/STATUS_GO_VERSION index 0f578940681..6162c3d599e 100644 --- a/STATUS_GO_VERSION +++ b/STATUS_GO_VERSION @@ -1,3 +1,3 @@ ## DO NOT EDIT THIS FILE BY HAND. USE `scripts/update-status-go.sh ` instead -v0.25.0-beta.0 +v0.25.0-beta.1 diff --git a/src/status_im/accounts/login/core.cljs b/src/status_im/accounts/login/core.cljs index 5e4252efd4d..e3b8ae5405d 100644 --- a/src/status_im/accounts/login/core.cljs +++ b/src/status_im/accounts/login/core.cljs @@ -4,9 +4,9 @@ [status-im.chaos-mode.core :as chaos-mode] [status-im.data-store.core :as data-store] [status-im.ethereum.subscriptions :as ethereum.subscriptions] + [status-im.ethereum.transactions.core :as transactions] [status-im.fleet.core :as fleet] [status-im.i18n :as i18n] - [status-im.models.transactions :as transactions] [status-im.models.wallet :as models.wallet] [status-im.native-module.core :as status] [status-im.node.core :as node] diff --git a/src/status_im/accounts/logout/core.cljs b/src/status_im/accounts/logout/core.cljs index f1c9c3b82ef..e891f311da7 100644 --- a/src/status_im/accounts/logout/core.cljs +++ b/src/status_im/accounts/logout/core.cljs @@ -1,12 +1,12 @@ (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.transport.core :as transport] - [status-im.utils.fx :as fx] - [status-im.models.transactions :as transactions] - [status-im.node.core :as node] [status-im.init.core :as init] - [status-im.chaos-mode.core :as chaos-mode])) + [status-im.node.core :as node] + [status-im.transport.core :as transport] + [status-im.utils.fx :as fx])) (fx/defn logout [{:keys [db] :as cofx}] diff --git a/src/status_im/ethereum/subscriptions.cljs b/src/status_im/ethereum/subscriptions.cljs index 6570bf6335c..6b670023884 100644 --- a/src/status_im/ethereum/subscriptions.cljs +++ b/src/status_im/ethereum/subscriptions.cljs @@ -1,32 +1,76 @@ (ns status-im.ethereum.subscriptions (:require [clojure.string :as string] [re-frame.core :as re-frame] + [status-im.constants :as constants] [status-im.ethereum.decode :as decode] [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.types :as types] [taoensso.timbre :as log])) -(defn get-block-by-hash [block-hash callback] +;; 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 - (.stringify js/JSON (clj->js {:jsonrpc "2.0" - :id 1 - :method "eth_getBlockByHash" - :params [block-hash false]})) + (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 - :number 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])] (handler data) (log/warn ::unknown-subscription :event event))) +(fx/defn handle-error + [cofx {:keys [subscription_id data] :as event}] + (log/error ::error event)) + (fx/defn register-subscription [{:keys [db]} id handler] {:db (assoc-in db [:ethereum/subscriptions id] handler)}) @@ -38,10 +82,10 @@ (defn subscribe-signal [filter params callback] (status/call-private-rpc - (.stringify js/JSON (clj->js {:jsonrpc "2.0" - :id 1 - :method "eth_subscribeSignal" - :params [filter, params]})) + (types/clj->json {:jsonrpc "2.0" + :id 1 + :method "eth_subscribeSignal" + :params [filter params]}) (fn [response] (if (string/blank? response) (log/error ::subscription-unknown-error :filter filter :params params) @@ -54,6 +98,100 @@ 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))) + (defn new-block-filter [] (subscribe-signal @@ -61,15 +199,50 @@ (fn [[block-hash]] (get-block-by-hash block-hash - (fn [block-number] - (when block-number + (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/reg-fx - :ethereum.subscriptions/new-block-filter + :ethereum.subscriptions/new-block new-block-filter) (fx/defn initialize - [cofx] - {:ethereum.subscriptions/new-block-filter nil}) + [{: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))) + padded-address (add-padding (ethereum/normalized-address (:address account)))] + {:ethereum.subscriptions/new-block nil + :ethereum.subscriptions/token-transactions {:chain-tokens chain-tokens + :address padded-address}})) diff --git a/src/status_im/models/transactions.cljs b/src/status_im/ethereum/transactions/core.cljs similarity index 54% rename from src/status_im/models/transactions.cljs rename to src/status_im/ethereum/transactions/core.cljs index 1b46207a42a..da3b1134220 100644 --- a/src/status_im/models/transactions.cljs +++ b/src/status_im/ethereum/transactions/core.cljs @@ -1,166 +1,19 @@ -(ns status-im.models.transactions +(ns status-im.ethereum.transactions.core (:require [clojure.set :as set] - [cljs.core.async :as async] [clojure.string :as string] + [re-frame.core :as re-frame] + re-frame.db [status-im.utils.async :as async-util] [status-im.utils.ethereum.core :as ethereum] - [status-im.constants :as constants] - [status-im.native-module.core :as status] [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] - [status-im.utils.fx :as fx] - [re-frame.core :as re-frame] - [re-frame.db] - [status-im.utils.config :as config]) - (:require-macros - [cljs.core.async.macros :refer [go-loop go]])) + [taoensso.timbre :as log])) (def sync-interval-ms 15000) (def sync-timeout-ms 20000) (def confirmations-count-threshold 12) -(def block-query-limit 100000) - -;; ---------------------------------------------------------------------------- -;; token transfer event logs from eth-node -;; ---------------------------------------------------------------------------- - -(defn- parse-json [s] - {:pre [(string? 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] - {:pre [(string? address)]} - (str "0x000000000000000000000000" (subs address 2))) - -(defn- remove-padding [topic] - {:pre [(string? topic)]} - (str "0x" (subs topic 26))) - -(defn- parse-transaction-entries [current-block-number block-info chain-tokens direction transfers] - {:pre [(integer? current-block-number) (map? block-info) - (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 (-> block-info :number str) - :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 - - :confirmations (str (- current-block-number (-> transfer :blockNumber ethereum/hex->int))) - - :gas-price nil - :nonce nil - :data nil - - :gas-limit nil - :timestamp (-> block-info :timestamp (* 1000) str) - - :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}])))))) - -(defn- add-block-info [web3 current-block-number chain-tokens direction result success-fn] - {:pre [web3 (integer? current-block-number) (map? chain-tokens) (keyword? direction) - (every? map? result) - (fn? success-fn)]} - (let [transfers-by-block (group-by :blockNumber result)] - (doseq [[block-number transfers] transfers-by-block] - (ethereum/get-block-info web3 (ethereum/hex->int block-number) - (fn [block-info] - (if-not (map? block-info) - (log/error "Request for block info failed") - (success-fn (parse-transaction-entries current-block-number - block-info - chain-tokens - direction - transfers)))))))) - -(defn- response-handler [web3 current-block-number chain-tokens direction error-fn success-fn] - (fn handle-response - ([response] - #_(log/debug "Token transaction logs recieved --" (pr-str response)) - (let [{:keys [error result]} (parse-json response)] - (handle-response error result))) - ([error result] - (if error - (error-fn error) - (add-block-info web3 current-block-number chain-tokens direction result success-fn))))) - -(defn- limited-from-block [current-block-number latest-block-checked] - {:pre [(integer? current-block-number)] - ;; needs to be a positive etherium hex - :post [(string? %) (string/starts-with? % "0x")]} - (if latest-block-checked - (-> latest-block-checked (- confirmations-count-threshold) ethereum/int->hex) - (-> current-block-number (- block-query-limit) (max 0) ethereum/int->hex))) - -;; 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 -;; - -(defonce latest-block-checked (atom nil)) - -(defn- get-token-transfer-logs - ;; NOTE(goranjovic): here we use direct JSON-RPC calls to get event logs because of web3 event issues with infura - ;; we still use web3 to get other data, such as block info - [web3 current-block-number from-block chain-tokens direction address cb] - {:pre [web3 (integer? current-block-number) (map? chain-tokens) (keyword? direction) (string? address) (fn? cb)]} - (let [[from to] (if (= :inbound direction) - [nil (add-padding (ethereum/normalized-address address))] - [(add-padding (ethereum/normalized-address address)) nil]) - args {:jsonrpc "2.0" - :id 2 - :method constants/web3-get-logs - :params [{:address (keys chain-tokens) - :fromBlock from-block - :topics [constants/event-transfer-hash from to]}]} - payload (.stringify js/JSON (clj->js args))] - (status/call-private-rpc payload - (response-handler web3 current-block-number chain-tokens direction ethereum/handle-error cb)))) - -(defn- get-token-transactions - [web3 chain-tokens address cb] - {:pre [web3 (map? chain-tokens) (string? address) (fn? cb)]} - (ethereum/get-block-number web3 - (fn [current-block-number] - (let [from-block (limited-from-block current-block-number @latest-block-checked)] - (reset! latest-block-checked current-block-number) - (get-token-transfer-logs web3 current-block-number from-block chain-tokens :inbound address cb) - (get-token-transfer-logs web3 current-block-number from-block chain-tokens :outbound address cb))))) ;; -------------------------------------------------------------------------- ;; etherscan transactions @@ -199,8 +52,7 @@ (defn- format-transaction [account {:keys [value timeStamp blockNumber hash from to - gas gasPrice gasUsed nonce confirmations - input isError]}] + gas gasPrice gasUsed nonce input isError]}] (let [inbound? (= (str "0x" account) to) error? (= "1" isError)] {:value value @@ -218,7 +70,6 @@ :gas-price gasPrice :gas-used gasUsed :nonce nonce - :confirmations confirmations :data input})) (defn- format-transactions-response [response account] @@ -249,11 +100,7 @@ account-address success-fn error-fn - chaos-mode?) - (get-token-transactions web3 - chain-tokens - account-address - success-fn)) + chaos-mode?)) ;; ----------------------------------------------------------------------------- ;; Helpers functions that help determine if a background sync should execute @@ -302,21 +149,8 @@ (map :tx-hash) set)))) -;; Seq[transaction] -> truthy -(defn- have-unconfirmed-transactions? - "Detects if some of the transactions have less than 12 confirmations" - [transactions] - {:pre [(every? string? (map :confirmations transactions))]} - (->> transactions - (map :confirmations) - (map int) - (some #(< % confirmations-count-threshold)))) - (letfn [(combine-entries [transaction token-transfer] (merge transaction (select-keys token-transfer [:symbol :from :to :value :type :token :transfer]))) - (update-confirmations [tx1 tx2] - (assoc tx1 :confirmations (str (max (int (:confirmations tx1)) - (int (:confirmations tx2)))))) (tx-and-transfer? [tx1 tx2] (and (not (:transfer tx1)) (:transfer tx2))) (both-transfer? @@ -325,7 +159,6 @@ (defn- dedupe-transactions [tx1 tx2] (cond (tx-and-transfer? tx1 tx2) (combine-entries tx1 tx2) (tx-and-transfer? tx2 tx1) (combine-entries tx2 tx1) - (both-transfer? tx1 tx2) (update-confirmations tx1 tx2) :else tx2))) ;; ---------------------------------------------------------------------------- @@ -382,14 +215,11 @@ transaction-map (:transactions wallet) transaction-ids (set (keys transaction-map)) chaos-mode? (get-in account [:settings :chaos-mode?])] - (if-not (or @latest-block-checked - (have-unconfirmed-transactions? (vals transaction-map)) - (not-empty (set/difference chat-transaction-ids transaction-ids))) + (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}] - (reset! latest-block-checked nil) (let [account-address (:address account)] (when @polling-executor (async-util/async-periodic-stop! @polling-executor)) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 88d3304af7d..ac4229d65f6 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -2054,17 +2054,6 @@ (fn [cofx _] (bottom-sheet/hide-bottom-sheet cofx))) -;; ethereum subscriptions events -(handlers/register-handler-fx - :ethereum.callback/subscription-success - (fn [cofx [_ id handler]] - (ethereum.subscriptions/register-subscription cofx id handler))) - -(handlers/register-handler-fx - :ethereum.signal/new-block - (fn [cofx [_ block-number]] - (ethereum.subscriptions/new-block cofx block-number))) - ;;custom tokens (handlers/register-handler-fx @@ -2120,4 +2109,21 @@ (fx/merge cofx (custom-tokens/remove-custom-token token) (when navigate-back? - (navigation/navigate-back))))) \ No newline at end of file + (navigation/navigate-back))))) + +;; ethereum subscriptions events + +(handlers/register-handler-fx + :ethereum.callback/subscription-success + (fn [cofx [_ id handler]] + (ethereum.subscriptions/register-subscription cofx id handler))) + +(handlers/register-handler-fx + :ethereum.signal/new-block + (fn [cofx [_ block-number]] + (ethereum.subscriptions/new-block cofx block-number))) + +(handlers/register-handler-fx + :ethereum.signal/new-transactions + (fn [cofx [_ transactions]] + (ethereum.subscriptions/new-transactions cofx transactions))) diff --git a/src/status_im/init/core.cljs b/src/status_im/init/core.cljs index 9d99fd91d30..b717262dc7f 100644 --- a/src/status_im/init/core.cljs +++ b/src/status_im/init/core.cljs @@ -1,37 +1,28 @@ (ns status-im.init.core (:require [re-frame.core :as re-frame] - [status-im.chat.models.loading :as chat-loading] - [status-im.accounts.core :as accounts.core] [status-im.accounts.login.core :as accounts.login] [status-im.accounts.update.core :as accounts.update] - [status-im.constants :as constants] - [status-im.react-native.js-dependencies :as rn-dependencies] + [status-im.browser.core :as browser] + [status-im.chat.models :as chat-model] + [status-im.contact.core :as contact] [status-im.data-store.core :as data-store] [status-im.data-store.realm.core :as realm] [status-im.extensions.registry :as extensions.registry] [status-im.i18n :as i18n] - [status-im.browser.core :as browser] - [status-im.contact.core :as contact] [status-im.models.dev-server :as models.dev-server] - [status-im.protocol.core :as protocol] - [status-im.pairing.core :as pairing] - [status-im.models.transactions :as transactions] - [status-im.models.wallet :as models.wallet] [status-im.native-module.core :as status] [status-im.node.core :as node] [status-im.notifications.core :as notifications] + [status-im.pairing.core :as pairing] + [status-im.react-native.js-dependencies :as rn-dependencies] + [status-im.stickers.core :as stickers] [status-im.ui.screens.db :refer [app-db]] [status-im.ui.screens.navigation :as navigation] - [status-im.utils.config :as config] [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.fx :as fx] [status-im.utils.keychain.core :as keychain] [status-im.utils.platform :as platform] - [status-im.utils.utils :as utils] - [taoensso.timbre :as log] - [status-im.utils.fx :as fx] - [status-im.chat.models :as chat-model] - [status-im.accounts.db :as accounts.db] - [status-im.stickers.core :as stickers])) + [taoensso.timbre :as log])) (defn init-store! "Try to decrypt the database, move on if successful otherwise go back to diff --git a/src/status_im/signals/core.cljs b/src/status_im/signals/core.cljs index 9a148622258..548365872e7 100644 --- a/src/status_im/signals/core.cljs +++ b/src/status_im/signals/core.cljs @@ -74,4 +74,5 @@ "messages.decrypt.failed" (contact-recovery/handle-contact-recovery-fx cofx (:sender event)) "discovery.summary" (summary cofx event) "subscriptions.data" (ethereum.subscriptions/handle-signal cofx event) + "subscriptions.error" (ethereum.subscriptions/handle-error cofx event) (log/debug "Event " type " not handled")))) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 3d095acbb0b..48774cc556e 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -10,9 +10,9 @@ [status-im.chat.db :as chat.db] [status-im.constants :as constants] [status-im.contact.db :as contact.db] + [status-im.ethereum.transactions.core :as transactions] [status-im.fleet.core :as fleet] [status-im.i18n :as i18n] - [status-im.models.transactions :as transactions] [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] diff --git a/src/status_im/ui/screens/wallet/events.cljs b/src/status_im/ui/screens/wallet/events.cljs index 3fd74858b37..fd221da199d 100644 --- a/src/status_im/ui/screens/wallet/events.cljs +++ b/src/status_im/ui/screens/wallet/events.cljs @@ -1,19 +1,19 @@ (ns status-im.ui.screens.wallet.events (:require [re-frame.core :as re-frame] - [status-im.models.transactions :as wallet.transactions] + [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] status-im.ui.screens.wallet.navigation [status-im.utils.ethereum.core :as ethereum] [status-im.utils.ethereum.erc20 :as erc20] [status-im.utils.ethereum.tokens :as tokens] + [status-im.utils.fx :as fx] [status-im.utils.handlers :as handlers] [status-im.utils.money :as money] [status-im.utils.prices :as prices] - [taoensso.timbre :as log] - [status-im.utils.fx :as fx] - [status-im.i18n :as i18n] - [status-im.utils.utils :as utils.utils])) + [status-im.utils.utils :as utils.utils] + [taoensso.timbre :as log])) (defn get-balance [{:keys [web3 account-id on-success on-error]}] (if (and web3 account-id) @@ -146,7 +146,7 @@ (handlers/register-handler-fx :update-transactions (fn [{:keys [db]} _] - {::wallet.transactions/sync-transactions-now + {::transactions/sync-transactions-now (select-keys db [:network-status :account/account :wallet/all-tokens :app-state :network :web3])})) diff --git a/src/status_im/ui/screens/wallet/send/events.cljs b/src/status_im/ui/screens/wallet/send/events.cljs index f8bf32cfeee..82e901a89a8 100644 --- a/src/status_im/ui/screens/wallet/send/events.cljs +++ b/src/status_im/ui/screens/wallet/send/events.cljs @@ -1,13 +1,11 @@ (ns status-im.ui.screens.wallet.send.events (:require [re-frame.core :as re-frame] [status-im.chat.commands.sending :as commands-sending] - [status-im.chat.models.message :as models.message] - [status-im.chat.models :as chat.models] [status-im.constants :as constants] [status-im.i18n :as i18n] - [status-im.models.transactions :as wallet.transactions] [status-im.models.wallet :as models.wallet] [status-im.native-module.core :as status] + [status-im.transport.utils :as transport.utils] [status-im.ui.screens.navigation :as navigation] [status-im.ui.screens.wallet.db :as wallet.db] [status-im.utils.ethereum.core :as ethereum] @@ -18,10 +16,7 @@ [status-im.utils.money :as money] [status-im.utils.security :as security] [status-im.utils.types :as types] - [status-im.utils.utils :as utils] - [status-im.utils.config :as config] - [status-im.transport.utils :as transport.utils] - [status-im.hardwallet.core :as hardwallet])) + [status-im.utils.utils :as utils])) ;;;; FX diff --git a/test/cljs/status_im/test/sign_in/flow.cljs b/test/cljs/status_im/test/sign_in/flow.cljs index 065511c9456..29eede285e0 100644 --- a/test/cljs/status_im/test/sign_in/flow.cljs +++ b/test/cljs/status_im/test/sign_in/flow.cljs @@ -2,11 +2,11 @@ "The main purpose of these tests is to signal that some steps of the sign in flow has been changed. Such changes should be reflected in both these tests and documents which describe the whole \"sign in\" flow." - (:require [cljs.test :refer-macros [deftest is are testing]] + (:require [cljs.test :refer-macros [deftest is testing]] [status-im.accounts.login.core :as login.core] [status-im.events :as events] - [status-im.test.sign-in.data :as data] - [status-im.signals.core :as signals])) + [status-im.signals.core :as signals] + [status-im.test.sign-in.data :as data])) (deftest on-password-input-submitted (testing @@ -205,7 +205,7 @@ (is (contains? efx :web3/get-syncing)) (is (contains? efx :get-tokens-balance)) (is (contains? efx :get-prices)) - (is (contains? efx :status-im.models.transactions/start-sync-transactions)))))) + (is (contains? efx :status-im.ethereum.transactions.core/start-sync-transactions)))))) (deftest login-failed (testing diff --git a/test/cljs/status_im/test/wallet/transactions.cljs b/test/cljs/status_im/test/wallet/transactions.cljs index 789b788b327..f64ef4e5710 100644 --- a/test/cljs/status_im/test/wallet/transactions.cljs +++ b/test/cljs/status_im/test/wallet/transactions.cljs @@ -1,21 +1,8 @@ (ns status-im.test.wallet.transactions - (:require [cljs.test :refer-macros [deftest is testing async run-tests]] - [cljs.core.async.impl.protocols :refer [closed?]] - [status-im.utils.datetime :as time] - [status-im.utils.http :as http] - [status-im.models.transactions :as transactions] - [goog.Uri :as goog-uri])) - -(deftest have-unconfirmed-transactions - (is (transactions/have-unconfirmed-transactions? - [{:confirmations "0"}])) - (is (transactions/have-unconfirmed-transactions? - [{:confirmations "11"}])) - (is (transactions/have-unconfirmed-transactions? - [{:confirmations "200"} - {:confirmations "0"}])) - (is (not (transactions/have-unconfirmed-transactions? - [{:confirmations "12"}])))) + (:require [cljs.test :refer-macros [deftest is]] + [goog.Uri :as goog-uri] + [status-im.ethereum.transactions.core :as transactions] + [status-im.utils.http :as http])) (deftest chat-map->transaction-ids (is (= #{} (transactions/chat-map->transaction-ids "testnet_rpc" {}))) @@ -222,7 +209,7 @@ (deftest etherscan-transactions (let [ky-set #{:block :hash :symbol :gas-price :value :gas-limit :type - :confirmations :gas-used :from :timestamp :nonce :to :data}] + :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)] From b274ed9fa9bfa314195d6fba9d3d39dca86f6730 Mon Sep 17 00:00:00 2001 From: yenda Date: Mon, 13 May 2019 13:44:46 +0200 Subject: [PATCH 2/8] [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 --- src/status_im/accounts/login/core.cljs | 4 +- src/status_im/accounts/logout/core.cljs | 2 - src/status_im/ethereum/subscriptions.cljs | 223 ++------- src/status_im/ethereum/transactions/core.cljs | 430 ++++++++---------- .../ethereum/transactions/etherscan.cljs | 192 ++++++++ src/status_im/events.cljs | 25 +- src/status_im/subs.cljs | 6 +- src/status_im/ui/screens/wallet/events.cljs | 8 - .../ui/screens/wallet/navigation.cljs | 6 +- .../ui/screens/wallet/transactions/views.cljs | 14 +- src/status_im/utils/http.cljs | 8 +- test/cljs/status_im/test/sign_in/flow.cljs | 3 +- .../status_im/test/wallet/transactions.cljs | 180 +------- 13 files changed, 485 insertions(+), 616 deletions(-) create mode 100644 src/status_im/ethereum/transactions/etherscan.cljs diff --git a/src/status_im/accounts/login/core.cljs b/src/status_im/accounts/login/core.cljs index e3b8ae5405d..6a62cb1cb33 100644 --- a/src/status_im/accounts/login/core.cljs +++ b/src/status_im/accounts/login/core.cljs @@ -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)] diff --git a/src/status_im/accounts/logout/core.cljs b/src/status_im/accounts/logout/core.cljs index e891f311da7..c4e8a815e7f 100644 --- a/src/status_im/accounts/logout/core.cljs +++ b/src/status_im/accounts/logout/core.cljs @@ -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))) diff --git a/src/status_im/ethereum/subscriptions.cljs b/src/status_im/ethereum/subscriptions.cljs index 6b670023884..842f8671702 100644 --- a/src/status_im/ethereum/subscriptions.cljs +++ b/src/status_im/ethereum/subscriptions.cljs @@ -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 - :address padded-address}})) + :ethereum.subscriptions/token-transactions + {:chain-tokens chain-tokens + :address padded-address}})) diff --git a/src/status_im/ethereum/transactions/core.cljs b/src/status_im/ethereum/transactions/core.cljs index da3b1134220..99f6047f966 100644 --- a/src/status_im/ethereum/transactions/core.cljs +++ b/src/status_im/ethereum/transactions/core.cljs @@ -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 -;; -------------------------------------------------------------------------- - -(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 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"))))) - -(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") - :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-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)))) - -(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- 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?)) - -;; ----------------------------------------------------------------------------- -;; Helpers functions that help determine if a background sync should execute -;; ----------------------------------------------------------------------------- - -(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 - [: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?})) - -(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?)))))) - -;; 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)) +(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))))))) + +(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)))))) + +(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)))))) + +(defn add-padding [address] + {:pre [(string? address)]} + (str "0x000000000000000000000000" (subs address 2))) + +(defn- remove-padding [topic] + {:pre [(string? topic)]} + (str "0x" (subs topic 26))) + +(def default-erc20-token + {:symbol :ERC20 + :decimals 18 + :name "ERC20"}) + +(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 + :from from + :to to + :type direction + :value (str (decode/uint value))}))]))))) (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}) + :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] + (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))))]))))))))))))) + +;; ----------------------------------------------- +;; transactions api +;; ----------------------------------------------- + +(fx/defn new + [{:keys [db]} {:keys [hash] :as transaction}] + {:db (assoc-in db [:wallet :transactions hash] transaction)}) + +(fx/defn handle-history + [{:keys [db]} transactions] + {:db (update-in db + [:wallet :transactions] + #(merge transactions %))}) + +(fx/defn handle-token-history + [{:keys [db]} transactions] + {:db (update-in db + [:wallet :transactions] + merge transactions)}) + +(fx/defn initialize + [cofx] + (transactions.etherscan/fetch-history cofx)) diff --git a/src/status_im/ethereum/transactions/etherscan.cljs b/src/status_im/ethereum/transactions/etherscan.cljs new file mode 100644 index 00000000000..8076e93183a --- /dev/null +++ b/src/status_im/ethereum/transactions/etherscan.cljs @@ -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?}})) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index ac4229d65f6..acd9bca2dbc 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -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.signal/new-transactions + :ethereum.transactions.callback/fetch-history-success (fn [cofx [_ transactions]] - (ethereum.subscriptions/new-transactions cofx transactions))) + (ethereum.transactions/handle-history cofx transactions))) + +(handlers/register-handler-fx + :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.transactions/handle-token-history cofx transactions))) + +(handlers/register-handler-fx + :ethereum.transactions/new + (fn [cofx [_ transaction]] + (ethereum.transactions/new cofx transaction))) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 48774cc556e..32fea9c10b8 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -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 diff --git a/src/status_im/ui/screens/wallet/events.cljs b/src/status_im/ui/screens/wallet/events.cljs index fd221da199d..f94fc8d7a1d 100644 --- a/src/status_im/ui/screens/wallet/events.cljs +++ b/src/status_im/ui/screens/wallet/events.cljs @@ -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]] diff --git a/src/status_im/ui/screens/wallet/navigation.cljs b/src/status_im/ui/screens/wallet/navigation.cljs index 90d25270335..639f4ecf695 100644 --- a/src/status_im/ui/screens/wallet/navigation.cljs +++ b/src/status_im/ui/screens/wallet/navigation.cljs @@ -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) diff --git a/src/status_im/ui/screens/wallet/transactions/views.cljs b/src/status_im/ui/screens/wallet/transactions/views.cljs index c668a93c309..f5d86f9e119 100644 --- a/src/status_im/ui/screens/wallet/transactions/views.cljs +++ b/src/status_im/ui/screens/wallet/transactions/views.cljs @@ -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]]])) - diff --git a/src/status_im/utils/http.cljs b/src/status_im/utils/http.cljs index 8d194ef3f03..bb19e3752e6 100644 --- a/src/status_im/utils/http.cljs +++ b/src/status_im/utils/http.cljs @@ -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)) diff --git a/test/cljs/status_im/test/sign_in/flow.cljs b/test/cljs/status_im/test/sign_in/flow.cljs index 29eede285e0..7741d4f0871 100644 --- a/test/cljs/status_im/test/sign_in/flow.cljs +++ b/test/cljs/status_im/test/sign_in/flow.cljs @@ -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 diff --git a/test/cljs/status_im/test/wallet/transactions.cljs b/test/cljs/status_im/test/wallet/transactions.cljs index f64ef4e5710..37941cc94b3 100644 --- a/test/cljs/status_im/test/wallet/transactions.cljs +++ b/test/cljs/status_im/test/wallet/transactions.cljs @@ -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!\"}") From f1b8ba87644943152f722741d298bccf1964112f Mon Sep 17 00:00:00 2001 From: yenda Date: Thu, 16 May 2019 23:50:03 +0200 Subject: [PATCH 3/8] [refactor] transaction details --- src/status_im/subs.cljs | 146 ++++++++++-------- .../ui/components/bottom_bar/core.cljs | 1 - src/status_im/ui/screens/db.cljs | 2 - .../ui/screens/wallet/navigation.cljs | 4 +- .../screens/wallet/transactions/events.cljs | 14 +- .../ui/screens/wallet/transactions/views.cljs | 88 +++++------ 6 files changed, 144 insertions(+), 111 deletions(-) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 32fea9c10b8..08db7a50765 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -281,6 +281,18 @@ (-> selected-participants (contains? element)))) +(re-frame/reg-sub + :ethereum/chain-keyword + :<- [:network] + (fn [network] + (ethereum/network->chain-keyword network))) + +(re-frame/reg-sub + :ethereum/native-currency + :<- [:ethereum/chain-keyword] + (fn [chain-keyword] + (tokens/native-currency chain-keyword))) + ;;ACCOUNT ============================================================================================================== (re-frame/reg-sub @@ -905,23 +917,11 @@ (fn [prices [_ fsym tsym]] (get-in prices [fsym tsym :last-day]))) -(re-frame/reg-sub - :wallet-transactions - :<- [:wallet] - (fn [wallet] - (get wallet :transactions))) - (re-frame/reg-sub :wallet.settings/currency :<- [:account-settings] - (fn [sett] - (or (get-in sett [:wallet :currency]) :usd))) - -(re-frame/reg-sub - :wallet.transactions/filters - :<- [:wallet.transactions] - (fn [txs] - (get txs :filters))) + (fn [settings] + (or (get-in settings [:wallet :currency]) :usd))) (re-frame/reg-sub :asset-value @@ -937,7 +937,8 @@ str (i18n/format-currency (:code currency)))))) -(defn- get-balance-total-value [balance prices currency token->decimals] +(defn- get-balance-total-value + [balance prices currency token->decimals] (reduce-kv (fn [acc symbol value] (if-let [price (get-in prices [symbol currency :price])] (+ acc (or (some-> (money/internal->formatted value symbol (token->decimals symbol)) @@ -984,11 +985,6 @@ (or (get-in wallet [:errors :balance-update]) (get-in wallet [:errors :prices-update])))) -(re-frame/reg-sub - :get-wallet-unread-messages-number - (fn [db] - 0)) - (re-frame/reg-sub :wallet/visible-tokens-symbols :<- [:network] @@ -1030,12 +1026,19 @@ ;;WALLET TRANSACTIONS ================================================================================================== (re-frame/reg-sub - :wallet.transactions/current-tab + :wallet-transactions :<- [:wallet] (fn [wallet] - (get wallet :current-tab 0))) + (get wallet :transactions))) -(defn enrich-transaction [{:keys [type to from timestamp] :as transaction} contacts] +(re-frame/reg-sub + :wallet.transactions/filters + :<- [:wallet.transactions] + (fn [txs] + (get txs :filters))) + +(defn enrich-transaction + [{:keys [type to from timestamp] :as transaction} contacts] (let [[contact-address key-contact key-wallet] (if (= type :inbound) [from :from-contact :to-wallet] [to :to-contact :from-wallet]) @@ -1112,48 +1115,69 @@ (:current-transaction wallet))) (re-frame/reg-sub - :wallet.transactions/transaction-details + :wallet.transactions.details/current-transaction :<- [:wallet.transactions/transactions] :<- [:wallet.transactions/current-transaction] - :<- [:network] - (fn [[transactions current-transaction network]] - (let [{:keys [gas-used gas-price hash timestamp type] :as transaction} (get transactions current-transaction) - chain (ethereum/network->chain-keyword network) - native-currency (tokens/native-currency chain) - display-unit (wallet.utils/display-symbol native-currency)] + :<- [:ethereum/native-currency] + :<- [:ethereum/chain-keyword] + (fn [[transactions current-transaction native-currency chain-keyword]] + (let [{:keys [gas-used gas-price hash timestamp type token value] + :as transaction} + (get transactions current-transaction)] (when transaction - (merge transaction - {:gas-price-eth (if gas-price (money/wei->str :eth gas-price display-unit) "-") - :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) - :cost (i18n/label :not-applicable) - :gas-limit (i18n/label :not-applicable) - :gas-used (i18n/label :not-applicable) - :nonce (i18n/label :not-applicable) - :hash (i18n/label :not-applicable)} - {:cost (when gas-used - (money/wei->str :eth (money/fee-value gas-used gas-price) display-unit)) - :url (transactions.etherscan/get-transaction-details-url chain hash)})))))) - -(re-frame/reg-sub - :wallet.transactions.details/confirmations + (let [{:keys [symbol-display symbol decimals] :as asset} + (or token native-currency) + amount-text (if value + (wallet.utils/format-amount value decimals) + "...") + currency-text (when asset + (clojure.core/name (or symbol-display symbol))) + native-currency-text (-> native-currency + :symbol-display + name)] + (merge transaction + {:amount-text amount-text + :currency-text currency-text + :gas-price-eth (if gas-price + (money/wei->str :eth + gas-price + native-currency-text) + "-") + :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) + :cost (i18n/label :not-applicable) + :gas-limit (i18n/label :not-applicable) + :gas-used (i18n/label :not-applicable) + :nonce (i18n/label :not-applicable) + :hash (i18n/label :not-applicable)} + {:cost (when gas-used + (money/wei->str :eth + (money/fee-value gas-used gas-price) + native-currency-text)) + :url (transactions.etherscan/get-transaction-details-url + chain-keyword + hash)}))))))) + +(re-frame/reg-sub + :wallet.transactions.details/screen + :<- [:wallet.transactions.details/current-transaction] :<- [:ethereum/current-block] - :<- [:wallet.transactions/transaction-details] - (fn [[current-block {:keys [block]}]] - (if (and current-block block) - (inc (- current-block block)) - 0))) - -(re-frame/reg-sub - :wallet.transactions.details/confirmations-progress - :<- [:wallet.transactions.details/confirmations] - (fn [confirmations] - (let [max-confirmations 10] - (if (>= confirmations max-confirmations) - 100 - (* 100 (/ confirmations max-confirmations)))))) + (fn [[{:keys [block] :as transaction} current-block]] + :wallet.transactions.details/current-transaction + (let [confirmations (if (and current-block block) + (inc (- current-block block)) + 0)] + (assoc transaction + :confirmations confirmations + :confirmations-progress + (if (>= confirmations transactions/confirmations-count-threshold) + 100 + (* 100 (/ confirmations transactions/confirmations-count-threshold))))))) ;;WALLET SEND ========================================================================================================== diff --git a/src/status_im/ui/components/bottom_bar/core.cljs b/src/status_im/ui/components/bottom_bar/core.cljs index 2c2e4f0b635..8db2c0a87ea 100644 --- a/src/status_im/ui/components/bottom_bar/core.cljs +++ b/src/status_im/ui/components/bottom_bar/core.cljs @@ -43,7 +43,6 @@ {:nav-stack :wallet-stack :content {:title (i18n/label :t/wallet) :icon :main-icons/wallet} - :count-subscription :get-wallet-unread-messages-number :accessibility-label :wallet-tab-button}) {:nav-stack :profile-stack :content {:title (i18n/label :t/profile) diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index 13406e088ca..2701fa47b13 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -33,7 +33,6 @@ :sync-state :done :app-state "active" :wallet.transactions constants/default-wallet-transactions - :wallet-selected-asset {} :wallet/all-tokens {} :prices {} :peers-count 0 @@ -331,7 +330,6 @@ :chat/access-scope->command-id :wallet/wallet :wallet/wallet.transactions - :wallet/wallet-selected-asset :prices/prices :prices/prices-loading? :notifications/notifications diff --git a/src/status_im/ui/screens/wallet/navigation.cljs b/src/status_im/ui/screens/wallet/navigation.cljs index 639f4ecf695..2f03b1b42d3 100644 --- a/src/status_im/ui/screens/wallet/navigation.cljs +++ b/src/status_im/ui/screens/wallet/navigation.cljs @@ -14,7 +14,7 @@ (re-frame/dispatch [:wallet.ui/pull-to-refresh]) (re-frame/dispatch [:update-wallet])) 500) - (assoc-in db [:wallet :current-tab] 0)) + db) (defmethod navigation/preload-data! :wallet-stack [db _] @@ -24,7 +24,7 @@ (re-frame/dispatch [:wallet.ui/pull-to-refresh]) (re-frame/dispatch [:update-wallet])) 500) - (assoc-in db [:wallet :current-tab] 0)) + db) (def transaction-send-default (let [symbol :ETH] diff --git a/src/status_im/ui/screens/wallet/transactions/events.cljs b/src/status_im/ui/screens/wallet/transactions/events.cljs index e35cca3593c..148da85e860 100644 --- a/src/status_im/ui/screens/wallet/transactions/events.cljs +++ b/src/status_im/ui/screens/wallet/transactions/events.cljs @@ -2,10 +2,20 @@ (:require [status-im.utils.handlers :as handlers])) (defn- mark-all-checked [filters] - (update filters :type #(map (fn [m] (assoc m :checked? true)) %))) + (update filters + :type + #(map (fn [m] + (assoc m :checked? true)) + %))) (defn- mark-checked [filters {:keys [type]} checked?] - (update filters :type #(map (fn [{:keys [id] :as m}] (if (= type id) (assoc m :checked? checked?) m)) %))) + (update filters + :type + #(map (fn [{:keys [id] :as m}] + (if (= type id) + (assoc m :checked? checked?) + m)) + %))) (defn- update-filters [db f] (update-in db [:wallet.transactions :filters] f)) diff --git a/src/status_im/ui/screens/wallet/transactions/views.cljs b/src/status_im/ui/screens/wallet/transactions/views.cljs index f5d86f9e119..e2775b1698f 100644 --- a/src/status_im/ui/screens/wallet/transactions/views.cljs +++ b/src/status_im/ui/screens/wallet/transactions/views.cljs @@ -49,14 +49,18 @@ (:postponed :pending) (transaction-icon :main-icons/arrow-right colors/gray-light colors/gray) (throw (str "Unknown transaction type: " k)))) -(defn render-transaction [{:keys [hash from-contact to-contact to from type value time-formatted symbol]} - network all-tokens hide-details?] - (let [[label contact address - contact-accessibility-label - address-accessibility-label] (if (inbound? type) - [(i18n/label :t/from) from-contact from :sender-text :sender-address-text] - [(i18n/label :t/to) to-contact to :recipient-name-text :recipient-address-text]) - {:keys [decimals] :as token} (tokens/asset-for all-tokens (ethereum/network->chain-keyword network) symbol)] +(defn render-transaction + [{:keys [hash from-contact to-contact to from type value time-formatted symbol]} + network all-tokens hide-details?] + (let [[label contact address contact-accessibility-label + address-accessibility-label] + (if (inbound? type) + [(i18n/label :t/from) from-contact from :sender-text :sender-address-text] + [(i18n/label :t/to) to-contact to :recipient-name-text :recipient-address-text]) + {:keys [decimals] :as token} + (tokens/asset-for all-tokens + (ethereum/network->chain-keyword network) + symbol)] [list/touchable-item #(when-not hide-details? (re-frame/dispatch [:show-transaction-details hash])) [react/view {:accessibility-label :transaction-item} [list/item @@ -96,7 +100,10 @@ (:checked? (some #(when (= (:type transaction) (:id %)) %) (:type filter-data)))) (defn update-transactions [m filter-data] - (update m :data (fn [v] (filter #(filtered-transaction? % filter-data) v)))) + (update m + :data + (fn [v] + (filter #(filtered-transaction? % filter-data) v)))) (defview history-list [& [hide-details?]] (letsubs [transactions-history-list [:wallet.transactions/transactions-history-list] @@ -157,34 +164,26 @@ [toolbar-view filter-data] [history-list]])) -(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 [network all-tokens {:keys [value date type symbol token]}] - (let [asset (tokens/asset-for all-tokens (ethereum/network->chain-keyword network) symbol)] - [react/view {:style styles/details-header} - [react/view {:style styles/details-header-icon} - (when type - [list/item-icon (transaction-type->icon type)])] - [react/view {:style styles/details-header-infos} - [react/nested-text {:style styles/details-header-value} - [{:accessibility-label :amount-text} - (pretty-print-asset symbol value token)] - " " - [{:accessibility-label :currency-text} - (wallet.utils/display-symbol asset)]] - [react/text {:style styles/details-header-date} date]]])) +(defn details-header + [date type amount-text currency-text] + [react/view {:style styles/details-header} + [react/view {:style styles/details-header-icon} + (when type + [list/item-icon (transaction-type->icon type)])] + [react/view {:style styles/details-header-infos} + [react/nested-text {:style styles/details-header-value} + [{:accessibility-label :amount-text} amount-text] + " " + [{:accessibility-label :currency-text} currency-text]] + [react/text {:style styles/details-header-date} date]]]) (defn progress-bar [progress failed?] [react/view {:style styles/progress-bar} [react/view {:style (styles/progress-bar-done progress failed?)}] [react/view {:style (styles/progress-bar-todo (- 100 progress) failed?)}]]) -(defn details-confirmations [confirmations confirmations-progress type] +(defn details-confirmations + [confirmations confirmations-progress type] [react/view {:style styles/details-block} [progress-bar confirmations-progress (failed? type)] (if (failed? type) @@ -213,10 +212,12 @@ [react/text (merge {:style styles/details-item-extra-value} extra-props) (str extra-value)]]]))) -(defn details-list [{:keys [block hash - from from-wallet from-contact - to to-wallet to-contact - gas-limit gas-price-gwei gas-price-eth gas-used cost nonce data]}] +(defn details-list + [{:keys [block hash + from from-wallet from-contact + to to-wallet to-contact + gas-limit gas-price-gwei gas-price-eth gas-used + cost nonce data]}] [react/view {:style styles/details-block} [details-list-row :t/block block] [details-list-row :t/hash hash] @@ -240,15 +241,16 @@ [details-list-row :t/data data]]) (defn details-action [hash url] - [(actions/opts [{:label (i18n/label :t/copy-transaction-hash) :action #(react/copy-to-clipboard hash)} - {:label (i18n/label :t/open-on-etherscan) :action #(.openURL react/linking url)}])]) + [(actions/opts [{:label (i18n/label :t/copy-transaction-hash) + :action #(react/copy-to-clipboard hash)} + {:label (i18n/label :t/open-on-etherscan) + :action #(.openURL react/linking url)}])]) (defview transaction-details [] - (letsubs [{:keys [hash url type] :as transaction} [:wallet.transactions/transaction-details] - confirmations [:wallet.transactions.details/confirmations] - confirmations-progress [:wallet.transactions.details/confirmations-progress] - network [:account/network] - all-tokens [:wallet/all-tokens]] + (letsubs [{:keys [hash url type confirmations confirmations-progress + date amount-text currency-text] + :as transaction} + [:wallet.transactions.details/screen]] [react/view {:style components.styles/flex} [status-bar/status-bar] [toolbar/toolbar {} @@ -256,7 +258,7 @@ [toolbar/content-title (i18n/label :t/transaction-details)] (when transaction [toolbar/actions (details-action hash url)])] [react/scroll-view {:style components.styles/main-container} - [details-header network all-tokens transaction] + [details-header date type amount-text currency-text] [details-confirmations confirmations confirmations-progress type] [react/view {:style styles/details-separator}] [details-list transaction]]])) From 2cd26c585d094b09283521228a8446da14824cee Mon Sep 17 00:00:00 2001 From: yenda Date: Fri, 17 May 2019 12:02:16 +0200 Subject: [PATCH 4/8] [refactor] transaction history and filters --- .../chat/commands/impl/transactions.cljs | 47 ++-- src/status_im/constants.cljs | 7 - src/status_im/events.cljs | 18 ++ src/status_im/subs.cljs | 251 ++++++++++-------- src/status_im/ui/screens/db.cljs | 6 +- src/status_im/ui/screens/events.cljs | 1 - .../ui/screens/wallet/request/events.cljs | 12 +- .../ui/screens/wallet/send/events.cljs | 2 +- .../screens/wallet/transactions/events.cljs | 31 --- .../ui/screens/wallet/transactions/views.cljs | 231 ++++++++-------- src/status_im/{ui/screens => }/wallet/db.cljs | 16 +- test/cljs/status_im/test/runner.cljs | 2 - .../status_im/test/ui/screens/wallet/db.cljs | 2 +- .../test/wallet/transactions/views.cljs | 12 - 14 files changed, 326 insertions(+), 312 deletions(-) delete mode 100644 src/status_im/ui/screens/wallet/transactions/events.cljs rename src/status_im/{ui/screens => }/wallet/db.cljs (86%) delete mode 100644 test/cljs/status_im/test/wallet/transactions/views.cljs diff --git a/src/status_im/chat/commands/impl/transactions.cljs b/src/status_im/chat/commands/impl/transactions.cljs index 8f7a1d08dbd..ecf1adad990 100644 --- a/src/status_im/chat/commands/impl/transactions.cljs +++ b/src/status_im/chat/commands/impl/transactions.cljs @@ -1,30 +1,34 @@ (ns status-im.chat.commands.impl.transactions - (:require-macros [status-im.utils.views :refer [defview letsubs]]) (:require [clojure.string :as string] - [reagent.core :as reagent] [re-frame.core :as re-frame] + [reagent.core :as reagent] + [status-im.chat.commands.impl.transactions.styles + :as + transactions-styles] [status-im.chat.commands.protocol :as protocol] - [status-im.chat.commands.impl.transactions.styles :as transactions-styles] + [status-im.contact.db :as db.contact] [status-im.data-store.messages :as messages-store] - [status-im.ui.components.react :as react] - [status-im.ui.components.icons.vector-icons :as vector-icons] + [status-im.i18n :as i18n] + [status-im.ui.components.animation :as animation] [status-im.ui.components.colors :as colors] + [status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.list.views :as list] - [status-im.ui.components.animation :as animation] + [status-im.ui.components.react :as react] [status-im.ui.components.svgimage :as svgimage] - [status-im.i18n :as i18n] - [status-im.contact.db :as db.contact] + [status-im.ui.screens.navigation :as navigation] + [status-im.ui.screens.wallet.choose-recipient.events + :as + choose-recipient.events] + [status-im.ui.screens.wallet.utils :as wallet.utils] + [status-im.utils.datetime :as datetime] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.datetime :as datetime] [status-im.utils.fx :as fx] [status-im.utils.money :as money] [status-im.utils.platform :as platform] - [status-im.ui.screens.wallet.db :as wallet.db] - [status-im.ui.screens.wallet.choose-recipient.events :as choose-recipient.events] - [status-im.ui.screens.navigation :as navigation] - [status-im.ui.screens.wallet.utils :as wallet.utils] - [status-im.ui.components.chat-icon.screen :as chat-icon])) + [status-im.ui.components.chat-icon.screen :as chat-icon] + [status-im.wallet.db :as wallet.db]) + (:require-macros [status-im.utils.views :refer [defview letsubs]])) ;; common `send/request` functionality @@ -179,19 +183,22 @@ ;; `/send` command (defview send-status [tx-hash outgoing] - (letsubs [confirmed? [:chats/transaction-confirmed? tx-hash] - tx-exists? [:chats/wallet-transaction-exists? tx-hash]] - [react/touchable-highlight {:on-press #(when tx-exists? + (letsubs [{:keys [exists? confirmed?]} [:chats/transaction-status tx-hash]] + [react/touchable-highlight {:on-press #(when exists? (re-frame/dispatch [:show-transaction-details tx-hash]))} [react/view transactions-styles/command-send-status-container - [vector-icons/icon (if confirmed? :tiny-icons/tiny-check :tiny-icons/tiny-pending) - {:color (if outgoing colors/blue-light colors/blue) + [vector-icons/icon (if confirmed? + :tiny-icons/tiny-check + :tiny-icons/tiny-pending) + {:color (if outgoing + colors/blue-light + colors/blue) :container-style (transactions-styles/command-send-status-icon outgoing)}] [react/view [react/text {:style (transactions-styles/command-send-status-text outgoing)} (i18n/label (cond confirmed? :status-confirmed - tx-exists? :status-pending + exists? :status-pending :else :status-tx-not-found))]]]])) (defn transaction-status [{:keys [tx-hash outgoing]}] diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 732ef73de69..8a6eb6e4307 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -30,13 +30,6 @@ (def system "system") -(def default-wallet-transactions - {:filters - {:type [{:id :inbound :label (i18n/label :t/incoming) :checked? true} - {:id :outbound :label (i18n/label :t/outgoing) :checked? true} - {:id :pending :label (i18n/label :t/pending) :checked? true} - {:id :failed :label (i18n/label :t/failed) :checked? true}]}}) - (def mainnet-networks {"mainnet" {:id "mainnet", :name "Mainnet", diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index acd9bca2dbc..7bb5d7b8573 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -57,6 +57,7 @@ [status-im.utils.handlers :as handlers] [status-im.utils.logging.core :as logging] [status-im.utils.utils :as utils] + [status-im.wallet.db :as wallet.db] [status-im.web3.core :as web3] [taoensso.timbre :as log] [status-im.wallet.custom-tokens.core :as custom-tokens])) @@ -2144,3 +2145,20 @@ :ethereum.transactions/new (fn [cofx [_ transaction]] (ethereum.transactions/new cofx transaction))) + +;; wallet events +(handlers/register-handler-fx + :wallet.transactions/add-filter + (fn [{:keys [db]} [_ id]] + {:db (update-in db [:wallet :filters] conj id)})) + +(handlers/register-handler-fx + :wallet.transactions/remove-filter + (fn [{:keys [db]} [_ id]] + {:db (update-in db [:wallet :filters] disj id)})) + +(handlers/register-handler-fx + :wallet.transactions/add-all-filters + (fn [{:keys [db]} _] + {:db (assoc-in db [:wallet :filters] + wallet.db/default-wallet-filters)})) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 08db7a50765..fc2cb85f180 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -39,7 +39,8 @@ status-im.ui.screens.hardwallet.connect.subs status-im.ui.screens.hardwallet.settings.subs status-im.ui.screens.hardwallet.pin.subs - status-im.ui.screens.hardwallet.setup.subs)) + status-im.ui.screens.hardwallet.setup.subs + [status-im.wallet.db :as wallet.db])) ;; TOP LEVEL =========================================================================================================== @@ -690,18 +691,16 @@ (filter-contacts selected-contacts active-contacts))) (re-frame/reg-sub - :chats/transaction-confirmed? - :<- [:wallet-transactions] - (fn [txs [_ tx-hash]] - (-> (get-in txs [tx-hash :confirmations] "0") - (js/parseInt) - (>= transactions/confirmations-count-threshold)))) - -(re-frame/reg-sub - :chats/wallet-transaction-exists? - :<- [:wallet-transactions] - (fn [txs [_ tx-hash]] - (not (nil? (get txs tx-hash))))) + :chats/transaction-status + :<- [:wallet/transactions] + :<- [:ethereum/current-block] + (fn [[transactions current-block] [_ hash]] + (when-let [transaction (get transactions hash)] + {:exists? true + :confirmed? + (-> transaction + (wallet.db/get-confirmations current-block) + (>= transactions/confirmations-count-threshold))}))) ;;BOOTNODES ============================================================================================================ @@ -1026,87 +1025,133 @@ ;;WALLET TRANSACTIONS ================================================================================================== (re-frame/reg-sub - :wallet-transactions + :wallet/transactions :<- [:wallet] (fn [wallet] (get wallet :transactions))) (re-frame/reg-sub - :wallet.transactions/filters - :<- [:wallet.transactions] - (fn [txs] - (get txs :filters))) + :wallet/filters + :<- [:wallet] + (fn [wallet] + (get wallet :filters))) (defn enrich-transaction - [{:keys [type to from timestamp] :as transaction} contacts] - (let [[contact-address key-contact key-wallet] (if (= type :inbound) - [from :from-contact :to-wallet] - [to :to-contact :from-wallet]) + [{:keys [type to from value token] :as transaction} + contacts native-currency] + (let [[contact-address key-contact key-wallet] + (if (= type :inbound) + [from :from-contact :to-wallet] + [to :to-contact :from-wallet]) wallet (i18n/label :main-wallet) - contact (get contacts (utils.hex/normalize-hex contact-address))] + contact (get contacts contact-address) + {:keys [symbol-display symbol decimals] :as asset} + (or token native-currency) + amount-text (if value + (wallet.utils/format-amount value decimals) + "...") + currency-text (when asset + (clojure.core/name (or symbol-display symbol)))] (cond-> transaction contact (assoc key-contact (:name contact)) :always (assoc key-wallet wallet - :time-formatted (datetime/timestamp->time timestamp))))) + :amount-text amount-text + :currency-text currency-text)))) (re-frame/reg-sub :wallet.transactions/transactions - :<- [:wallet] + :<- [:wallet/transactions] :<- [:contacts/contacts-by-address] - (fn [[wallet contacts]] - (reduce (fn [acc [hash transaction]] - (assoc acc hash (enrich-transaction transaction contacts))) - {} - (:transactions wallet)))) + :<- [:ethereum/native-currency] + (fn [[transactions contacts native-currency]] + (reduce-kv (fn [acc hash transaction] + (assoc acc + hash + (enrich-transaction transaction contacts native-currency))) + {} + transactions))) (re-frame/reg-sub - :wallet.transactions/grouped-transactions - :<- [:wallet.transactions/transactions] - (fn [transactions] - (group-by :type (vals transactions)))) + :wallet.transactions/all-filters? + :<- [:wallet/filters] + (fn [filters] + (= wallet.db/default-wallet-filters + filters))) -(re-frame/reg-sub - :wallet.transactions/pending-transactions-list - :<- [:wallet.transactions/grouped-transactions] - (fn [{:keys [pending]}] - (when pending - {:title "Pending" - :key :pending - :data pending}))) +(def filters-labels + {:inbound (i18n/label :t/incoming) + :outbound (i18n/label :t/outgoing) + :pending (i18n/label :t/pending) + :failed (i18n/label :t/failed)}) (re-frame/reg-sub - :wallet.transactions/failed-transactions-list - :<- [:wallet.transactions/grouped-transactions] - (fn [{:keys [failed]}] - (when failed - {:title "Failed" - :key :failed - :data failed}))) - -(defn group-transactions-by-date [transactions] + :wallet.transactions/filters + :<- [:wallet/filters] + (fn [filters] + (map (fn [id] + (let [checked? (filters id)] + {:id id + :label (filters-labels id) + :checked? checked? + :on-touch #(if checked? + (re-frame/dispatch [:wallet.transactions/remove-filter id]) + (re-frame/dispatch [:wallet.transactions/add-filter id]))})) + wallet.db/default-wallet-filters))) + +(re-frame/reg-sub + :wallet.transactions.filters/screen + :<- [:wallet.transactions/filters] + :<- [:wallet.transactions/all-filters?] + (fn [[filters all-filters?]] + {:all-filters? all-filters? + :filters filters + :on-touch-select-all (when-not all-filters? + #(re-frame/dispatch + [:wallet.transactions/add-all-filters]))})) + +(defn- enrich-transaction-for-list + [filters + {:keys [type from-contact from to-contact to hash timestamp] :as transaction}] + (when (filters type) + (assoc (case type + :inbound + (assoc transaction + :label (i18n/label :t/from) + :contact-accessibility-label :sender-text + :address-accessibility-label :sender-address-text + :contact from-contact + :address from) + (assoc transaction + :label (i18n/label :t/to) + :contact-accessibility-label :recipient-name-text + :address-accessibility-label :recipient-address-text + :contact to-contact + :address to)) + :time-formatted (datetime/timestamp->time timestamp) + :on-touch-fn #(re-frame/dispatch [:show-transaction-details hash])))) + +(defn- group-transactions-by-date + [transactions] (->> transactions (group-by #(datetime/timestamp->date-key (:timestamp %))) - (sort-by key) - reverse + (sort-by key >) (map (fn [[date-key transactions]] {:title (datetime/timestamp->mini-date (:timestamp (first transactions))) :key date-key :data (sort-by :timestamp > transactions)})))) (re-frame/reg-sub - :wallet.transactions/completed-transactions-list - :<- [:wallet.transactions/grouped-transactions] - (fn [{:keys [inbound outbound failed]}] - (group-transactions-by-date (concat inbound outbound failed)))) - -(re-frame/reg-sub - :wallet.transactions/transactions-history-list - :<- [:wallet.transactions/pending-transactions-list] - :<- [:wallet.transactions/completed-transactions-list] - (fn [[pending completed]] - (cond-> [] - pending (into pending) - completed (into completed)))) + :wallet.transactions.history/screen + :<- [:wallet.transactions/transactions] + :<- [:wallet/filters] + :<- [:wallet.transactions/all-filters?] + (fn [[transactions filters all-filters?]] + {:all-filters? all-filters? + :transaction-history-sections + (->> transactions + vals + (keep #(enrich-transaction-for-list filters %)) + (group-transactions-by-date))})) (re-frame/reg-sub :wallet.transactions/current-transaction @@ -1123,55 +1168,45 @@ (fn [[transactions current-transaction native-currency chain-keyword]] (let [{:keys [gas-used gas-price hash timestamp type token value] :as transaction} - (get transactions current-transaction)] + (get transactions current-transaction) + native-currency-text (-> native-currency + :symbol-display + name)] (when transaction - (let [{:keys [symbol-display symbol decimals] :as asset} - (or token native-currency) - amount-text (if value - (wallet.utils/format-amount value decimals) - "...") - currency-text (when asset - (clojure.core/name (or symbol-display symbol))) - native-currency-text (-> native-currency - :symbol-display - name)] - (merge transaction - {:amount-text amount-text - :currency-text currency-text - :gas-price-eth (if gas-price - (money/wei->str :eth - gas-price - native-currency-text) - "-") - :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) - :cost (i18n/label :not-applicable) - :gas-limit (i18n/label :not-applicable) - :gas-used (i18n/label :not-applicable) - :nonce (i18n/label :not-applicable) - :hash (i18n/label :not-applicable)} - {:cost (when gas-used - (money/wei->str :eth - (money/fee-value gas-used gas-price) - native-currency-text)) - :url (transactions.etherscan/get-transaction-details-url - chain-keyword - hash)}))))))) + (merge transaction + {:gas-price-eth (if gas-price + (money/wei->str :eth + gas-price + native-currency-text) + "-") + :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) + :cost (i18n/label :not-applicable) + :gas-limit (i18n/label :not-applicable) + :gas-used (i18n/label :not-applicable) + :nonce (i18n/label :not-applicable) + :hash (i18n/label :not-applicable)} + {:cost (when gas-used + (money/wei->str :eth + (money/fee-value gas-used gas-price) + native-currency-text)) + :url (transactions.etherscan/get-transaction-details-url + chain-keyword + hash)})))))) (re-frame/reg-sub :wallet.transactions.details/screen :<- [:wallet.transactions.details/current-transaction] :<- [:ethereum/current-block] - (fn [[{:keys [block] :as transaction} current-block]] + (fn [[transaction current-block]] :wallet.transactions.details/current-transaction - (let [confirmations (if (and current-block block) - (inc (- current-block block)) - 0)] + (let [confirmations (wallet.db/get-confirmations transaction + current-block)] (assoc transaction :confirmations confirmations :confirmations-progress diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index 2701fa47b13..050def0c904 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -16,7 +16,8 @@ status-im.browser.db status-im.ui.screens.add-new.db status-im.ui.screens.add-new.new-public-chat.db - status-im.ui.components.bottom-sheet.core)) + status-im.ui.components.bottom-sheet.core + [status-im.wallet.db :as wallet.db])) ;; initial state of app-db (def app-db {:keyboard-height 0 @@ -32,7 +33,7 @@ :selected-participants #{} :sync-state :done :app-state "active" - :wallet.transactions constants/default-wallet-transactions + :wallet wallet.db/default-wallet :wallet/all-tokens {} :prices {} :peers-count 0 @@ -329,7 +330,6 @@ :chat/id->command :chat/access-scope->command-id :wallet/wallet - :wallet/wallet.transactions :prices/prices :prices/prices-loading? :notifications/notifications diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs index ae3a3f87a94..7c1c2e13237 100644 --- a/src/status_im/ui/screens/events.cljs +++ b/src/status_im/ui/screens/events.cljs @@ -15,7 +15,6 @@ status-im.ui.screens.wallet.send.events status-im.ui.screens.wallet.request.events status-im.ui.screens.wallet.settings.events - status-im.ui.screens.wallet.transactions.events status-im.ui.screens.wallet.choose-recipient.events status-im.ui.screens.wallet.collectibles.cryptokitties.events status-im.ui.screens.wallet.collectibles.cryptostrikers.events diff --git a/src/status_im/ui/screens/wallet/request/events.cljs b/src/status_im/ui/screens/wallet/request/events.cljs index a8329d176e1..8bd06811276 100644 --- a/src/status_im/ui/screens/wallet/request/events.cljs +++ b/src/status_im/ui/screens/wallet/request/events.cljs @@ -1,12 +1,10 @@ (ns status-im.ui.screens.wallet.request.events - (:require [re-frame.core :as re-frame] - [status-im.ui.screens.wallet.db :as wallet-db] - [status-im.ui.screens.navigation :as navigation] - [status-im.utils.handlers :as handlers] + (:require [status-im.chat.commands.sending :as commands-sending] [status-im.chat.models :as chat-model] - [status-im.chat.commands.sending :as commands-sending] [status-im.utils.fx :as fx] - [status-im.utils.money :as money])) + [status-im.utils.handlers :as handlers] + [status-im.utils.money :as money] + [status-im.wallet.db :as wallet.db])) (handlers/register-handler-fx :wallet-send-request @@ -23,7 +21,7 @@ (handlers/register-handler-fx :wallet.request/set-and-validate-amount (fn [{:keys [db]} [_ amount symbol decimals]] - (let [{:keys [value error]} (wallet-db/parse-amount amount decimals)] + (let [{:keys [value error]} (wallet.db/parse-amount amount decimals)] {:db (-> db (assoc-in [:wallet :request-transaction :amount] (money/formatted->internal value symbol decimals)) (assoc-in [:wallet :request-transaction :amount-text] amount) diff --git a/src/status_im/ui/screens/wallet/send/events.cljs b/src/status_im/ui/screens/wallet/send/events.cljs index 82e901a89a8..be7ff0c9a72 100644 --- a/src/status_im/ui/screens/wallet/send/events.cljs +++ b/src/status_im/ui/screens/wallet/send/events.cljs @@ -7,7 +7,7 @@ [status-im.native-module.core :as status] [status-im.transport.utils :as transport.utils] [status-im.ui.screens.navigation :as navigation] - [status-im.ui.screens.wallet.db :as wallet.db] + [status-im.wallet.db :as wallet.db] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.ethereum.erc20 :as erc20] [status-im.utils.ethereum.tokens :as tokens] diff --git a/src/status_im/ui/screens/wallet/transactions/events.cljs b/src/status_im/ui/screens/wallet/transactions/events.cljs deleted file mode 100644 index 148da85e860..00000000000 --- a/src/status_im/ui/screens/wallet/transactions/events.cljs +++ /dev/null @@ -1,31 +0,0 @@ -(ns status-im.ui.screens.wallet.transactions.events - (:require [status-im.utils.handlers :as handlers])) - -(defn- mark-all-checked [filters] - (update filters - :type - #(map (fn [m] - (assoc m :checked? true)) - %))) - -(defn- mark-checked [filters {:keys [type]} checked?] - (update filters - :type - #(map (fn [{:keys [id] :as m}] - (if (= type id) - (assoc m :checked? checked?) - m)) - %))) - -(defn- update-filters [db f] - (update-in db [:wallet.transactions :filters] f)) - -(handlers/register-handler-fx - :wallet.transactions/filter - (fn [{:keys [db]} [_ path checked?]] - {:db (update-filters db #(mark-checked % path checked?))})) - -(handlers/register-handler-fx - :wallet.transactions/filter-all - (fn [{:keys [db]} _] - {:db (update-filters db mark-all-checked)})) diff --git a/src/status_im/ui/screens/wallet/transactions/views.cljs b/src/status_im/ui/screens/wallet/transactions/views.cljs index e2775b1698f..dc1aeba6bcc 100644 --- a/src/status_im/ui/screens/wallet/transactions/views.cljs +++ b/src/status_im/ui/screens/wallet/transactions/views.cljs @@ -15,154 +15,149 @@ [status-im.utils.money :as money]) (:require-macros [status-im.utils.views :refer [defview letsubs]])) -(defn history-action [filter?] +;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TRANSACTION HISTORY +;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn history-action + [all-filters?] (cond-> {:icon :main-icons/filter :icon-opts {:accessibility-label :filters-button} :handler #(re-frame/dispatch [:navigate-to :wallet-transactions-filter])} - filter? (assoc-in [:icon-opts :overlay-style] styles/corner-dot))) - -(defn- all-checked? [filter-data] - (and (every? :checked? (:type filter-data)) - (every? :checked? (:tokens filter-data)))) + (not all-filters?) (assoc-in [:icon-opts :overlay-style] styles/corner-dot))) -(defn- toolbar-view [filter-data] +(defn- toolbar-view + [all-filters?] [toolbar/toolbar nil toolbar/default-nav-back [toolbar/content-title (i18n/label :t/transactions-history)] [toolbar/actions - [(history-action (not (all-checked? filter-data)))]]]) - -(defn- inbound? [type] (= :inbound type)) -(defn- failed? [type] (= :failed type)) + [(history-action all-filters?)]]]) -(defn- transaction-icon [k background-color color] - {:icon k +(defn- transaction-icon + [icon-key background-color color] + {:icon icon-key :icon-opts {:color color} :style (styles/transaction-icon-background background-color)}) -(defn- transaction-type->icon [k] +(defn- transaction-type->icon + [k] (case k - :inbound (transaction-icon :main-icons/arrow-left (colors/alpha colors/green 0.2) colors/green) - :outbound (transaction-icon :main-icons/arrow-right (colors/alpha colors/blue 0.1) colors/blue) - :failed (transaction-icon :main-icons/warning colors/gray-light colors/red) - (:postponed :pending) (transaction-icon :main-icons/arrow-right colors/gray-light colors/gray) + :inbound (transaction-icon :main-icons/arrow-left + (colors/alpha colors/green 0.2) + colors/green) + :outbound (transaction-icon :main-icons/arrow-right + (colors/alpha colors/blue 0.1) + colors/blue) + :failed (transaction-icon :main-icons/warning + colors/gray-light + colors/red) + :pending (transaction-icon :main-icons/arrow-right + colors/gray-light colors/gray) (throw (str "Unknown transaction type: " k)))) (defn render-transaction - [{:keys [hash from-contact to-contact to from type value time-formatted symbol]} - network all-tokens hide-details?] - (let [[label contact address contact-accessibility-label - address-accessibility-label] - (if (inbound? type) - [(i18n/label :t/from) from-contact from :sender-text :sender-address-text] - [(i18n/label :t/to) to-contact to :recipient-name-text :recipient-address-text]) - {:keys [decimals] :as token} - (tokens/asset-for all-tokens - (ethereum/network->chain-keyword network) - symbol)] - [list/touchable-item #(when-not hide-details? (re-frame/dispatch [:show-transaction-details hash])) - [react/view {:accessibility-label :transaction-item} - [list/item - (when type - [list/item-icon (transaction-type->icon (keyword type))]) - [list/item-content - [react/view {:style styles/amount-time} - [react/nested-text {:style styles/tx-amount - :ellipsize-mode "tail" - :number-of-lines 1} - [{:accessibility-label :amount-text} - (-> value (money/internal->formatted symbol decimals) money/to-fixed str)] - " " - [{:accessibility-label :currency-text} - (wallet.utils/display-symbol token)]] - [react/text {:style styles/tx-time} - time-formatted]] - [react/view {:style styles/address-row} - [react/text {:style styles/address-label} - label] - (when contact - [react/text {:style styles/address-contact - :accessibility-label contact-accessibility-label} - contact]) - [react/text {:style styles/address-hash - :ellipsize-mode "middle" - :number-of-lines 1 - :accessibility-label address-accessibility-label} - address]]] - (when-not hide-details? - [list/item-icon {:icon :main-icons/next - :style {:margin-top 10} - :icon-opts (merge styles/forward - {:accessibility-label :show-transaction-button})}])]]])) - -(defn filtered-transaction? [transaction filter-data] - (:checked? (some #(when (= (:type transaction) (:id %)) %) (:type filter-data)))) + [{:keys [label contact address contact-accessibility-label + address-accessibility-label currency-text amount-text + time-formatted on-touch-fn type]}] + [list/touchable-item on-touch-fn + [react/view {:accessibility-label :transaction-item} + [list/item + (when type + [list/item-icon (transaction-type->icon (keyword type))]) + [list/item-content + [react/view {:style styles/amount-time} + [react/nested-text {:style styles/tx-amount + :ellipsize-mode "tail" + :number-of-lines 1} + [{:accessibility-label :amount-text} + amount-text] + " " + [{:accessibility-label :currency-text} + currency-text]] + [react/text {:style styles/tx-time} + time-formatted]] + [react/view {:style styles/address-row} + [react/text {:style styles/address-label} + label] + (when contact + [react/text {:style styles/address-contact + :accessibility-label contact-accessibility-label} + contact]) + [react/text {:style styles/address-hash + :ellipsize-mode "middle" + :number-of-lines 1 + :accessibility-label address-accessibility-label} + address]]] + [list/item-icon {:icon :main-icons/next + :style {:margin-top 10} + :icon-opts (merge styles/forward + {:accessibility-label :show-transaction-button})}]]]]) -(defn update-transactions [m filter-data] - (update m - :data - (fn [v] - (filter #(filtered-transaction? % filter-data) v)))) +(defn history-list + [transactions-history-sections] + [react/view components.styles/flex + [list/section-list {:sections transactions-history-sections + :key-fn :hash + :render-fn #(render-transaction %) + :empty-component + [react/i18n-text {:style styles/empty-text + :key :transactions-history-empty}] + :refreshing false}]]) -(defview history-list [& [hide-details?]] - (letsubs [transactions-history-list [:wallet.transactions/transactions-history-list] - filter-data [:wallet.transactions/filters] - network [:account/network] - all-tokens [:wallet/all-tokens]] - [react/view components.styles/flex - [list/section-list {:sections (map #(update-transactions % filter-data) transactions-history-list) - :key-fn :hash - :render-fn #(render-transaction % network all-tokens hide-details?) - :empty-component [react/i18n-text {:style styles/empty-text - :key :transactions-history-empty}] - :refreshing false}]])) +(defview transactions + [] + (letsubs [{:keys [transaction-history-sections all-filters?]} + [:wallet.transactions.history/screen]] + [react/view styles/transactions-view + [status-bar/status-bar] + [toolbar-view all-filters?] + [history-list transaction-history-sections]])) -;; Filter history +;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TRANSACTION FILTERS +;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- item-filter [{:keys [icon checked? path]} content] +(defn- render-item-filter [{:keys [id label checked? on-touch]}] [react/view {:accessibility-label :filter-item} [list/list-item-with-checkbox {:checked? checked? - :on-value-change #(re-frame/dispatch [:wallet.transactions/filter path %])} + :on-value-change on-touch} [list/item - [list/item-icon icon] - content]]]) - -(defn- render-item-filter [{:keys [id label checked?]}] - (when id - [item-filter {:icon (transaction-type->icon id) :checked? checked? :path {:type id}} + [list/item-icon (transaction-type->icon id)] [list/item-content [list/item-primary-only {:accessibility-label :filter-name-text} - label]]])) - -(defn- wrap-filter-data [m] - [{:title (i18n/label :t/transactions-filter-type) - :key :type - :render-fn render-item-filter - :data (:type m)}]) + label]]]]]) (defview filter-history [] - (letsubs [filter-data [:wallet.transactions/filters]] + (letsubs [{:keys [filters all-filters? on-touch-select-all]} + [:wallet.transactions.filters/screen]] [react/view styles/filter-container [status-bar/status-bar {:type :modal-main}] [toolbar/toolbar {} - [toolbar/nav-clear-text {:accessibility-label :done-button} (i18n/label :t/done)] - [toolbar/content-title (i18n/label :t/transactions-filter-title)] - [toolbar/text-action {:handler #(re-frame/dispatch [:wallet.transactions/filter-all]) - :disabled? (all-checked? filter-data) - :accessibility-label :select-all-button} + [toolbar/nav-clear-text + {:accessibility-label :done-button} + (i18n/label :t/done)] + [toolbar/content-title + (i18n/label :t/transactions-filter-title)] + [toolbar/text-action + {:handler on-touch-select-all + :disabled? all-filters? + :accessibility-label :select-all-button} (i18n/label :t/transactions-filter-select-all)]] - [react/view {:style (merge {:background-color :white} components.styles/flex)} - [list/section-list {:sections (wrap-filter-data filter-data) - :key-fn (comp str :id)}]]])) + [react/view + {:style (merge {:background-color :white} components.styles/flex)} + [list/section-list {:sections [{:title + (i18n/label :t/transactions-filter-type) + :key :type + :render-fn render-item-filter + :data filters}] + :key-fn :id}]]])) -(defview transactions [] - (letsubs [filter-data [:wallet.transactions/filters]] - [react/view styles/transactions-view - [status-bar/status-bar] - [toolbar-view filter-data] - [history-list]])) +;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TRANSACTION DETAILS +;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn details-header [date type amount-text currency-text] @@ -183,10 +178,10 @@ [react/view {:style (styles/progress-bar-todo (- 100 progress) failed?)}]]) (defn details-confirmations - [confirmations confirmations-progress type] + [confirmations confirmations-progress failed?] [react/view {:style styles/details-block} - [progress-bar confirmations-progress (failed? type)] - (if (failed? type) + [progress-bar confirmations-progress failed?] + (if failed? [react/i18n-text {:style styles/details-failed :key :failed}] [react/text {:style styles/details-confirmations-count} @@ -259,6 +254,6 @@ (when transaction [toolbar/actions (details-action hash url)])] [react/scroll-view {:style components.styles/main-container} [details-header date type amount-text currency-text] - [details-confirmations confirmations confirmations-progress type] + [details-confirmations confirmations confirmations-progress (= :failed type)] [react/view {:style styles/details-separator}] [details-list transaction]]])) diff --git a/src/status_im/ui/screens/wallet/db.cljs b/src/status_im/wallet/db.cljs similarity index 86% rename from src/status_im/ui/screens/wallet/db.cljs rename to src/status_im/wallet/db.cljs index adeb3216057..364a421bbb9 100644 --- a/src/status_im/ui/screens/wallet/db.cljs +++ b/src/status_im/wallet/db.cljs @@ -1,4 +1,4 @@ -(ns status-im.ui.screens.wallet.db +(ns status-im.wallet.db (:require [cljs.spec.alpha :as spec] [status-im.i18n :as i18n] status-im.ui.screens.wallet.request.db @@ -22,6 +22,7 @@ (spec/def :wallet/visible-tokens any?) (spec/def :wallet/currency any?) (spec/def :wallet/balance any?) +(spec/def :wallet/filters set?) (spec/def :wallet/wallet (spec/keys :opt-un [:wallet/send-transaction :wallet/request-transaction @@ -30,6 +31,7 @@ :wallet/errors :wallet/transactions :wallet/edit + :wallet/filters :wallet/current-tab :wallet/current-transaction :wallet/modal-history? @@ -58,3 +60,15 @@ :else {:value value})))) + +(def default-wallet-filters + #{:inbound :outbound :pending :failed}) + +(def default-wallet + {:filters default-wallet-filters}) + +(defn get-confirmations + [{:keys [block]} current-block] + (if (and current-block block) + (inc (- current-block block)) + 0)) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 85a88f04c8c..4d7bb92f8e6 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -12,7 +12,6 @@ [status-im.test.wallet.subs] [status-im.test.wallet.transactions] [status-im.test.wallet.transactions.subs] - [status-im.test.wallet.transactions.views] [status-im.test.mailserver.core] [status-im.test.fleet.core] [status-im.test.group-chats.core] @@ -99,7 +98,6 @@ 'status-im.test.wallet.subs 'status-im.test.wallet.transactions 'status-im.test.wallet.transactions.subs - 'status-im.test.wallet.transactions.views 'status-im.test.chat.models.loading 'status-im.test.chat.models.input 'status-im.test.chat.models.message diff --git a/test/cljs/status_im/test/ui/screens/wallet/db.cljs b/test/cljs/status_im/test/ui/screens/wallet/db.cljs index 29fb2ac34d7..64e9e15dfcd 100644 --- a/test/cljs/status_im/test/ui/screens/wallet/db.cljs +++ b/test/cljs/status_im/test/ui/screens/wallet/db.cljs @@ -1,6 +1,6 @@ (ns status-im.test.ui.screens.wallet.db (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.ui.screens.wallet.db :as wallet.db] + [status-im.wallet.db :as wallet.db] [status-im.utils.money :as money] [status-im.i18n :as i18n])) diff --git a/test/cljs/status_im/test/wallet/transactions/views.cljs b/test/cljs/status_im/test/wallet/transactions/views.cljs deleted file mode 100644 index 5bce9ad1881..00000000000 --- a/test/cljs/status_im/test/wallet/transactions/views.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns status-im.test.wallet.transactions.views - (:require [cljs.test :refer [deftest is testing]] - [status-im.ui.screens.wallet.transactions.views :as views])) - -(deftest filtered-transaction? - (is (not (true? (views/filtered-transaction? {:type :inbound} {:type [{:id :outbound :checked? true}]})))) - (is (not (true? (views/filtered-transaction? {:type :inbound} {:type [{:id :inbound :checked? false}]})))) - (is (true? (views/filtered-transaction? {:type :inbound} {:type [{:id :inbound :checked? true}]}))) - (is (true? (views/filtered-transaction? {:type :inbound} {:type [{:id :outbound :checked? true} {:id :inbound :checked? true}]})))) - -(deftest update-transactions - (is (= {:data '()} (views/update-transactions {:data {:type :inbound}} {:type [{:id :outbound :checked? true}]})))) \ No newline at end of file From 89680f4861921f48f27ba9ca57a5d9294c47100e Mon Sep 17 00:00:00 2001 From: yenda Date: Sat, 18 May 2019 13:06:42 +0200 Subject: [PATCH 5/8] [performance] improve wallet update performances `wallet-autoconfig-token` is a very expensive call on mainnet because it checks the balance of every known token. it is called: - when wallet is refreshed by pulling - when user goes on any wallet screen this PR changes that by: - calling it only when the wallet is initialized and there is no visible-token configuration it only calls update-wallet when a new transaction arrives --- src/status_im/accounts/core.cljs | 8 +- src/status_im/accounts/login/core.cljs | 6 +- .../chat/commands/impl/transactions.cljs | 2 +- src/status_im/constants.cljs | 6 +- src/status_im/ethereum/transactions/core.cljs | 17 +- src/status_im/events.cljs | 95 +++ .../extensions/capacities/ethereum.cljs | 12 +- src/status_im/hardwallet/core.cljs | 32 +- src/status_im/models/wallet.cljs | 285 --------- src/status_im/stickers/core.cljs | 18 +- src/status_im/subs.cljs | 18 +- .../ui/screens/currency_settings/models.cljs | 4 +- src/status_im/ui/screens/events.cljs | 11 +- src/status_im/ui/screens/wallet/events.cljs | 240 -------- .../ui/screens/wallet/main/views.cljs | 6 +- .../ui/screens/wallet/navigation.cljs | 24 +- .../ui/screens/wallet/onboarding/views.cljs | 2 +- .../ui/screens/wallet/send/events.cljs | 36 +- .../ui/screens/wallet/settings/events.cljs | 19 - .../ui/screens/wallet/settings/models.cljs | 58 -- .../ui/screens/wallet/settings/views.cljs | 5 +- src/status_im/utils/ethereum/contracts.cljs | 4 +- src/status_im/wallet/core.cljs | 557 ++++++++++++++++++ src/status_im/wallet/custom_tokens/core.cljs | 8 +- test/cljs/status_im/test/models/wallet.cljs | 28 +- 25 files changed, 761 insertions(+), 740 deletions(-) delete mode 100644 src/status_im/models/wallet.cljs delete mode 100644 src/status_im/ui/screens/wallet/events.cljs delete mode 100644 src/status_im/ui/screens/wallet/settings/events.cljs delete mode 100644 src/status_im/ui/screens/wallet/settings/models.cljs create mode 100644 src/status_im/wallet/core.cljs diff --git a/src/status_im/accounts/core.cljs b/src/status_im/accounts/core.cljs index d21ea66aeb4..c38bf7583d1 100644 --- a/src/status_im/accounts/core.cljs +++ b/src/status_im/accounts/core.cljs @@ -2,14 +2,13 @@ (:require [re-frame.core :as re-frame] [status-im.accounts.update.core :as accounts.update] [status-im.i18n :as i18n] - [status-im.ui.screens.navigation :as navigation] [status-im.native-module.core :as native-module] - [status-im.ui.screens.wallet.settings.models :as wallet.settings.models] + [status-im.ui.screens.navigation :as navigation] + [status-im.utils.build :as build] [status-im.utils.config :as config] - [status-im.utils.utils :as utils] [status-im.utils.fx :as fx] [status-im.utils.platform :as platform] - [status-im.utils.build :as build])) + [status-im.utils.utils :as utils])) (re-frame/reg-fx ::chaos-mode-changed @@ -49,7 +48,6 @@ [{:keys [db] :as cofx} modal?] (fx/merge cofx (continue-after-wallet-onboarding modal?) - (wallet.settings.models/wallet-autoconfig-tokens) (accounts.update/account-update {:wallet-set-up-passed? true} {}))) (fx/defn update-dev-server-state diff --git a/src/status_im/accounts/login/core.cljs b/src/status_im/accounts/login/core.cljs index 6a62cb1cb33..44882bd2d69 100644 --- a/src/status_im/accounts/login/core.cljs +++ b/src/status_im/accounts/login/core.cljs @@ -7,7 +7,6 @@ [status-im.ethereum.transactions.core :as transactions] [status-im.fleet.core :as fleet] [status-im.i18n :as i18n] - [status-im.models.wallet :as models.wallet] [status-im.native-module.core :as status] [status-im.node.core :as node] [status-im.protocol.core :as protocol] @@ -22,6 +21,7 @@ [status-im.utils.security :as security] [status-im.utils.types :as types] [status-im.utils.universal-links.core :as universal-links] + [status-im.wallet.core :as wallet] [taoensso.timbre :as log])) (def rpc-endpoint "https://goerli.infura.io/v3/f315575765b14720b32382a61a89341a") @@ -84,10 +84,10 @@ (fx/defn initialize-wallet [cofx] (fx/merge cofx - (models.wallet/initialize-tokens) + (wallet/initialize-tokens) (transactions/initialize) (ethereum.subscriptions/initialize) - (models.wallet/update-wallet))) + (wallet/update-wallet))) (fx/defn user-login [{:keys [db] :as cofx} create-database?] (let [{:keys [address password]} (accounts.db/credentials cofx)] diff --git a/src/status_im/chat/commands/impl/transactions.cljs b/src/status_im/chat/commands/impl/transactions.cljs index ecf1adad990..fdd73ac25e8 100644 --- a/src/status_im/chat/commands/impl/transactions.cljs +++ b/src/status_im/chat/commands/impl/transactions.cljs @@ -185,7 +185,7 @@ (defview send-status [tx-hash outgoing] (letsubs [{:keys [exists? confirmed?]} [:chats/transaction-status tx-hash]] [react/touchable-highlight {:on-press #(when exists? - (re-frame/dispatch [:show-transaction-details tx-hash]))} + (re-frame/dispatch [:wallet.ui/show-transaction-details tx-hash]))} [react/view transactions-styles/command-send-status-container [vector-icons/icon (if confirmed? :tiny-icons/tiny-check diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 8a6eb6e4307..57f31d4259c 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -103,11 +103,7 @@ (defn default-account-settings [] {:web3-opt-in? true :preview-privacy? false - :wallet {:visible-tokens {:testnet #{:STT :HND} - :mainnet #{:SNT} - :rinkeby #{:MOKSHA :KDO} - :xdai #{} - :poa #{}}}}) + :wallet {:visible-tokens {}}}) (def currencies {:aed {:id :aed :code "AED" :display-name (i18n/label :t/currency-display-name-aed) :symbol "د.إ"} diff --git a/src/status_im/ethereum/transactions/core.cljs b/src/status_im/ethereum/transactions/core.cljs index 99f6047f966..fe9abff8756 100644 --- a/src/status_im/ethereum/transactions/core.cljs +++ b/src/status_im/ethereum/transactions/core.cljs @@ -8,6 +8,7 @@ [status-im.utils.ethereum.core :as ethereum] [status-im.utils.fx :as fx] [status-im.utils.types :as types] + [status-im.wallet.core :as wallet] [taoensso.timbre :as log])) (def confirmations-count-threshold 12) @@ -180,14 +181,18 @@ ;; ----------------------------------------------- (fx/defn new - [{:keys [db]} {:keys [hash] :as transaction}] - {:db (assoc-in db [:wallet :transactions hash] transaction)}) + [{:keys [db] :as cofx} {:keys [hash] :as transaction}] + (fx/merge cofx + {:db (assoc-in db [:wallet :transactions hash] transaction)} + wallet/update-wallet)) (fx/defn handle-history - [{:keys [db]} transactions] - {:db (update-in db - [:wallet :transactions] - #(merge transactions %))}) + [{:keys [db] :as cofx} transactions] + (fx/merge cofx + {:db (update-in db + [:wallet :transactions] + #(merge transactions %))} + wallet/update-wallet)) (fx/defn handle-token-history [{:keys [db]} transactions] diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 7bb5d7b8573..e30456593a4 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -57,6 +57,7 @@ [status-im.utils.handlers :as handlers] [status-im.utils.logging.core :as logging] [status-im.utils.utils :as utils] + [status-im.wallet.core :as wallet] [status-im.wallet.db :as wallet.db] [status-im.web3.core :as web3] [taoensso.timbre :as log] @@ -2147,6 +2148,12 @@ (ethereum.transactions/new cofx transaction))) ;; wallet events + +(handlers/register-handler-fx + :wallet.ui/pull-to-refresh + (fn [cofx _] + (wallet/update-wallet cofx))) + (handlers/register-handler-fx :wallet.transactions/add-filter (fn [{:keys [db]} [_ id]] @@ -2162,3 +2169,91 @@ (fn [{:keys [db]} _] {:db (assoc-in db [:wallet :filters] wallet.db/default-wallet-filters)})) + +(handlers/register-handler-fx + :wallet.settings/toggle-visible-token + (fn [cofx [_ symbol checked?]] + (wallet/toggle-visible-token cofx symbol checked?))) + +(handlers/register-handler-fx + :wallet/token-found + (fn [cofx [_ symbol balance]] + (wallet/configure-token-balance-and-visibility cofx symbol balance))) + +(handlers/register-handler-fx + :TODO.remove/update-wallet + (fn [cofx _] + (wallet/update-wallet cofx))) + +(handlers/register-handler-fx + :wallet.settings.ui/navigate-back-pressed + (fn [cofx [_ on-close]] + (fx/merge cofx + (when on-close + {:dispatch on-close}) + (navigation/navigate-back) + (wallet/update-wallet)))) + +(handlers/register-handler-fx + :wallet.callback/update-balance-success + (fn [cofx [_ balance]] + (wallet/update-balance cofx balance))) + +(handlers/register-handler-fx + :wallet.callback/update-balance-fail + (fn [cofx [_ err]] + (wallet/on-update-balance-fail cofx err))) + +(handlers/register-handler-fx + :wallet.callback/update-token-balance-success + (fn [cofx [_ symbol balance]] + (wallet/update-token-balance cofx symbol balance))) + +(handlers/register-handler-fx + :wallet.callback/update-token-balance-fail + (fn [cofx [_ symbol err]] + (wallet/on-update-token-balance-fail cofx symbol err))) + +(handlers/register-handler-fx + :wallet.callback/update-prices-success + (fn [cofx [_ prices]] + (wallet/update-prices cofx prices))) + +(handlers/register-handler-fx + :wallet.callback/update-prices-fail + (fn [cofx [_ err]] + (wallet/on-update-prices-fail cofx err))) + +(handlers/register-handler-fx + :wallet.ui/show-transaction-details + (fn [cofx [_ hash]] + (wallet/open-transaction-details cofx hash))) + +(handlers/register-handler-fx + :wallet/show-sign-transaction + (fn [cofx [_ {:keys [id method]} from-chat?]] + (wallet/open-send-transaction-modal cofx id method from-chat?))) + +(handlers/register-handler-fx + :wallet/update-gas-price-success + (fn [cofx [_ price edit?]] + (wallet/update-gas-price cofx price edit?))) + +(handlers/register-handler-fx + :TODO.remove/update-estimated-gas + (fn [{:keys [db]} [_ obj]] + {:update-estimated-gas {:web3 (:web3 db) + :obj obj + :success-event :wallet/update-estimated-gas-success}})) + +(handlers/register-handler-fx + :wallet/update-estimated-gas-success + (fn [cofx [_ gas]] + (wallet/update-estimated-gas-price cofx gas))) + +(handlers/register-handler-fx + :wallet.setup.ui/navigate-back-pressed + (fn [{:keys [db] :as cofx}] + (fx/merge cofx + {:db (assoc-in db [:wallet :send-transaction] {})} + (navigation/navigate-back)))) diff --git a/src/status_im/extensions/capacities/ethereum.cljs b/src/status_im/extensions/capacities/ethereum.cljs index 8857b16743c..4de54e8de24 100644 --- a/src/status_im/extensions/capacities/ethereum.cljs +++ b/src/status_im/extensions/capacities/ethereum.cljs @@ -2,17 +2,17 @@ (:require [clojure.string :as string] [status-im.constants :as constants] [status-im.i18n :as i18n] - [status-im.models.wallet :as models.wallet] - [status-im.utils.hex :as hex] + [status-im.native-module.core :as status] [status-im.ui.screens.navigation :as navigation] [status-im.utils.ethereum.abi-spec :as abi-spec] - [status-im.utils.fx :as fx] - [status-im.utils.ethereum.ens :as ens] [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.ethereum.ens :as ens] + [status-im.utils.fx :as fx] [status-im.utils.handlers :as handlers] + [status-im.utils.hex :as hex] [status-im.utils.money :as money] [status-im.utils.types :as types] - [status-im.native-module.core :as status])) + [status-im.wallet.core :as wallet])) (handlers/register-handler-fx :extensions/wallet-ui-on-success @@ -72,7 +72,7 @@ (let [tx-object (assoc (select-keys arguments [:to :gas :gas-price :value :nonce]) :data (when (and method params) (abi-spec/encode method params))) transaction (prepare-extension-transaction tx-object (:contacts/contacts db) on-success on-failure)] - (models.wallet/open-modal-wallet-for-transaction db transaction tx-object))) + (wallet/open-modal-wallet-for-transaction db transaction tx-object))) (handlers/register-handler-fx :extensions/ethereum-send-transaction diff --git a/src/status_im/hardwallet/core.cljs b/src/status_im/hardwallet/core.cljs index 1c61b9c1771..47120b69193 100644 --- a/src/status_im/hardwallet/core.cljs +++ b/src/status_im/hardwallet/core.cljs @@ -1,24 +1,24 @@ (ns status-im.hardwallet.core - (:require [re-frame.core :as re-frame] - status-im.hardwallet.fx - [status-im.ui.screens.navigation :as navigation] - [status-im.utils.config :as config] - [status-im.utils.fx :as fx] - [status-im.utils.platform :as platform] - [taoensso.timbre :as log] - [status-im.i18n :as i18n] - [status-im.utils.types :as types] + (:require [clojure.string :as string] + [re-frame.core :as re-frame] [status-im.accounts.create.core :as accounts.create] + [status-im.accounts.login.core :as accounts.login] + [status-im.accounts.logout.core :as accounts.logout] + [status-im.accounts.recover.core :as accounts.recover] + [status-im.data-store.accounts :as accounts-store] + [status-im.i18n :as i18n] [status-im.node.core :as node] + [status-im.ui.screens.navigation :as navigation] + [status-im.utils.config :as config] [status-im.utils.datetime :as utils.datetime] - [status-im.data-store.accounts :as accounts-store] [status-im.utils.ethereum.core :as ethereum] - [clojure.string :as string] - [status-im.accounts.login.core :as accounts.login] - [status-im.accounts.recover.core :as accounts.recover] - [status-im.models.wallet :as models.wallet] [status-im.utils.ethereum.mnemonic :as mnemonic] - [status-im.accounts.logout.core :as accounts.logout])) + [status-im.utils.fx :as fx] + [status-im.utils.platform :as platform] + [status-im.utils.types :as types] + [status-im.wallet.core :as wallet] + [taoensso.timbre :as log] + status-im.hardwallet.fx)) (def default-pin "000000") @@ -54,7 +54,7 @@ (if navigate-to-browser? (fx/merge cofx {:db (assoc-in db [:hardwallet :on-card-connected] nil)} - (models.wallet/discard-transaction) + (wallet/discard-transaction) (navigation/navigate-to-cofx :browser nil)) (if (= :enter-pin-login (:view-id db)) (navigation/navigate-to-clean cofx :accounts nil) diff --git a/src/status_im/models/wallet.cljs b/src/status_im/models/wallet.cljs deleted file mode 100644 index c6d3bb6cb9d..00000000000 --- a/src/status_im/models/wallet.cljs +++ /dev/null @@ -1,285 +0,0 @@ -(ns status-im.models.wallet - (:require [clojure.set :as set] - [status-im.constants :as constants] - [status-im.i18n :as i18n] - [status-im.ui.screens.navigation :as navigation] - [status-im.ui.screens.wallet.utils :as wallet.utils] - [status-im.utils.config :as config] - [status-im.utils.core :as utils.core] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.fx :as fx] - [status-im.utils.hex :as utils.hex] - [status-im.utils.money :as money])) - -(def min-gas-price-wei (money/bignumber 1)) - -(defmulti invalid-send-parameter? (fn [type _] type)) - -(defmethod invalid-send-parameter? :gas-price [_ value] - (cond - (not value) :invalid-number - (.lt (money/->wei :gwei value) min-gas-price-wei) :not-enough-wei - (-> (money/->wei :gwei value) .decimalPlaces pos?) :invalid-number)) - -(defmethod invalid-send-parameter? :default [_ value] - (when (or (not value) - (<= value 0)) - :invalid-number)) - -(defn- calculate-max-fee - [gas gas-price] - (if (and gas gas-price) - (money/to-fixed (money/wei->ether (.times gas gas-price))) - "0")) - -(defn- edit-max-fee [edit] - (let [gas (get-in edit [:gas-price :value-number]) - gas-price (get-in edit [:gas :value-number])] - (assoc edit :max-fee (calculate-max-fee gas gas-price)))) - -(defn add-max-fee [{:keys [gas gas-price] :as transaction}] - (assoc transaction :max-fee (calculate-max-fee gas gas-price))) - -(defn build-edit [edit-value key value] - "Takes the previous edit, either :gas or :gas-price and a value as string. - Wei for gas, and gwei for gas price. - Validates them and sets max fee" - (let [bn-value (money/bignumber value) - invalid? (invalid-send-parameter? key bn-value) - data (if invalid? - {:value value - :max-fee 0 - :invalid? invalid?} - {:value value - :value-number (if (= :gas-price key) - (money/->wei :gwei bn-value) - bn-value) - :invalid? false})] - (-> edit-value - (assoc key data) - edit-max-fee))) - -(defn edit-value - [key value {:keys [db]}] - {:db (update-in db [:wallet :edit] build-edit key value)}) - -;; DAPP TRANSACTION -> SEND TRANSACTION -(defn prepare-dapp-transaction [{{:keys [id method params]} :payload message-id :message-id} contacts] - (let [{:keys [to value data gas gasPrice nonce]} (first params) - contact (get contacts (utils.hex/normalize-hex to))] - (cond-> {:id (str id) - :to-name (or (when (nil? to) - (i18n/label :t/new-contract)) - contact) - :symbol :ETH - :method method - :to to - :amount (money/bignumber (or value 0)) - :gas (cond - gas - (money/bignumber gas) - (and value (empty? data)) - (money/bignumber 21000)) - :gas-price (when gasPrice - (money/bignumber gasPrice)) - :data data - :on-result [:wallet.dapp/transaction-on-result message-id] - :on-error [:wallet.dapp/transaction-on-error message-id]} - nonce - (assoc :nonce nonce)))) - -;; SEND TRANSACTION -> RPC TRANSACTION -(defn prepare-send-transaction [from {:keys [amount to gas gas-price data nonce]}] - (cond-> {:from (ethereum/normalized-address from) - :to (ethereum/normalized-address to) - :value (ethereum/int->hex amount) - :gas (ethereum/int->hex gas) - :gasPrice (ethereum/int->hex gas-price)} - data - (assoc :data data) - nonce - (assoc :nonce nonce))) - -;; NOTE (andrey) we need this function, because params may be mixed up, so we need to figure out which one is address -;; and which message -(defn normalize-sign-message-params [params] - (let [first_param (first params) - second_param (second params) - first-param-address? (ethereum/address? first_param) - second-param-address? (ethereum/address? second_param)] - (when (or first-param-address? second-param-address?) - (if first-param-address? - [first_param second_param] - [second_param first_param])))) - -(defn web3-error-callback [fx {:keys [webview-bridge]} message-id message] - (assoc fx :browser/send-to-bridge {:message {:type constants/web3-send-async-callback - :messageId message-id - :error message} - :webview webview-bridge})) - -(defn dapp-complete-transaction [id result method message-id webview keycard?] - (cond-> {:browser/send-to-bridge {:message {:type constants/web3-send-async-callback - :messageId message-id - :result {:jsonrpc "2.0" - :id (int id) - :result result}} - :webview webview} - :dispatch [:navigate-back]} - - (constants/web3-sign-message? method) - (assoc :dispatch (if keycard? [:navigate-to :browser] [:navigate-back])) - - (= method constants/web3-send-transaction) - (assoc :dispatch [:navigate-to-clean :wallet-transaction-sent-modal]))) - -(fx/defn discard-transaction - [{:keys [db]}] - (let [{:keys [on-error]} (get-in db [:wallet :send-transaction])] - (merge {:db (update db :wallet - assoc - :send-transaction {} - :transactions-queue nil)} - (when on-error - {:dispatch (conj on-error "transaction was cancelled by user")})))) - -(defn prepare-unconfirmed-transaction [db now hash] - (let [transaction (get-in db [:wallet :send-transaction]) - all-tokens (:wallet/all-tokens db)] - (let [chain (:chain db) - token (tokens/symbol->token all-tokens (keyword chain) (:symbol transaction))] - (-> transaction - (assoc :confirmations "0" - :timestamp (str now) - :type :outbound - :hash hash - :value (:amount transaction) - :token token - :gas-limit (str (:gas transaction))) - (update :gas-price str) - (dissoc :message-id :id :gas))))) - -(fx/defn handle-transaction-error - [{:keys [db] :as cofx} {:keys [code message]}] - (let [{:keys [on-error]} (get-in db [:wallet :send-transaction])] - (case code - ;;WRONG PASSWORD - constants/send-transaction-err-decrypt - {:db (-> db - (assoc-in [:wallet :send-transaction :wrong-password?] true))} - - (fx/merge cofx - (merge {:db (-> db - (assoc-in [:wallet :transactions-queue] nil) - (assoc-in [:wallet :send-transaction] {})) - :wallet/show-transaction-error message} - (when on-error - {:dispatch (conj on-error message)})) - navigation/navigate-back)))) - -(defn clear-error-message [db error-type] - (update-in db [:wallet :errors] dissoc error-type)) - -(defn tokens-symbols [visible-token-symbols all-tokens chain] - (set/difference (set visible-token-symbols) (set (map :symbol (tokens/nfts-for all-tokens chain))))) - -(fx/defn initialize-tokens - [{:keys [db]}] - (let [network-id (get-in db [:account/account :network]) - network (get-in db [:account/account :networks network-id]) - custom-tokens (get-in db [:account/account :settings :wallet :custom-tokens]) - chain (ethereum/network->chain-keyword network)] - (merge - {:db (assoc db :wallet/all-tokens - (merge-with merge - (utils.core/map-values #(utils.core/index-by :address %) tokens/all-default-tokens) - custom-tokens))} - (when config/erc20-contract-warnings-enabled? - {:wallet/validate-tokens {:web3 (:web3 db) - :tokens (get tokens/all-default-tokens chain)}})))) - -(fx/defn update-wallet - [{{:keys [web3 network network-status] - {:keys [address settings]} :account/account :as db} :db}] - (let [all-tokens (:wallet/all-tokens db) - network (get-in db [:account/account :networks network]) - chain (ethereum/network->chain-keyword network) - mainnet? (= :mainnet chain) - assets (get-in settings [:wallet :visible-tokens chain]) - tokens (tokens-symbols (get-in settings [:wallet :visible-tokens chain]) all-tokens chain) - currency-id (or (get-in settings [:wallet :currency]) :usd) - currency (get constants/currencies currency-id)] - (when (not= network-status :offline) - {:get-balance {:web3 web3 - :account-id address - :success-event :update-balance-success - :error-event :update-balance-fail} - :get-tokens-balance {:web3 web3 - :account-id address - :symbols assets - :chain chain - :all-tokens all-tokens - :success-event :update-token-balance-success - :error-event :update-token-balance-fail} - :get-prices {:from (if mainnet? - (conj tokens "ETH") - [(-> (tokens/native-currency chain) - (wallet.utils/exchange-symbol))]) - :to [(:code currency)] - :mainnet? mainnet? - :success-event :update-prices-success - :error-event :update-prices-fail - :chaos-mode? (:chaos-mode? settings)} - :db (-> db - (clear-error-message :prices-update) - (clear-error-message :balance-update) - (assoc-in [:wallet :balance-loading?] true) - (assoc :prices-loading? true))}))) - -(defn open-modal-wallet-for-transaction [db transaction tx-object] - (let [{:keys [gas gas-price]} transaction - {:keys [wallet-set-up-passed?]} (:account/account db)] - {:db (-> db - (assoc-in [:navigation/screen-params :wallet-send-modal-stack :modal?] true) - (assoc-in [:wallet :send-transaction] transaction) - (assoc-in [:wallet :send-transaction :original-gas] gas)) - :dispatch-n [[:update-wallet] - (when-not gas - [:wallet/update-estimated-gas tx-object]) - (when-not gas-price - [:wallet/update-gas-price]) - [:navigate-to - (if wallet-set-up-passed? - :wallet-send-modal-stack - :wallet-send-modal-stack-with-onboarding)]]})) - -(fx/defn open-sign-transaction-flow - [{:keys [db] :as cofx} {:keys [gas gas-price] :as transaction}] - (let [go-to-view-id (if (get-in db [:account/account :wallet-set-up-passed?]) - :wallet-send-modal-stack - :wallet-send-modal-stack-with-onboarding)] - (fx/merge cofx - (cond-> {:db (-> db - (assoc-in [:navigation/screen-params :wallet-send-modal-stack :modal?] true) - (assoc-in [:wallet :send-transaction] - transaction) - (assoc-in [:wallet :send-transaction :original-gas] - gas))} - (not gas) - (assoc :update-estimated-gas - {:web3 (:web3 db) - :obj (select-keys transaction [:to :data]) - :success-event :wallet/update-estimated-gas-success}) - - (not gas-price) - (assoc :update-gas-price - {:web3 (:web3 db) - :success-event :wallet/update-gas-price-success - :edit? false})) - (update-wallet) - (navigation/navigate-to-cofx go-to-view-id {})))) - -(defn send-transaction-screen-did-load - [{:keys [db]}] - {:db (assoc-in db [:navigation/screen-params :wallet-send-modal-stack :modal?] false)}) diff --git a/src/status_im/stickers/core.cljs b/src/status_im/stickers/core.cljs index fccccb7a0ad..3039e2a4717 100644 --- a/src/status_im/stickers/core.cljs +++ b/src/status_im/stickers/core.cljs @@ -1,17 +1,17 @@ (ns status-im.stickers.core - (:require [status-im.utils.fx :as fx] - [cljs.reader :as edn] - [status-im.accounts.core :as accounts] - [status-im.ui.screens.navigation :as navigation] + (:require [cljs.reader :as edn] [re-frame.core :as re-frame] - [status-im.utils.multihash :as multihash] + [status-im.accounts.core :as accounts] [status-im.constants :as constants] + [status-im.ui.screens.navigation :as navigation] + [status-im.utils.ethereum.abi-spec :as abi-spec] [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.ethereum.erc20 :as erc20] [status-im.utils.ethereum.stickers :as ethereum.stickers] - [status-im.models.wallet :as models.wallet] + [status-im.utils.fx :as fx] [status-im.utils.money :as money] - [status-im.utils.ethereum.abi-spec :as abi-spec] - [status-im.utils.ethereum.erc20 :as erc20])) + [status-im.utils.multihash :as multihash] + [status-im.wallet.core :as wallet])) (fx/defn init-stickers-packs [{:keys [db]}] (let [sticker-packs (into {} (map #(let [pack (edn/read-string %)] @@ -82,7 +82,7 @@ data (abi-spec/encode "buyToken(uint256,address)" [pack-id address]) tx-object {:to (get erc20/snt-contracts chain) :data (abi-spec/encode "approveAndCall(address,uint256,bytes)" [stickers-contract price data])}] - (models.wallet/open-modal-wallet-for-transaction + (wallet/open-modal-wallet-for-transaction db (prepare-transaction "approve" tx-object [:stickers/pending-pack pack-id]) tx-object))) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index fc2cb85f180..8b9ebbccc52 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -11,11 +11,9 @@ [status-im.constants :as constants] [status-im.contact.db :as contact.db] [status-im.ethereum.transactions.core :as transactions] + [status-im.ethereum.transactions.etherscan :as transactions.etherscan] [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] [status-im.ui.screens.add-new.new-public-chat.db :as db] @@ -29,18 +27,18 @@ [status-im.utils.datetime :as datetime] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.hex :as utils.hex] [status-im.utils.identicon :as identicon] [status-im.utils.money :as money] [status-im.utils.platform :as platform] [status-im.utils.security :as security] [status-im.utils.universal-links.core :as links] + [status-im.wallet.core :as wallet] + [status-im.wallet.db :as wallet.db] status-im.tribute-to-talk.subs status-im.ui.screens.hardwallet.connect.subs status-im.ui.screens.hardwallet.settings.subs status-im.ui.screens.hardwallet.pin.subs - status-im.ui.screens.hardwallet.setup.subs - [status-im.wallet.db :as wallet.db])) + status-im.ui.screens.hardwallet.setup.subs)) ;; TOP LEVEL =========================================================================================================== @@ -1128,7 +1126,7 @@ :contact to-contact :address to)) :time-formatted (datetime/timestamp->time timestamp) - :on-touch-fn #(re-frame/dispatch [:show-transaction-details hash])))) + :on-touch-fn #(re-frame/dispatch [:wallet.ui/show-transaction-details hash])))) (defn- group-transactions-by-date [transactions] @@ -1257,12 +1255,12 @@ [transaction edit] (cond-> edit (not (get-in edit [:gas-price :value])) - (models.wallet/build-edit + (wallet/build-edit :gas-price (money/to-fixed (money/wei-> :gwei (:gas-price transaction)))) (not (get-in edit [:gas :value])) - (models.wallet/build-edit + (wallet/build-edit :gas (money/to-fixed (:gas transaction))))) @@ -1299,7 +1297,7 @@ :<- [:balance] (fn [[{:keys [amount symbol] :as transaction} balance]] (-> transaction - (models.wallet/add-max-fee) + (wallet/add-max-fee) (check-sufficient-funds balance symbol amount) (check-sufficient-gas balance symbol amount)))) diff --git a/src/status_im/ui/screens/currency_settings/models.cljs b/src/status_im/ui/screens/currency_settings/models.cljs index feed815f5da..e39200bed41 100644 --- a/src/status_im/ui/screens/currency_settings/models.cljs +++ b/src/status_im/ui/screens/currency_settings/models.cljs @@ -1,7 +1,7 @@ (ns status-im.ui.screens.currency-settings.models (:require [status-im.accounts.update.core :as accounts.update] - [status-im.models.wallet :as wallet] - [status-im.utils.fx :as fx])) + [status-im.utils.fx :as fx] + [status-im.wallet.core :as wallet])) (fx/defn set-currency [{:keys [db] :as cofx} currency] diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs index 7c1c2e13237..4dcc391c3e4 100644 --- a/src/status_im/ui/screens/events.cljs +++ b/src/status_im/ui/screens/events.cljs @@ -10,30 +10,29 @@ status-im.ui.screens.add-new.new-chat.navigation status-im.ui.screens.profile.events status-im.ui.screens.extensions.add.events - status-im.ui.screens.wallet.events status-im.ui.screens.wallet.collectibles.events status-im.ui.screens.wallet.send.events status-im.ui.screens.wallet.request.events - status-im.ui.screens.wallet.settings.events status-im.ui.screens.wallet.choose-recipient.events status-im.ui.screens.wallet.collectibles.cryptokitties.events status-im.ui.screens.wallet.collectibles.cryptostrikers.events status-im.ui.screens.wallet.collectibles.etheremon.events status-im.ui.screens.wallet.collectibles.superrare.events status-im.ui.screens.wallet.collectibles.kudos.events + status-im.ui.screens.wallet.navigation status-im.utils.keychain.events [re-frame.core :as re-frame] - [status-im.hardwallet.core :as hardwallet] [status-im.chat.models :as chat] - [status-im.native-module.core :as status] + [status-im.hardwallet.core :as hardwallet] [status-im.mailserver.core :as mailserver] + [status-im.native-module.core :as status] [status-im.ui.components.permissions :as permissions] [status-im.utils.dimensions :as dimensions] + [status-im.utils.fx :as fx] [status-im.utils.handlers :as handlers] [status-im.utils.http :as http] [status-im.utils.utils :as utils] - [status-im.utils.fx :as fx] - [status-im.models.wallet :as wallet])) + [status-im.wallet.core :as wallet])) (defn- http-get [{:keys [url response-validator success-event-creator failure-event-creator timeout-ms]}] (let [on-success #(re-frame/dispatch (success-event-creator %)) diff --git a/src/status_im/ui/screens/wallet/events.cljs b/src/status_im/ui/screens/wallet/events.cljs deleted file mode 100644 index f94fc8d7a1d..00000000000 --- a/src/status_im/ui/screens/wallet/events.cljs +++ /dev/null @@ -1,240 +0,0 @@ -(ns status-im.ui.screens.wallet.events - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.models.wallet :as models] - [status-im.ui.screens.navigation :as navigation] - status-im.ui.screens.wallet.navigation - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.erc20 :as erc20] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.fx :as fx] - [status-im.utils.handlers :as handlers] - [status-im.utils.money :as money] - [status-im.utils.prices :as prices] - [status-im.utils.utils :as utils.utils] - [taoensso.timbre :as log])) - -(defn get-balance [{:keys [web3 account-id on-success on-error]}] - (if (and web3 account-id) - (.getBalance - (.-eth web3) - account-id - (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))) - (on-error "web3 or account-id not available"))) - -(defn get-token-balance [{:keys [web3 contract account-id on-success on-error]}] - (if (and web3 contract account-id) - (erc20/balance-of - web3 - contract - (ethereum/normalized-address account-id) - (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))) - (on-error "web3, contract or account-id not available"))) - -(defn assoc-error-message [db error-type err] - (assoc-in db [:wallet :errors error-type] (or err :unknown-error))) - -;; FX - -(re-frame/reg-fx - :get-balance - (fn [{:keys [web3 account-id success-event error-event]}] - (get-balance {:web3 web3 - :account-id account-id - :on-success #(re-frame/dispatch [success-event %]) - :on-error #(re-frame/dispatch [error-event %])}))) - -(re-frame/reg-fx - :get-tokens-balance - (fn [{:keys [web3 symbols all-tokens chain account-id success-event error-event]}] - (doseq [symbol symbols] - (let [contract (:address (tokens/symbol->token all-tokens chain symbol))] - (get-token-balance {:web3 web3 - :contract contract - :account-id account-id - :on-success #(re-frame/dispatch [success-event symbol %]) - :on-error #(re-frame/dispatch [error-event symbol %])}))))) - -;; TODO(oskarth): At some point we want to get list of relevant assets to get prices for -(re-frame/reg-fx - :get-prices - (fn [{:keys [from to mainnet? success-event error-event chaos-mode?]}] - (prices/get-prices from - to - mainnet? - #(re-frame/dispatch [success-event %]) - #(re-frame/dispatch [error-event %]) - chaos-mode?))) - -(re-frame/reg-fx - :update-gas-price - (fn [{:keys [web3 success-event edit?]}] - (ethereum/gas-price web3 #(re-frame/dispatch [success-event %2 edit?])))) - -(re-frame/reg-fx - :update-estimated-gas - (fn [{:keys [web3 obj success-event]}] - (ethereum/estimate-gas-web3 web3 (clj->js obj) #(re-frame/dispatch [success-event %2])))) - -(defn- validate-token-name! [web3 {:keys [address symbol name]}] - (erc20/name web3 address #(when (and (seq %2) ;;NOTE(goranjovic): skipping check if field not set in contract - (not= name %2)) - (let [message (i18n/label :t/token-auto-validate-name-error - {:symbol symbol - :expected name - :actual %2 - :address address})] - (log/warn message) - (utils.utils/show-popup (i18n/label :t/warning) message))))) - -(defn- validate-token-symbol! [web3 {:keys [address symbol]}] - (erc20/symbol web3 address #(when (and (seq %2) ;;NOTE(goranjovic): skipping check if field not set in contract - (not= (clojure.core/name symbol) %2)) - (let [message (i18n/label :t/token-auto-validate-symbol-error - {:symbol symbol - :expected (clojure.core/name symbol) - :actual %2 - :address address})] - (log/warn message) - (utils.utils/show-popup (i18n/label :t/warning) message))))) - -(defn- validate-token-decimals! [web3 {:keys [address symbol decimals nft? skip-decimals-check?]}] - ;;NOTE(goranjovic): only skipping check if skip-decimals-check? flag is present because we can't differentiate - ;;between unset decimals and 0 decimals. - (when-not skip-decimals-check? - (erc20/decimals web3 address #(when (and %2 - (not nft?) - (not= decimals (int %2))) - (let [message (i18n/label :t/token-auto-validate-decimals-error - {:symbol symbol - :expected decimals - :actual %2 - :address address})] - (log/warn message) - (utils.utils/show-popup (i18n/label :t/warning) message)))))) - -(re-frame/reg-fx - :wallet/validate-tokens - (fn [{:keys [web3 tokens]}] - (doseq [token tokens] - (validate-token-decimals! web3 token) - (validate-token-symbol! web3 token) - (validate-token-name! web3 token)))) - -;; Handlers -(handlers/register-handler-fx - :update-wallet - (fn [cofx _] - (models/update-wallet cofx))) - -(handlers/register-handler-fx - :update-wallet-and-nav-back - (fn [cofx [_ on-close]] - (fx/merge cofx - (when on-close - {:dispatch on-close}) - (navigation/navigate-back) - (models/update-wallet)))) - -(handlers/register-handler-fx - :update-balance-success - (fn [{:keys [db]} [_ balance]] - {:db (-> db - (assoc-in [:wallet :balance :ETH] balance) - (assoc-in [:wallet :balance-loading?] false))})) - -(handlers/register-handler-fx - :update-balance-fail - (fn [{:keys [db]} [_ err]] - (log/debug "Unable to get balance: " err) - {:db (-> db - (assoc-error-message :balance-update :error-unable-to-get-balance) - (assoc-in [:wallet :balance-loading?] false))})) - -(fx/defn update-token-balance-success [{:keys [db]} symbol balance] - {:db (-> db - (assoc-in [:wallet :balance symbol] balance) - (assoc-in [:wallet :balance-loading?] false))}) - -(handlers/register-handler-fx - :update-token-balance-success - (fn [cofx [_ symbol balance]] - (update-token-balance-success cofx symbol balance))) - -(handlers/register-handler-fx - :update-token-balance-fail - (fn [{:keys [db]} [_ symbol err]] - (log/debug "Unable to get token " symbol "balance: " err) - {:db (-> db - (assoc-error-message :balance-update :error-unable-to-get-token-balance) - (assoc-in [:wallet :balance-loading?] false))})) - -(handlers/register-handler-fx - :update-prices-success - (fn [{:keys [db]} [_ prices]] - {:db (assoc db - :prices prices - :prices-loading? false)})) - -(handlers/register-handler-fx - :update-prices-fail - (fn [{:keys [db]} [_ err]] - (log/debug "Unable to get prices: " err) - {:db (-> db - (assoc-error-message :prices-update :error-unable-to-get-prices) - (assoc :prices-loading? false))})) - -(handlers/register-handler-fx - :show-transaction-details - (fn [{:keys [db]} [_ hash]] - {:db (assoc-in db [:wallet :current-transaction] hash) - :dispatch [:navigate-to :wallet-transaction-details]})) - -(handlers/register-handler-fx - :wallet/show-sign-transaction - (fn [{:keys [db]} [_ {:keys [id method]} from-chat?]] - {:db (assoc-in db [:wallet :send-transaction] {:id id - :method method - :from-chat? from-chat?}) - :dispatch [:navigate-to-clean :wallet-send-transaction-modal]})) - -(handlers/register-handler-fx - :wallet/update-gas-price-success - (fn [{:keys [db] :as cofx} [_ price edit?]] - (if edit? - (models/edit-value - :gas-price - (money/to-fixed - (money/wei-> :gwei price)) - cofx) - {:db (assoc-in db [:wallet :send-transaction :gas-price] price)}))) - -(handlers/register-handler-fx - :wallet/update-estimated-gas - (fn [{:keys [db]} [_ obj]] - {:update-estimated-gas {:web3 (:web3 db) - :obj obj - :success-event :wallet/update-estimated-gas-success}})) - -(handlers/register-handler-fx - :wallet/update-estimated-gas-success - (fn [{:keys [db]} [_ gas]] - (when gas - (let [adjusted-gas (money/bignumber (int (* gas 1.2))) - db-with-adjusted-gas (assoc-in db [:wallet :send-transaction :gas] adjusted-gas)] - {:db (if (some? (-> db :wallet :send-transaction :original-gas)) - db-with-adjusted-gas - (assoc-in db-with-adjusted-gas [:wallet :send-transaction :original-gas] adjusted-gas))})))) - -(handlers/register-handler-fx - :wallet.setup-ui/navigate-back-pressed - (fn [{:keys [db] :as cofx}] - (fx/merge cofx - {:db (assoc-in db [:wallet :send-transaction] {})} - (navigation/navigate-back)))) diff --git a/src/status_im/ui/screens/wallet/main/views.cljs b/src/status_im/ui/screens/wallet/main/views.cljs index f278e83ac2c..e4e7d02109f 100644 --- a/src/status_im/ui/screens/wallet/main/views.cljs +++ b/src/status_im/ui/screens/wallet/main/views.cljs @@ -167,11 +167,7 @@ [react/scroll-view {:end-fill-color colors/white :refresh-control (reagent/as-element - [react/refresh-control {:on-refresh (fn [_] - ;;TODO temporay fix to update balance, should be fixed - ;;properly later - (re-frame/dispatch [:wallet.ui/pull-to-refresh]) - (re-frame/dispatch [:update-wallet])) + [react/refresh-control {:on-refresh #(re-frame/dispatch [:wallet.ui/pull-to-refresh]) :tint-color :white :refreshing false}])} (if error-message diff --git a/src/status_im/ui/screens/wallet/navigation.cljs b/src/status_im/ui/screens/wallet/navigation.cljs index 2f03b1b42d3..ea606f14bb1 100644 --- a/src/status_im/ui/screens/wallet/navigation.cljs +++ b/src/status_im/ui/screens/wallet/navigation.cljs @@ -2,29 +2,7 @@ (: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] - [status-im.utils.utils :as utils])) - -(defmethod navigation/preload-data! :wallet - [db _] - ;;TODO(goranjovic) - get rid of this preload hook completely - ;;TODO(andrey) - temporary "improvement" with timeout, to fix screen rendering, wallet update should be optimized - (utils/set-timeout (fn [] - (re-frame/dispatch [:wallet.ui/pull-to-refresh]) - (re-frame/dispatch [:update-wallet])) - 500) - db) - -(defmethod navigation/preload-data! :wallet-stack - [db _] - ;;TODO(goranjovic) - get rid of this preload hook completely - ;;TODO(andrey) - temporary "improvement" with timeout, to fix screen rendering, wallet update should be optimized - (utils/set-timeout (fn [] - (re-frame/dispatch [:wallet.ui/pull-to-refresh]) - (re-frame/dispatch [:update-wallet])) - 500) - db) + [status-im.utils.ethereum.core :as ethereum])) (def transaction-send-default (let [symbol :ETH] diff --git a/src/status_im/ui/screens/wallet/onboarding/views.cljs b/src/status_im/ui/screens/wallet/onboarding/views.cljs index 913b47a8d5e..181473d3230 100644 --- a/src/status_im/ui/screens/wallet/onboarding/views.cljs +++ b/src/status_im/ui/screens/wallet/onboarding/views.cljs @@ -55,7 +55,7 @@ ^{:key "toolbar"} [wallet.components/toolbar {:transparent? true} - (actions/back-white #(re-frame/dispatch [:wallet.setup-ui/navigate-back-pressed])) + (actions/back-white #(re-frame/dispatch [:wallet.setup.ui/navigate-back-pressed])) (i18n/label :t/wallet-set-up-title)]) (defn main-panel [signing-phrase on-confirm] diff --git a/src/status_im/ui/screens/wallet/send/events.cljs b/src/status_im/ui/screens/wallet/send/events.cljs index be7ff0c9a72..53e753969f4 100644 --- a/src/status_im/ui/screens/wallet/send/events.cljs +++ b/src/status_im/ui/screens/wallet/send/events.cljs @@ -3,11 +3,9 @@ [status-im.chat.commands.sending :as commands-sending] [status-im.constants :as constants] [status-im.i18n :as i18n] - [status-im.models.wallet :as models.wallet] [status-im.native-module.core :as status] [status-im.transport.utils :as transport.utils] [status-im.ui.screens.navigation :as navigation] - [status-im.wallet.db :as wallet.db] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.ethereum.erc20 :as erc20] [status-im.utils.ethereum.tokens :as tokens] @@ -16,7 +14,9 @@ [status-im.utils.money :as money] [status-im.utils.security :as security] [status-im.utils.types :as types] - [status-im.utils.utils :as utils])) + [status-im.utils.utils :as utils] + [status-im.wallet.core :as wallet] + [status-im.wallet.db :as wallet.db])) ;;;; FX @@ -66,7 +66,7 @@ {:db (-> db (assoc-in [:wallet :send-transaction :wrong-password?] false) (assoc-in [:wallet :send-transaction :in-progress?] true)) - ::send-transaction [(models.wallet/prepare-send-transaction from transaction) + ::send-transaction [(wallet/prepare-send-transaction from transaction) all-tokens symbol chain @@ -116,7 +116,7 @@ modal-screen-was-used? (get-in db [:navigation/screen-params :wallet-send-modal-stack :modal?])] (if error ;; ERROR - (models.wallet/handle-transaction-error (assoc cofx :db db') error) + (wallet/handle-transaction-error (assoc cofx :db db') error) ;; RESULT (fx/merge cofx (merge @@ -124,7 +124,7 @@ (not (constants/web3-sign-message? method)) (assoc-in [:wallet :transactions result] - (models.wallet/prepare-unconfirmed-transaction db now result)))} + (wallet/prepare-unconfirmed-transaction db now result)))} (when on-result {:dispatch (conj on-result id result method)})) #(when (or (not on-result) @@ -168,7 +168,7 @@ db' (assoc-in db [:wallet :send-transaction :in-progress?] false)] (if error ;; ERROR - (models.wallet/handle-transaction-error (assoc cofx :db db') error) + (wallet/handle-transaction-error (assoc cofx :db db') error) ;; RESULT (fx/merge cofx {:db (-> db @@ -186,19 +186,19 @@ (handlers/register-handler-fx :wallet/discard-transaction (fn [cofx _] - (models.wallet/discard-transaction cofx))) + (wallet/discard-transaction cofx))) (handlers/register-handler-fx :wallet.dapp/transaction-on-result (fn [{db :db} [_ message-id id result method]] (let [webview (:webview-bridge db) keycard? (boolean (get-in db [:account/account :keycard-instance-uid]))] - (models.wallet/dapp-complete-transaction (int id) result method message-id webview keycard?)))) + (wallet/dapp-complete-transaction (int id) result method message-id webview keycard?)))) (handlers/register-handler-fx :wallet.dapp/transaction-on-error (fn [{db :db} [_ message-id message]] - (models.wallet/web3-error-callback {} db message-id message))) + (wallet/web3-error-callback {} db message-id message))) ;; DAPP TRANSACTIONS QUEUE ;; NOTE(andrey) We need this queue because dapp can send several transactions in a row, this is bad behaviour @@ -219,13 +219,13 @@ ;;SEND TRANSACTION (= method constants/web3-send-transaction) - (let [transaction (models.wallet/prepare-dapp-transaction queued-transaction (:contacts/contacts db))] - (models.wallet/open-modal-wallet-for-transaction db' transaction (first params))) + (let [transaction (wallet/prepare-dapp-transaction queued-transaction (:contacts/contacts db))] + (wallet/open-modal-wallet-for-transaction db' transaction (first params))) ;;SIGN MESSAGE (constants/web3-sign-message? method) (let [typed? (not= constants/web3-personal-sign method) - [address data] (models.wallet/normalize-sign-message-params params)] + [address data] (wallet/normalize-sign-message-params params)] (if (and address data) (let [signing-phrase (-> (get-in db [:account/account :signing-phrase]) (clojure.string/replace-all #" " " ")) @@ -259,7 +259,7 @@ (fn [cofx _] (fx/merge cofx (navigation/navigate-back) - (models.wallet/discard-transaction)))) + (wallet/discard-transaction)))) (defn update-gas-price ([db edit? success-event] @@ -311,7 +311,7 @@ (handlers/register-handler-fx :wallet.send/edit-value (fn [cofx [_ key value]] - (models.wallet/edit-value key value cofx))) + (wallet/edit-value key value cofx))) (handlers/register-handler-fx :wallet.send/set-gas-details @@ -333,7 +333,7 @@ (money/to-fixed (ethereum/estimate-gas (-> db :wallet :send-transaction :symbol))))] - (assoc (models.wallet/edit-value + (assoc (wallet/edit-value :gas gas-default cofx) @@ -364,9 +364,9 @@ (defn- prepare-keycard-transaction [transaction from symbol chain all-tokens] (if (= :ETH symbol) - (models.wallet/prepare-send-transaction from transaction) + (wallet/prepare-send-transaction from transaction) (let [contract (:address (tokens/symbol->token all-tokens (keyword chain) symbol)) - {:keys [gas gasPrice to from value]} (models.wallet/prepare-send-transaction from transaction)] + {:keys [gas gasPrice to from value]} (wallet/prepare-send-transaction from transaction)] (merge (ethereum/call-params contract "transfer(address,uint256)" to value) {:from from :gas gas diff --git a/src/status_im/ui/screens/wallet/settings/events.cljs b/src/status_im/ui/screens/wallet/settings/events.cljs deleted file mode 100644 index 636fe3f244e..00000000000 --- a/src/status_im/ui/screens/wallet/settings/events.cljs +++ /dev/null @@ -1,19 +0,0 @@ -(ns status-im.ui.screens.wallet.settings.events - (:require [status-im.ui.screens.wallet.settings.models :as models] - - [status-im.utils.handlers :as handlers])) - -(handlers/register-handler-fx - :wallet.settings/toggle-visible-token - (fn [cofx [_ symbol checked?]] - (models/toggle-visible-token cofx symbol checked?))) - -(handlers/register-handler-fx - :configure-token-balance-and-visibility - (fn [cofx [_ symbol balance]] - (models/configure-token-balance-and-visibility cofx symbol balance))) - -(handlers/register-handler-fx - :wallet.ui/pull-to-refresh - (fn [cofx _] - (models/wallet-autoconfig-tokens cofx))) diff --git a/src/status_im/ui/screens/wallet/settings/models.cljs b/src/status_im/ui/screens/wallet/settings/models.cljs deleted file mode 100644 index adc841190c1..00000000000 --- a/src/status_im/ui/screens/wallet/settings/models.cljs +++ /dev/null @@ -1,58 +0,0 @@ -(ns status-im.ui.screens.wallet.settings.models - (:require [re-frame.core :as re-frame] - [status-im.accounts.update.core :as accounts.update] - [status-im.ui.screens.wallet.events :as wallet.events] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.fx :as fx])) - -(defn- set-checked [ids id checked?] - (if checked? - (conj (or ids #{}) id) - (disj ids id))) - -(defn update-toggle-in-settings [{{:account/keys [account]} :db} symbol checked?] - (let [network (get (:networks account) (:network account)) - chain (ethereum/network->chain-keyword network) - settings (get account :settings)] - (update-in settings [:wallet :visible-tokens chain] #(set-checked % symbol checked?)))) - -(fx/defn toggle-visible-token [cofx symbol checked?] - (let [new-settings (update-toggle-in-settings cofx symbol checked?)] - (accounts.update/update-settings cofx new-settings {}))) - -(fx/defn add-custom-token [{{:account/keys [account]} :db :as cofx} {:keys [symbol address] :as token}] - (let [network (get (:networks account) (:network account)) - chain (ethereum/network->chain-keyword network) - settings (update-toggle-in-settings cofx symbol true) - new-settings (assoc-in settings [:wallet :custom-tokens chain address] token)] - (accounts.update/update-settings cofx new-settings {}))) - -(fx/defn remove-custom-token [{{:account/keys [account]} :db :as cofx} {:keys [symbol address]}] - (let [network (get (:networks account) (:network account)) - chain (ethereum/network->chain-keyword network) - settings (update-toggle-in-settings cofx symbol false) - new-settings (update-in settings [:wallet :custom-tokens chain] dissoc address)] - (accounts.update/update-settings cofx new-settings {}))) - -(fx/defn configure-token-balance-and-visibility [cofx symbol balance] - (fx/merge cofx - (toggle-visible-token symbol true) - ;;TODO(goranjovic): move `update-token-balance-success` function to wallet models - (wallet.events/update-token-balance-success symbol balance))) - -(fx/defn wallet-autoconfig-tokens [{:keys [db]}] - (let [{:keys [account/account web3 network-status] :wallet/keys [all-tokens]} db - network (get (:networks account) (:network account)) - chain (ethereum/network->chain-keyword network) - contracts (->> (tokens/tokens-for all-tokens chain) - (remove :hidden?))] - (when-not (= network-status :offline) - (doseq [{:keys [address symbol]} contracts] - ;;TODO(goranjovic): move `get-token-balance` function to wallet models - (wallet.events/get-token-balance {:web3 web3 - :contract address - :account-id (:address account) - :on-error #(re-frame/dispatch [:update-token-balance-fail symbol %]) - :on-success #(when (> % 0) - (re-frame/dispatch [:configure-token-balance-and-visibility symbol %]))}))))) diff --git a/src/status_im/ui/screens/wallet/settings/views.cljs b/src/status_im/ui/screens/wallet/settings/views.cljs index 351043d74c5..389c60e7e7a 100644 --- a/src/status_im/ui/screens/wallet/settings/views.cljs +++ b/src/status_im/ui/screens/wallet/settings/views.cljs @@ -19,7 +19,8 @@ (defn toolbar [] [toolbar/toolbar nil [toolbar/nav-button - (toolbar.actions/back #(re-frame/dispatch [:update-wallet-and-nav-back]))] + (toolbar.actions/back #(re-frame/dispatch + [:wallet.settings.ui/navigate-back-pressed]))] [toolbar/content-title (i18n/label :t/wallet-assets)]]) @@ -96,7 +97,7 @@ {:style {:border-bottom-color colors/white-light-transparent}} [toolbar/nav-button (actions/back-white - #(re-frame/dispatch [:update-wallet-and-nav-back + #(re-frame/dispatch [:wallet.settings.ui/navigate-back-pressed (when (fn? on-close) (on-close (create-payload address)))]))] [toolbar/content-title {:color colors/white} diff --git a/src/status_im/utils/ethereum/contracts.cljs b/src/status_im/utils/ethereum/contracts.cljs index 51003e7037e..8cecd9f30a2 100644 --- a/src/status_im/utils/ethereum/contracts.cljs +++ b/src/status_im/utils/ethereum/contracts.cljs @@ -1,10 +1,10 @@ (ns status-im.utils.ethereum.contracts (:require [re-frame.core :as re-frame] - [status-im.models.wallet :as wallet] [status-im.utils.ethereum.abi-spec :as abi-spec] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.fx :as fx] - [status-im.utils.money :as money])) + [status-im.utils.money :as money] + [status-im.wallet.core :as wallet])) (def contracts {:status/snt diff --git a/src/status_im/wallet/core.cljs b/src/status_im/wallet/core.cljs new file mode 100644 index 00000000000..92373e0f98f --- /dev/null +++ b/src/status_im/wallet/core.cljs @@ -0,0 +1,557 @@ +(ns status-im.wallet.core + (:require [clojure.set :as set] + [re-frame.core :as re-frame] + [status-im.accounts.update.core :as accounts.update] + [status-im.constants :as constants] + [status-im.i18n :as i18n] + [status-im.ui.screens.navigation :as navigation] + [status-im.ui.screens.wallet.utils :as wallet.utils] + [status-im.utils.config :as config] + [status-im.utils.core :as utils.core] + [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.ethereum.erc20 :as erc20] + [status-im.utils.ethereum.tokens :as tokens] + [status-im.utils.fx :as fx] + [status-im.utils.hex :as utils.hex] + [status-im.utils.money :as money] + [status-im.utils.prices :as prices] + [status-im.utils.utils :as utils.utils] + [taoensso.timbre :as log])) + +(defn get-balance [{:keys [web3 account-id on-success on-error]}] + (if (and web3 account-id) + (.getBalance + (.-eth web3) + account-id + (fn [err resp] + (if-not err + (on-success resp) + (on-error err)))) + (on-error "web3 or account-id not available"))) + +(defn get-token-balance + [{:keys [web3 contract account-id on-success on-error]}] + (if (and web3 contract account-id) + (erc20/balance-of + web3 + contract + (ethereum/normalized-address account-id) + (fn [err resp] + (if-not err + (on-success resp) + (on-error err)))) + (on-error "web3, contract or account-id not available"))) + +(defn assoc-error-message [db error-type err] + (assoc-in db [:wallet :errors error-type] (or err :unknown-error))) + +(fx/defn on-update-prices-fail + [{:keys [db]} err] + (log/debug "Unable to get prices: " err) + {:db (-> db + (assoc-error-message :prices-update :error-unable-to-get-prices) + (assoc :prices-loading? false))}) + +(fx/defn on-update-balance-fail + [{:keys [db]} err] + (log/debug "Unable to get balance: " err) + {:db (-> db + (assoc-error-message :balance-update :error-unable-to-get-balance) + (assoc-in [:wallet :balance-loading?] false))}) + +(fx/defn on-update-token-balance-fail + [{:keys [db]} symbol err] + (log/debug "Unable to get token " symbol "balance: " err) + {:db (-> db + (assoc-error-message :balance-update :error-unable-to-get-token-balance) + (assoc-in [:wallet :balance-loading?] false))}) + +(fx/defn open-transaction-details + [{:keys [db] :as cofx} hash] + (fx/merge cofx + {:db (assoc-in db [:wallet :current-transaction] hash)} + (navigation/navigate-to-cofx :wallet-transaction-details nil))) + +(fx/defn open-send-transaction-modal + [{:keys [db] :as cofx} id method from-chat?] + (fx/merge cofx + {:db (assoc-in db [:wallet :send-transaction] {:id id + :method method + :from-chat? from-chat?})} + (navigation/navigate-to-clean :wallet-send-transaction-modal nil))) + +;; FX + +(re-frame/reg-fx + :get-balance + (fn [{:keys [web3 account-id success-event error-event]}] + (get-balance {:web3 web3 + :account-id account-id + :on-success #(re-frame/dispatch [success-event %]) + :on-error #(re-frame/dispatch [error-event %])}))) + +(re-frame/reg-fx + :get-tokens-balance + (fn [{:keys [web3 symbols all-tokens chain account-id success-event error-event]}] + (doseq [symbol symbols] + (let [contract (:address (tokens/symbol->token all-tokens chain symbol))] + (get-token-balance {:web3 web3 + :contract contract + :account-id account-id + :on-success #(re-frame/dispatch [success-event symbol %]) + :on-error #(re-frame/dispatch [error-event symbol %])}))))) + +;; TODO(oskarth): At some point we want to get list of relevant assets to get prices for +(re-frame/reg-fx + :get-prices + (fn [{:keys [from to mainnet? success-event error-event chaos-mode?]}] + (prices/get-prices from + to + mainnet? + #(re-frame/dispatch [success-event %]) + #(re-frame/dispatch [error-event %]) + chaos-mode?))) + +(re-frame/reg-fx + :update-gas-price + (fn [{:keys [web3 success-event edit?]}] + (ethereum/gas-price web3 #(re-frame/dispatch [success-event %2 edit?])))) + +(re-frame/reg-fx + :update-estimated-gas + (fn [{:keys [web3 obj success-event]}] + (ethereum/estimate-gas-web3 web3 (clj->js obj) #(re-frame/dispatch [success-event %2])))) + +(defn- validate-token-name! [web3 {:keys [address symbol name]}] + (erc20/name web3 address #(when (and (seq %2) ;;NOTE(goranjovic): skipping check if field not set in contract + (not= name %2)) + (let [message (i18n/label :t/token-auto-validate-name-error + {:symbol symbol + :expected name + :actual %2 + :address address})] + (log/warn message) + (utils.utils/show-popup (i18n/label :t/warning) message))))) + +(defn- validate-token-symbol! [web3 {:keys [address symbol]}] + (erc20/symbol web3 address #(when (and (seq %2) ;;NOTE(goranjovic): skipping check if field not set in contract + (not= (clojure.core/name symbol) %2)) + (let [message (i18n/label :t/token-auto-validate-symbol-error + {:symbol symbol + :expected (clojure.core/name symbol) + :actual %2 + :address address})] + (log/warn message) + (utils.utils/show-popup (i18n/label :t/warning) message))))) + +(defn- validate-token-decimals! [web3 {:keys [address symbol decimals nft? skip-decimals-check?]}] + ;;NOTE(goranjovic): only skipping check if skip-decimals-check? flag is present because we can't differentiate + ;;between unset decimals and 0 decimals. + (when-not skip-decimals-check? + (erc20/decimals web3 address #(when (and %2 + (not nft?) + (not= decimals (int %2))) + (let [message (i18n/label :t/token-auto-validate-decimals-error + {:symbol symbol + :expected decimals + :actual %2 + :address address})] + (log/warn message) + (utils.utils/show-popup (i18n/label :t/warning) message)))))) + +(re-frame/reg-fx + :wallet/validate-tokens + (fn [{:keys [web3 tokens]}] + (doseq [token tokens] + (validate-token-decimals! web3 token) + (validate-token-symbol! web3 token) + (validate-token-name! web3 token)))) + +(re-frame/reg-fx + :wallet/check-all-known-tokens-balance + (fn [{:keys [web3 contracts account]}] + (doseq [{:keys [address symbol]} contracts] + ;;TODO(goranjovic): move `get-token-balance` function to wallet models + (get-token-balance {:web3 web3 + :contract address + :account-id (:address account) + :on-error #(re-frame/dispatch [:update-token-balance-fail symbol %]) + :on-success #(when (> % 0) + (re-frame/dispatch [:wallet/token-found symbol %]))})))) + +(fx/defn wallet-autoconfig-tokens + [{:keys [db] :as cofx}] + (let [{:keys [account/account web3 network-status] :wallet/keys [all-tokens]} db + network (get (:networks account) (:network account)) + chain (ethereum/network->chain-keyword network) + contracts (->> (tokens/tokens-for all-tokens chain) + (remove :hidden?)) + settings (:settings account) + assets (get-in settings [:wallet :visible-tokens chain])] + (when-not (or (= network-status :offline) + assets) + (let [new-settings (assoc-in settings + [:wallet :visible-tokens chain] + #{})] + (fx/merge cofx + {:wallet/check-all-known-tokens-balance {:web3 web3 + :contracts contracts + :account account}} + (accounts.update/update-settings new-settings {})))))) + +(def min-gas-price-wei (money/bignumber 1)) + +(defmulti invalid-send-parameter? (fn [type _] type)) + +(defmethod invalid-send-parameter? :gas-price [_ value] + (cond + (not value) :invalid-number + (.lt (money/->wei :gwei value) min-gas-price-wei) :not-enough-wei + (-> (money/->wei :gwei value) .decimalPlaces pos?) :invalid-number)) + +(defmethod invalid-send-parameter? :default [_ value] + (when (or (not value) + (<= value 0)) + :invalid-number)) + +(defn- calculate-max-fee + [gas gas-price] + (if (and gas gas-price) + (money/to-fixed (money/wei->ether (.times gas gas-price))) + "0")) + +(defn- edit-max-fee [edit] + (let [gas (get-in edit [:gas-price :value-number]) + gas-price (get-in edit [:gas :value-number])] + (assoc edit :max-fee (calculate-max-fee gas gas-price)))) + +(defn add-max-fee [{:keys [gas gas-price] :as transaction}] + (assoc transaction :max-fee (calculate-max-fee gas gas-price))) + +(defn build-edit [edit-value key value] + "Takes the previous edit, either :gas or :gas-price and a value as string. + Wei for gas, and gwei for gas price. + Validates them and sets max fee" + (let [bn-value (money/bignumber value) + invalid? (invalid-send-parameter? key bn-value) + data (if invalid? + {:value value + :max-fee 0 + :invalid? invalid?} + {:value value + :value-number (if (= :gas-price key) + (money/->wei :gwei bn-value) + bn-value) + :invalid? false})] + (-> edit-value + (assoc key data) + edit-max-fee))) + +(defn edit-value + [key value {:keys [db]}] + {:db (update-in db [:wallet :edit] build-edit key value)}) + +;; DAPP TRANSACTION -> SEND TRANSACTION +(defn prepare-dapp-transaction [{{:keys [id method params]} :payload message-id :message-id} contacts] + (let [{:keys [to value data gas gasPrice nonce]} (first params) + contact (get contacts (utils.hex/normalize-hex to))] + (cond-> {:id (str id) + :to-name (or (when (nil? to) + (i18n/label :t/new-contract)) + contact) + :symbol :ETH + :method method + :to to + :amount (money/bignumber (or value 0)) + :gas (cond + gas + (money/bignumber gas) + (and value (empty? data)) + (money/bignumber 21000)) + :gas-price (when gasPrice + (money/bignumber gasPrice)) + :data data + :on-result [:wallet.dapp/transaction-on-result message-id] + :on-error [:wallet.dapp/transaction-on-error message-id]} + nonce + (assoc :nonce nonce)))) + +;; SEND TRANSACTION -> RPC TRANSACTION +(defn prepare-send-transaction [from {:keys [amount to gas gas-price data nonce]}] + (cond-> {:from (ethereum/normalized-address from) + :to (ethereum/normalized-address to) + :value (ethereum/int->hex amount) + :gas (ethereum/int->hex gas) + :gasPrice (ethereum/int->hex gas-price)} + data + (assoc :data data) + nonce + (assoc :nonce nonce))) + +;; NOTE (andrey) we need this function, because params may be mixed up, so we need to figure out which one is address +;; and which message +(defn normalize-sign-message-params [params] + (let [first_param (first params) + second_param (second params) + first-param-address? (ethereum/address? first_param) + second-param-address? (ethereum/address? second_param)] + (when (or first-param-address? second-param-address?) + (if first-param-address? + [first_param second_param] + [second_param first_param])))) + +(defn web3-error-callback [fx {:keys [webview-bridge]} message-id message] + (assoc fx :browser/send-to-bridge {:message {:type constants/web3-send-async-callback + :messageId message-id + :error message} + :webview webview-bridge})) + +(defn dapp-complete-transaction [id result method message-id webview keycard?] + (cond-> {:browser/send-to-bridge {:message {:type constants/web3-send-async-callback + :messageId message-id + :result {:jsonrpc "2.0" + :id (int id) + :result result}} + :webview webview} + :dispatch [:navigate-back]} + + (constants/web3-sign-message? method) + (assoc :dispatch (if keycard? [:navigate-to :browser] [:navigate-back])) + + (= method constants/web3-send-transaction) + (assoc :dispatch [:navigate-to-clean :wallet-transaction-sent-modal]))) + +(fx/defn discard-transaction + [{:keys [db]}] + (let [{:keys [on-error]} (get-in db [:wallet :send-transaction])] + (merge {:db (update db :wallet + assoc + :send-transaction {} + :transactions-queue nil)} + (when on-error + {:dispatch (conj on-error "transaction was cancelled by user")})))) + +(defn prepare-unconfirmed-transaction [db now hash] + (let [transaction (get-in db [:wallet :send-transaction]) + all-tokens (:wallet/all-tokens db)] + (let [chain (:chain db) + token (tokens/symbol->token all-tokens (keyword chain) (:symbol transaction))] + (-> transaction + (assoc :confirmations "0" + :timestamp (str now) + :type :outbound + :hash hash + :value (:amount transaction) + :token token + :gas-limit (str (:gas transaction))) + (update :gas-price str) + (dissoc :message-id :id :gas))))) + +(fx/defn handle-transaction-error + [{:keys [db] :as cofx} {:keys [code message]}] + (let [{:keys [on-error]} (get-in db [:wallet :send-transaction])] + (case code + ;;WRONG PASSWORD + constants/send-transaction-err-decrypt + {:db (-> db + (assoc-in [:wallet :send-transaction :wrong-password?] true))} + + (fx/merge cofx + (merge {:db (-> db + (assoc-in [:wallet :transactions-queue] nil) + (assoc-in [:wallet :send-transaction] {})) + :wallet/show-transaction-error message} + (when on-error + {:dispatch (conj on-error message)})) + navigation/navigate-back)))) + +(defn clear-error-message [db error-type] + (update-in db [:wallet :errors] dissoc error-type)) + +(defn tokens-symbols [visible-token-symbols all-tokens chain] + (set/difference (set visible-token-symbols) (set (map :symbol (tokens/nfts-for all-tokens chain))))) + +(fx/defn initialize-tokens + [{:keys [db] :as cofx}] + (let [network-id (get-in db [:account/account :network]) + network (get-in db [:account/account :networks network-id]) + custom-tokens (get-in db [:account/account :settings :wallet :custom-tokens]) + chain (ethereum/network->chain-keyword network) + all-tokens (merge-with + merge + (utils.core/map-values #(utils.core/index-by :address %) + tokens/all-default-tokens) + custom-tokens)] + (fx/merge cofx + (merge + {:db (assoc db :wallet/all-tokens all-tokens)} + (when config/erc20-contract-warnings-enabled? + {:wallet/validate-tokens {:web3 (:web3 db) + :tokens (get tokens/all-default-tokens chain)}})) + wallet-autoconfig-tokens))) + +(fx/defn update-wallet + [{{:keys [web3 network network-status] + {:keys [address settings]} :account/account :as db} :db}] + (let [all-tokens (:wallet/all-tokens db) + network (get-in db [:account/account :networks network]) + chain (ethereum/network->chain-keyword network) + mainnet? (= :mainnet chain) + assets (get-in settings [:wallet :visible-tokens chain]) + tokens (tokens-symbols (get-in settings [:wallet :visible-tokens chain]) all-tokens chain) + currency-id (or (get-in settings [:wallet :currency]) :usd) + currency (get constants/currencies currency-id)] + (when (not= network-status :offline) + {:get-balance {:web3 web3 + :account-id address + :success-event :wallet.callback/update-balance-success + :error-event :wallet.callback/update-balance-fail} + :get-tokens-balance {:web3 web3 + :account-id address + :symbols assets + :chain chain + :all-tokens all-tokens + :success-event :wallet.callback/update-token-balance-success + :error-event :wallet.callback/update-token-balance-fail} + :get-prices {:from (if mainnet? + (conj tokens "ETH") + [(-> (tokens/native-currency chain) + (wallet.utils/exchange-symbol))]) + :to [(:code currency)] + :mainnet? mainnet? + :success-event :wallet.callback/update-prices-success + :error-event :wallet.callback/update-prices-fail + :chaos-mode? (:chaos-mode? settings)} + :db (-> db + (clear-error-message :prices-update) + (clear-error-message :balance-update) + (assoc-in [:wallet :balance-loading?] true) + (assoc :prices-loading? true))}))) + +(defn open-modal-wallet-for-transaction [db transaction tx-object] + (let [{:keys [gas gas-price]} transaction + {:keys [wallet-set-up-passed?]} (:account/account db)] + {:db (-> db + (assoc-in [:navigation/screen-params :wallet-send-modal-stack :modal?] true) + (assoc-in [:wallet :send-transaction] transaction) + (assoc-in [:wallet :send-transaction :original-gas] gas)) + :dispatch-n [[:TODO.remove/update-wallet] + (when-not gas + [:TODO.remove/update-estimated-gas tx-object]) + (when-not gas-price + [:wallet/update-gas-price]) + [:navigate-to + (if wallet-set-up-passed? + :wallet-send-modal-stack + :wallet-send-modal-stack-with-onboarding)]]})) +2 +(fx/defn open-sign-transaction-flow + [{:keys [db] :as cofx} {:keys [gas gas-price] :as transaction}] + (let [go-to-view-id (if (get-in db [:account/account :wallet-set-up-passed?]) + :wallet-send-modal-stack + :wallet-send-modal-stack-with-onboarding)] + (fx/merge cofx + (cond-> {:db (-> db + (assoc-in [:navigation/screen-params :wallet-send-modal-stack :modal?] true) + (assoc-in [:wallet :send-transaction] + transaction) + (assoc-in [:wallet :send-transaction :original-gas] + gas))} + (not gas) + (assoc :update-estimated-gas + {:web3 (:web3 db) + :obj (select-keys transaction [:to :data]) + :success-event :wallet/update-estimated-gas-success}) + + (not gas-price) + (assoc :update-gas-price + {:web3 (:web3 db) + :success-event :wallet/update-gas-price-success + :edit? false})) + (update-wallet) + (navigation/navigate-to-cofx go-to-view-id {})))) + +(defn send-transaction-screen-did-load + [{:keys [db]}] + {:db (assoc-in db + [:navigation/screen-params :wallet-send-modal-stack :modal?] + false)}) + +(defn- set-checked [ids id checked?] + (if checked? + (conj (or ids #{}) id) + (disj ids id))) + +(fx/defn update-prices + [{:keys [db]} prices] + {:db (assoc db + :prices prices + :prices-loading? false)}) + +(fx/defn update-balance + [{:keys [db]} balance] + {:db (-> db + (assoc-in [:wallet :balance :ETH] balance) + (assoc-in [:wallet :balance-loading?] false))}) + +(fx/defn update-token-balance + [{:keys [db]} symbol balance] + {:db (-> db + (assoc-in [:wallet :balance symbol] balance) + (assoc-in [:wallet :balance-loading?] false))}) + +(fx/defn update-gas-price + [{:keys [db] :as cofx} price edit?] + (if edit? + (edit-value + :gas-price + (money/to-fixed + (money/wei-> :gwei price)) + cofx) + {:db (assoc-in db [:wallet :send-transaction :gas-price] price)})) + +(fx/defn update-estimated-gas-price + [{:keys [db]} gas] + (when gas + (let [adjusted-gas (money/bignumber (int (* gas 1.2))) + db-with-adjusted-gas (assoc-in db + [:wallet :send-transaction :gas] + adjusted-gas)] + {:db (if (some? (-> db :wallet :send-transaction :original-gas)) + db-with-adjusted-gas + (assoc-in db-with-adjusted-gas + [:wallet :send-transaction :original-gas] + adjusted-gas))}))) + +(defn update-toggle-in-settings + [{{:account/keys [account]} :db} symbol checked?] + (let [network (get (:networks account) (:network account)) + chain (ethereum/network->chain-keyword network) + settings (get account :settings)] + (update-in settings [:wallet :visible-tokens chain] #(set-checked % symbol checked?)))) + +(fx/defn toggle-visible-token + [cofx symbol checked?] + (let [new-settings (update-toggle-in-settings cofx symbol checked?)] + (accounts.update/update-settings cofx new-settings {}))) + +(fx/defn add-custom-token + [{{:account/keys [account]} :db :as cofx} {:keys [symbol address] :as token}] + (let [network (get (:networks account) (:network account)) + chain (ethereum/network->chain-keyword network) + settings (update-toggle-in-settings cofx symbol true) + new-settings (assoc-in settings [:wallet :custom-tokens chain address] token)] + (accounts.update/update-settings cofx new-settings {}))) + +(fx/defn remove-custom-token [{{:account/keys [account]} :db :as cofx} {:keys [symbol address]}] + (let [network (get (:networks account) (:network account)) + chain (ethereum/network->chain-keyword network) + settings (update-toggle-in-settings cofx symbol false) + new-settings (update-in settings [:wallet :custom-tokens chain] dissoc address)] + (accounts.update/update-settings cofx new-settings {}))) + +(fx/defn configure-token-balance-and-visibility [cofx symbol balance] + (fx/merge cofx + (toggle-visible-token symbol true) + ;;TODO(goranjovic): move `update-token-balance-success` function to wallet models + (update-token-balance symbol balance))) diff --git a/src/status_im/wallet/custom_tokens/core.cljs b/src/status_im/wallet/custom_tokens/core.cljs index 1ef3793436c..85fa604a15e 100644 --- a/src/status_im/wallet/custom_tokens/core.cljs +++ b/src/status_im/wallet/custom_tokens/core.cljs @@ -8,7 +8,7 @@ [clojure.string :as string] [status-im.ethereum.decode :as decode] [status-im.utils.fx :as fx] - [status-im.ui.screens.wallet.settings.models :as models])) + [status-im.wallet.core :as wallet])) (re-frame/reg-fx :wallet.custom-token/get-decimals @@ -111,12 +111,12 @@ new-token {:address contract :name name :symbol symbol :custom? true :decimals (int decimals) :color (rand-nth colors/chat-colors)}] (fx/merge (assoc-in cofx [:db :wallet/all-tokens chain-key contract] new-token) - (models/add-custom-token new-token)))) + (wallet/add-custom-token new-token)))) (fx/defn remove-custom-token [{:keys [db] :as cofx} {:keys [address] :as token}] (let [chain-key (ethereum/get-chain-keyword db)] (fx/merge (update-in cofx [:db :wallet/all-tokens chain-key] dissoc address) - (models/remove-custom-token token)))) + (wallet/remove-custom-token token)))) (fx/defn field-is-edited [{:keys [db] :as cofx} field-key value] (case field-key @@ -133,4 +133,4 @@ :error-symbol (when (field-exists? db field-key (keyword value)) (i18n/label :t/you-already-have-an-asset {:value value}))})} - :decimals {:db (assoc-in db [:wallet/custom-token-screen :decimals] value)})) \ No newline at end of file + :decimals {:db (assoc-in db [:wallet/custom-token-screen :decimals] value)})) diff --git a/test/cljs/status_im/test/models/wallet.cljs b/test/cljs/status_im/test/models/wallet.cljs index b9872d61dfb..f36d1bb655b 100644 --- a/test/cljs/status_im/test/models/wallet.cljs +++ b/test/cljs/status_im/test/models/wallet.cljs @@ -1,31 +1,31 @@ (ns status-im.test.models.wallet (:require [cljs.test :refer-macros [deftest is testing]] [status-im.utils.money :as money] - [status-im.models.wallet :as model])) + [status-im.wallet.core :as wallet])) (deftest valid-min-gas-price-test (testing "not an number" - (is (= :invalid-number (model/invalid-send-parameter? :gas-price nil)))) + (is (= :invalid-number (wallet/invalid-send-parameter? :gas-price nil)))) (testing "a number less than the minimum" - (is (= :not-enough-wei (model/invalid-send-parameter? :gas-price (money/bignumber "0.0000000001"))))) + (is (= :not-enough-wei (wallet/invalid-send-parameter? :gas-price (money/bignumber "0.0000000001"))))) (testing "a number greater than the mininum" - (is (not (model/invalid-send-parameter? :gas-price 3)))) + (is (not (wallet/invalid-send-parameter? :gas-price 3)))) (testing "the minimum" - (is (not (model/invalid-send-parameter? :gas-price (money/bignumber "0.000000001")))))) + (is (not (wallet/invalid-send-parameter? :gas-price (money/bignumber "0.000000001")))))) (deftest valid-gas (testing "not an number" - (is (= :invalid-number (model/invalid-send-parameter? :gas nil)))) + (is (= :invalid-number (wallet/invalid-send-parameter? :gas nil)))) (testing "0" - (is (= :invalid-number (model/invalid-send-parameter? :gas 0)))) + (is (= :invalid-number (wallet/invalid-send-parameter? :gas 0)))) (testing "a number" - (is (not (model/invalid-send-parameter? :gas 1))))) + (is (not (wallet/invalid-send-parameter? :gas 1))))) (deftest build-edit-test (testing "an invalid edit" (let [actual (-> {} - (model/build-edit :gas "invalid") - (model/build-edit :gas-price "0.00000000001"))] + (wallet/build-edit :gas "invalid") + (wallet/build-edit :gas-price "0.00000000001"))] (testing "it marks gas-price as invalid" (is (get-in actual [:gas-price :invalid?]))) (testing "it does not change value" @@ -38,13 +38,13 @@ (is (= "0" (:max-fee actual)))))) (testing "gas price in wei should be round" (let [actual (-> {} - (model/build-edit :gas "21000") - (model/build-edit :gas-price "0.0000000023"))] + (wallet/build-edit :gas "21000") + (wallet/build-edit :gas-price "0.0000000023"))] (is (get-in actual [:gas-price :invalid?])))) (testing "an valid edit" (let [actual (-> {} - (model/build-edit :gas "21000") - (model/build-edit :gas-price "10"))] + (wallet/build-edit :gas "21000") + (wallet/build-edit :gas-price "10"))] (testing "it does not mark gas-price as invalid" (is (not (get-in actual [:gas-price :invalid?])))) (testing "it sets the value in number for gas-price, in gwei" From fc4c772c0b7ff44162846bb1af78518fcada2f65 Mon Sep 17 00:00:00 2001 From: yenda Date: Sun, 19 May 2019 21:47:54 +0200 Subject: [PATCH 6/8] [refactoring] remove web3, clean up wallet effects - introduce json-rpc namespace, which provides `call` and `eth-call`, a generic way of calling a json-rpc method taking care of conversions and error handling - remove web3 usage from wallet - clean up effects, reducing the amount of computations when login in --- src/status_im/accounts/login/core.cljs | 5 +- .../chat/commands/impl/transactions.cljs | 6 +- src/status_im/ethereum/contracts.cljs | 61 +++ src/status_im/ethereum/json_rpc.cljs | 74 +++ src/status_im/ethereum/subscriptions.cljs | 51 +-- src/status_im/ethereum/transactions/core.cljs | 168 +++---- src/status_im/events.cljs | 22 +- src/status_im/node/core.cljs | 5 - src/status_im/protocol/core.cljs | 1 - src/status_im/stickers/core.cljs | 8 +- src/status_im/subs.cljs | 8 +- src/status_im/tribute_to_talk/core.cljs | 2 +- .../ui/screens/currency_settings/models.cljs | 2 +- src/status_im/ui/screens/db.cljs | 2 - .../wallet/choose_recipient/events.cljs | 6 +- .../ui/screens/wallet/send/events.cljs | 19 +- src/status_im/utils/ethereum/contracts.cljs | 77 ---- src/status_im/utils/ethereum/core.cljs | 75 +-- src/status_im/utils/ethereum/erc20.cljs | 174 ------- src/status_im/wallet/core.cljs | 428 +++++++++--------- src/status_im/web3/core.cljs | 7 - src/status_im/web3/events.cljs | 4 - test/cljs/status_im/test/sign_in/flow.cljs | 7 +- .../ui/screens/currency_settings/models.cljs | 2 +- 24 files changed, 499 insertions(+), 715 deletions(-) create mode 100644 src/status_im/ethereum/contracts.cljs create mode 100644 src/status_im/ethereum/json_rpc.cljs delete mode 100644 src/status_im/utils/ethereum/contracts.cljs delete mode 100644 src/status_im/utils/ethereum/erc20.cljs diff --git a/src/status_im/accounts/login/core.cljs b/src/status_im/accounts/login/core.cljs index 44882bd2d69..5a3d1d6f326 100644 --- a/src/status_im/accounts/login/core.cljs +++ b/src/status_im/accounts/login/core.cljs @@ -85,9 +85,10 @@ (fx/defn initialize-wallet [cofx] (fx/merge cofx (wallet/initialize-tokens) + (wallet/update-balances) + (wallet/update-prices) (transactions/initialize) - (ethereum.subscriptions/initialize) - (wallet/update-wallet))) + (ethereum.subscriptions/initialize))) (fx/defn user-login [{:keys [db] :as cofx} create-database?] (let [{:keys [address password]} (accounts.db/credentials cofx)] diff --git a/src/status_im/chat/commands/impl/transactions.cljs b/src/status_im/chat/commands/impl/transactions.cljs index fdd73ac25e8..160fefe4699 100644 --- a/src/status_im/chat/commands/impl/transactions.cljs +++ b/src/status_im/chat/commands/impl/transactions.cljs @@ -333,9 +333,9 @@ ;; TODO(janherich) - refactor wallet send events, updating gas price ;; is generic thing which shouldn't be defined in wallet.send, then ;; we can include the utility helper without running into circ-dep problem - :update-gas-price {:web3 (:web3 db) - :success-event :wallet/update-gas-price-success - :edit? false}} + :wallet/update-gas-price + {:success-event :wallet/update-gas-price-success + :edit? false}} (navigation/navigate-to-cofx next-view-id {})))) protocol/EnhancedParameters (enhance-send-parameters [_ parameters cofx] diff --git a/src/status_im/ethereum/contracts.cljs b/src/status_im/ethereum/contracts.cljs new file mode 100644 index 00000000000..517ca76fbbf --- /dev/null +++ b/src/status_im/ethereum/contracts.cljs @@ -0,0 +1,61 @@ +(ns status-im.ethereum.contracts + (:require [re-frame.core :as re-frame] + [status-im.utils.ethereum.abi-spec :as abi-spec] + [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.fx :as fx] + [status-im.utils.money :as money] + [status-im.wallet.core :as wallet])) + +(def contracts + {:status/tribute-to-talk + {:address + {:mainnet nil + :testnet "0x3da3fc53e24707f36c5b4433b442e896c4955f0e" + :rinkeby nil} + :methods + {:get-manifest + {:signature "getManifest(address)" + :outputs ["bytes"]} + :set-manifest + {:signature "setManifest(bytes)" + :write? true}}}}) + +(re-frame/reg-fx + ::call + (fn [{:keys [address data callback]}] + (ethereum/call {:to address + :data data} + callback))) + +(defn get-contract-address + [db contract] + (let [chain-keyword (-> (get-in db [:account/account :networks (:network db)]) + ethereum/network->chain-keyword)] + (get-in contracts [contract :address chain-keyword]))) + +(fx/defn call + [{:keys [db] :as cofx} + {:keys [contract contract-address method params + callback on-result on-error details]}] + (when-let [contract-address (or contract-address + (get-contract-address db contract))] + (let [{:keys [signature outputs write?]} + (get-in contracts [contract :methods method]) + data (abi-spec/encode signature params)] + (if write? + (wallet/open-sign-transaction-flow + cofx + (merge {:to contract-address + :data data + :id "approve" + :symbol :ETH + :method "eth_sendTransaction" + :amount (money/bignumber 0) + :on-result on-result + :on-error on-error} + details)) + {::call {:address contract-address + :data data + :callback #(callback (if (empty? outputs) + % + (abi-spec/decode % outputs)))}})))) diff --git a/src/status_im/ethereum/json_rpc.cljs b/src/status_im/ethereum/json_rpc.cljs new file mode 100644 index 00000000000..250ae942f31 --- /dev/null +++ b/src/status_im/ethereum/json_rpc.cljs @@ -0,0 +1,74 @@ +(ns status-im.ethereum.json-rpc + (:require [clojure.string :as string] + [re-frame.core :as re-frame] + [status-im.ethereum.decode :as decode] + [status-im.native-module.core :as status] + [status-im.utils.ethereum.abi-spec :as abi-spec] + [status-im.utils.money :as money] + [status-im.utils.types :as types] + [taoensso.timbre :as log])) + +(def json-rpc-api + {"eth_call" {} + "eth_getBalance" + {:on-result money/bignumber} + "eth_estimateGas" + {:on-result money/bignumber} + "eth_gasPrice" + {:on-result money/bignumber} + "eth_getBlockByHash" + {:on-result #(-> (update % :number decode/uint) + (update :timestamp decode/uint))} + "eth_getTransactionByHash" {} + "eth_getTransactionReceipt" {} + "eth_newBlockFilter" {:subscription? true} + "eth_newFilter" {:subscription? true}}) + +(defn call + [{:keys [method params on-success on-error]}] + (when-let [method-options (json-rpc-api method)] + (let [{:keys [id on-result subscription?] + :or {on-result identity + id 1 + params []}} method-options + on-error (or on-error + #(log/warn :json-rpc/error method :params params :error %))] + (if (nil? method) + (log/error :json-rpc/method-not-found method) + (status/call-private-rpc + (types/clj->json {:jsonrpc "2.0" + :id id + :method (if subscription? + "eth_subscribeSignal" + method) + :params (if subscription? + [method params] + params)}) + (fn [response] + (if (string/blank? response) + (on-error {:message "Blank response"}) + (let [{:keys [error result] :as response2} (types/json->clj response)] + (if error + (on-error error) + (if subscription? + (re-frame/dispatch + [:ethereum.callback/subscription-success + result on-success]) + (on-success (on-result result)))))))))))) + +(defn eth-call + [{:keys [contract method params outputs on-success on-error block] + :or {block "latest" + params []}}] + (call {:method "eth_call" + :params [{:to contract + :data (abi-spec/encode method params)} + (if (int? block) + (abi-spec/number-to-hex block) + block)] + :on-success + (if outputs + #(on-success (abi-spec/decode % outputs)) + on-success) + :on-error + on-error})) diff --git a/src/status_im/ethereum/subscriptions.cljs b/src/status_im/ethereum/subscriptions.cljs index 842f8671702..ac25fa915ff 100644 --- a/src/status_im/ethereum/subscriptions.cljs +++ b/src/status_im/ethereum/subscriptions.cljs @@ -8,7 +8,8 @@ [status-im.utils.ethereum.tokens :as tokens] [status-im.utils.fx :as fx] [status-im.utils.types :as types] - [taoensso.timbre :as log])) + [taoensso.timbre :as log] + [status-im.ethereum.json-rpc :as json-rpc])) (fx/defn handle-signal [cofx {:keys [subscription_id data] :as event}] @@ -56,33 +57,14 @@ ;; from etherscan (transactions/initialize)))))) -(defn subscribe-signal - [filter params callback] - (status/call-private-rpc - (types/clj->json {:jsonrpc "2.0" - :id 1 - :method "eth_subscribeSignal" - :params [filter params]}) - (fn [response] - (if (string/blank? response) - (log/error ::subscription-unknown-error :filter filter :params params) - (let [{:keys [error result]} - (-> (.parse js/JSON response) - (js->clj :keywordize-keys true))] - (if error - (log/error ::subscription-error error :filter filter :params params) - (re-frame/dispatch [:ethereum.callback/subscription-success - result - callback]))))))) - (defn new-token-transaction-filter [{:keys [chain-tokens from to] :as args}] - (subscribe-signal - "eth_newFilter" - [{:fromBlock "latest" - :toBlock "latest" - :topics [constants/event-transfer-hash from to]}] - (transactions/inbound-token-transfer-handler chain-tokens))) + (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 @@ -95,13 +77,16 @@ (defn new-block-filter [] - (subscribe-signal - "eth_newBlockFilter" [] - (fn [[block-hash]] - (transactions/get-block-by-hash - block-hash - (fn [block] - (re-frame/dispatch [:ethereum.signal/new-block block])))))) + (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 diff --git a/src/status_im/ethereum/transactions/core.cljs b/src/status_im/ethereum/transactions/core.cljs index fe9abff8756..3ad90681631 100644 --- a/src/status_im/ethereum/transactions/core.cljs +++ b/src/status_im/ethereum/transactions/core.cljs @@ -1,68 +1,22 @@ (ns status-im.ethereum.transactions.core - (:require [clojure.string :as string] - [re-frame.core :as re-frame] + (:require [re-frame.core :as re-frame] [status-im.constants :as constants] [status-im.ethereum.decode :as decode] + [status-im.ethereum.json-rpc :as json-rpc] [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.fx :as fx] - [status-im.utils.types :as types] - [status-im.wallet.core :as wallet] - [taoensso.timbre :as log])) + [status-im.utils.money :as money] + [status-im.wallet.core :as wallet])) (def confirmations-count-threshold 12) -(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))))))) - -(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)))))) - -(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)))))) - (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- remove-padding [address] + {:pre [(string? address)]} + (str "0x" (subs address 26))) (def default-erc20-token {:symbol :ERC20 @@ -80,7 +34,7 @@ :symbol symbol :from (remove-padding from) :to (remove-padding to) - :value (ethereum/hex->bignumber data) + :value (money/bignumber data) :type direction :token token :error? false @@ -96,31 +50,33 @@ [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 - :from from - :to to - :type direction - :value (str (decode/uint value))}))]))))) + (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))}))])))})) (re-frame/reg-fx :ethereum.transactions/enrich-transactions-from-new-blocks @@ -153,28 +109,34 @@ ;; 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))))]))))))))))))) + (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))))]))}))}))))}))))) ;; ----------------------------------------------- ;; transactions api @@ -184,7 +146,7 @@ [{:keys [db] :as cofx} {:keys [hash] :as transaction}] (fx/merge cofx {:db (assoc-in db [:wallet :transactions hash] transaction)} - wallet/update-wallet)) + wallet/update-balances)) (fx/defn handle-history [{:keys [db] :as cofx} transactions] @@ -192,7 +154,7 @@ {:db (update-in db [:wallet :transactions] #(merge transactions %))} - wallet/update-wallet)) + wallet/update-balances)) (fx/defn handle-token-history [{:keys [db]} transactions] diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index e30456593a4..ea005a5fe9f 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -979,11 +979,6 @@ (fn [cofx [_ error sync]] (web3/update-syncing-progress cofx error sync))) -(handlers/register-handler-fx - :web3.callback/get-block-number - (fn [cofx [_ error block-number]] - (node/update-block-number cofx error block-number))) - ;; notifications module (handlers/register-handler-fx @@ -2152,7 +2147,7 @@ (handlers/register-handler-fx :wallet.ui/pull-to-refresh (fn [cofx _] - (wallet/update-wallet cofx))) + (wallet/update-prices cofx))) (handlers/register-handler-fx :wallet.transactions/add-filter @@ -2180,11 +2175,6 @@ (fn [cofx [_ symbol balance]] (wallet/configure-token-balance-and-visibility cofx symbol balance))) -(handlers/register-handler-fx - :TODO.remove/update-wallet - (fn [cofx _] - (wallet/update-wallet cofx))) - (handlers/register-handler-fx :wallet.settings.ui/navigate-back-pressed (fn [cofx [_ on-close]] @@ -2192,7 +2182,7 @@ (when on-close {:dispatch on-close}) (navigation/navigate-back) - (wallet/update-wallet)))) + (wallet/update-balances)))) (handlers/register-handler-fx :wallet.callback/update-balance-success @@ -2217,7 +2207,7 @@ (handlers/register-handler-fx :wallet.callback/update-prices-success (fn [cofx [_ prices]] - (wallet/update-prices cofx prices))) + (wallet/on-update-prices-success cofx prices))) (handlers/register-handler-fx :wallet.callback/update-prices-fail @@ -2242,9 +2232,9 @@ (handlers/register-handler-fx :TODO.remove/update-estimated-gas (fn [{:keys [db]} [_ obj]] - {:update-estimated-gas {:web3 (:web3 db) - :obj obj - :success-event :wallet/update-estimated-gas-success}})) + {:wallet/update-estimated-gas + {:obj obj + :success-event :wallet/update-estimated-gas-success}})) (handlers/register-handler-fx :wallet/update-estimated-gas-success diff --git a/src/status_im/node/core.cljs b/src/status_im/node/core.cljs index 2f16f4513fa..71d1212ca41 100644 --- a/src/status_im/node/core.cljs +++ b/src/status_im/node/core.cljs @@ -157,11 +157,6 @@ {:error error} (when sync-state (js->clj sync-state :keywordize-keys true))))}) -(fx/defn update-block-number - [{:keys [db]} error block-number] - (when-not error - {:db (assoc db :node/latest-block-number block-number)})) - (fx/defn start [{:keys [db]} address] (let [network (if address diff --git a/src/status_im/protocol/core.cljs b/src/status_im/protocol/core.cljs index 4d28b482a63..35f6630383b 100644 --- a/src/status_im/protocol/core.cljs +++ b/src/status_im/protocol/core.cljs @@ -33,7 +33,6 @@ [{{:keys [web3] :as db} :db :as cofx}] (if (:account/account db) {:web3/get-syncing web3 - :web3/get-block-number web3 :utils/dispatch-later [{:ms 10000 :dispatch [:protocol/state-sync-timed-out]}]} (semaphores/free cofx :check-sync-state?))) diff --git a/src/status_im/stickers/core.cljs b/src/status_im/stickers/core.cljs index 3039e2a4717..8d83f36890e 100644 --- a/src/status_im/stickers/core.cljs +++ b/src/status_im/stickers/core.cljs @@ -6,7 +6,6 @@ [status-im.ui.screens.navigation :as navigation] [status-im.utils.ethereum.abi-spec :as abi-spec] [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.erc20 :as erc20] [status-im.utils.ethereum.stickers :as ethereum.stickers] [status-im.utils.fx :as fx] [status-im.utils.money :as money] @@ -74,13 +73,18 @@ (when on-result {:on-result on-result}) tx)) +(def snt-contracts + {:mainnet "0x744d70fdbe2ba4cf95131626614a1763df805b9e" + :testnet "0xc55cf4b03948d7ebc8b9e8bad92643703811d162" + :rinkeby nil}) + (fx/defn approve-pack [{db :db} pack-id price] (let [network (get-in db [:account/account :networks (:network db)]) address (ethereum/normalized-address (get-in db [:account/account :address])) chain (ethereum/network->chain-keyword network) stickers-contract (get ethereum.stickers/contracts chain) data (abi-spec/encode "buyToken(uint256,address)" [pack-id address]) - tx-object {:to (get erc20/snt-contracts chain) + tx-object {:to (get snt-contracts chain) :data (abi-spec/encode "approveAndCall(address,uint256,bytes)" [stickers-contract price data])}] (wallet/open-modal-wallet-for-transaction db diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 8b9ebbccc52..acd1fcbec3b 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -1273,15 +1273,17 @@ send-transaction edit))) -(defn check-sufficient-funds [transaction balance symbol amount] +(defn check-sufficient-funds + [transaction balance symbol amount] (assoc transaction :sufficient-funds? (or (nil? amount) (money/sufficient-funds? amount (get balance symbol))))) -(defn check-sufficient-gas [transaction balance symbol amount] +(defn check-sufficient-gas + [transaction balance symbol amount] (assoc transaction :sufficient-gas? (or (nil? amount) - (let [available-ether (get balance :ETH (money/bignumber 0)) + (let [available-ether (money/bignumber (get balance :ETH 0)) available-for-gas (if (= :ETH symbol) (.minus available-ether (money/bignumber amount)) available-ether)] diff --git a/src/status_im/tribute_to_talk/core.cljs b/src/status_im/tribute_to_talk/core.cljs index b33e26deddf..50afcd941ae 100644 --- a/src/status_im/tribute_to_talk/core.cljs +++ b/src/status_im/tribute_to_talk/core.cljs @@ -4,11 +4,11 @@ [re-frame.core :as re-frame] [status-im.accounts.update.core :as accounts.update] [status-im.contact.db :as contact.db] + [status-im.ethereum.contracts :as contracts] [status-im.ipfs.core :as ipfs] [status-im.tribute-to-talk.db :as tribute-to-talk.db] [status-im.ui.screens.navigation :as navigation] [status-im.utils.contenthash :as contenthash] - [status-im.utils.ethereum.contracts :as contracts] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.fx :as fx] [taoensso.timbre :as log])) diff --git a/src/status_im/ui/screens/currency_settings/models.cljs b/src/status_im/ui/screens/currency_settings/models.cljs index e39200bed41..2bf4462eb14 100644 --- a/src/status_im/ui/screens/currency_settings/models.cljs +++ b/src/status_im/ui/screens/currency_settings/models.cljs @@ -9,4 +9,4 @@ new-settings (assoc-in settings [:wallet :currency] currency)] (fx/merge cofx (accounts.update/update-settings new-settings {}) - (wallet/update-wallet)))) + (wallet/update-prices)))) diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index 050def0c904..4bbb6a34e54 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -101,7 +101,6 @@ ;; contents of eth_syncing or `nil` if the node isn't syncing now (spec/def :node/chain-sync-state (spec/nilable map?)) -(spec/def :node/latest-block-number (spec/nilable number?)) ;;;;NAVIGATION @@ -234,7 +233,6 @@ :node/restart? :node/address :node/chain-sync-state - :node/latest-block-number :universal-links/url :push-notifications/stored :browser/browsers diff --git a/src/status_im/ui/screens/wallet/choose_recipient/events.cljs b/src/status_im/ui/screens/wallet/choose_recipient/events.cljs index 72bd021dcb9..1b32c54734b 100644 --- a/src/status_im/ui/screens/wallet/choose_recipient/events.cljs +++ b/src/status_im/ui/screens/wallet/choose_recipient/events.cljs @@ -51,9 +51,9 @@ ;; NOTE(janherich) - whenever changing assets, we want to clear the previusly set amount/amount-text (defn changed-asset [{:keys [db] :as fx} old-symbol new-symbol] (-> fx - (merge {:update-gas-price {:web3 (:web3 db) - :success-event :wallet/update-gas-price-success - :edit? false}}) + (merge {:wallet/update-gas-price + {:success-event :wallet/update-gas-price-success + :edit? false}}) (assoc-in [:db :wallet :send-transaction :amount] nil) (assoc-in [:db :wallet :send-transaction :amount-text] nil) (assoc-in [:db :wallet :send-transaction :asset-error] diff --git a/src/status_im/ui/screens/wallet/send/events.cljs b/src/status_im/ui/screens/wallet/send/events.cljs index 53e753969f4..51ac18a9245 100644 --- a/src/status_im/ui/screens/wallet/send/events.cljs +++ b/src/status_im/ui/screens/wallet/send/events.cljs @@ -7,7 +7,6 @@ [status-im.transport.utils :as transport.utils] [status-im.ui.screens.navigation :as navigation] [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.erc20 :as erc20] [status-im.utils.ethereum.tokens :as tokens] [status-im.utils.fx :as fx] [status-im.utils.handlers :as handlers] @@ -25,9 +24,17 @@ (security/safe-unmask-data masked-password) on-completed)) -(defn- send-tokens [all-tokens symbol chain {:keys [from to value gas gasPrice]} on-completed masked-password] +(defn- send-tokens + [all-tokens symbol chain + {:keys [from to value gas gasPrice]} on-completed masked-password] (let [contract (:address (tokens/symbol->token all-tokens (keyword chain) symbol))] - (erc20/transfer contract from to value gas gasPrice masked-password on-completed))) + (status/send-transaction (types/clj->json + (merge (ethereum/call-params contract "transfer(address,uint256)" to value) + {:from from + :gas gas + :gasPrice gasPrice})) + (security/safe-unmask-data masked-password) + on-completed))) (re-frame/reg-fx ::send-transaction @@ -263,9 +270,9 @@ (defn update-gas-price ([db edit? success-event] - {:update-gas-price {:web3 (:web3 db) - :success-event (or success-event :wallet/update-gas-price-success) - :edit? edit?}}) + {:wallet/update-gas-price + {:success-event (or success-event :wallet/update-gas-price-success) + :edit? edit?}}) ([db edit?] (update-gas-price db edit? :wallet/update-gas-price-success)) ([db] (update-gas-price db false :wallet/update-gas-price-success))) diff --git a/src/status_im/utils/ethereum/contracts.cljs b/src/status_im/utils/ethereum/contracts.cljs deleted file mode 100644 index 8cecd9f30a2..00000000000 --- a/src/status_im/utils/ethereum/contracts.cljs +++ /dev/null @@ -1,77 +0,0 @@ -(ns status-im.utils.ethereum.contracts - (:require [re-frame.core :as re-frame] - [status-im.utils.ethereum.abi-spec :as abi-spec] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.fx :as fx] - [status-im.utils.money :as money] - [status-im.wallet.core :as wallet])) - -(def contracts - {:status/snt - {:address - {:mainnet "0x744d70fdbe2ba4cf95131626614a1763df805b9e" - :testnet "0xc55cf4b03948d7ebc8b9e8bad92643703811d162" - :rinkeby nil} - :methods - {:erc20/transfer - {:signature "transfer(address,uint256)" - :write? true}}} - :status/tribute-to-talk - {:address - {:mainnet nil - :testnet "0x3da3fc53e24707f36c5b4433b442e896c4955f0e" - :rinkeby nil} - :methods - {:get-manifest - {:signature "getManifest(address)" - :return-params ["bytes"]} - :set-manifest - {:signature "setManifest(bytes)" - :write? true}}} - :status/sticker-market - {:address - {:mainnet nil - :testnet "0x39d16CdB56b5a6a89e1A397A13Fe48034694316E" - :rinkeby nil} - :methods - {:pack-count - {:signature "packCount()" - :return-params ["uint256"]} - :pack-data - {:signature "getPackData(uint256)" - :return-params ["bytes4[]" "address" "bool" "uint256" "uint256" "bytes"]}}}}) - -(re-frame/reg-fx - ::call - (fn [{:keys [address data callback]}] - (ethereum/call {:to address - :data data} - callback))) - -(fx/defn call - [{:keys [db] :as cofx} - {:keys [contract method params callback on-result on-error details]}] - (let [chain-keyword (-> (get-in db [:account/account :networks (:network db)]) - ethereum/network->chain-keyword) - contract-address (get-in contracts [contract :address chain-keyword])] - (when contract-address - (let [{:keys [signature return-params write?]} - (get-in contracts [contract :methods method]) - data (abi-spec/encode signature params)] - (if write? - (wallet/open-sign-transaction-flow - cofx - (merge {:to contract-address - :data data - :id "approve" - :symbol :ETH - :method "eth_sendTransaction" - :amount (money/bignumber 0) - :on-result on-result - :on-error on-error} - details)) - {::call {:address contract-address - :data data - :callback #(callback (if (empty? return-params) - % - (abi-spec/decode % return-params)))}}))))) diff --git a/src/status_im/utils/ethereum/core.cljs b/src/status_im/utils/ethereum/core.cljs index 6e14acb5517..6d719a78c20 100644 --- a/src/status_im/utils/ethereum/core.cljs +++ b/src/status_im/utils/ethereum/core.cljs @@ -1,12 +1,9 @@ (ns status-im.utils.ethereum.core (:require [clojure.string :as string] - [status-im.ethereum.decode :as decode] + [status-im.ethereum.json-rpc :as json-rpc] [status-im.js-dependencies :as dependencies] - [status-im.native-module.core :as status] [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.ethereum.abi-spec :as abi-spec] - [status-im.utils.money :as money] - [taoensso.timbre :as log])) + [status-im.utils.money :as money])) ;; IDs standardized in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#list-of-chain-ids @@ -65,6 +62,12 @@ network->chain-keyword name)) +(defn chain-keyword + [db] + (let [network-id (get-in db [:account/account :network]) + network (get-in db [:account/account :networks network-id])] + (network->chain-keyword network))) + (defn sha3 ([s] (.sha3 dependencies/Web3.prototype (str s))) @@ -127,77 +130,23 @@ (apply str (take 10 (sha3 signature)))) (defn call [params callback] - (status/call-private-rpc - (.stringify js/JSON (clj->js {:jsonrpc "2.0" - :id 1 - :method "eth_call" - :params [params "latest"]})) - (fn [response] - (if (string/blank? response) - (log/warn :web3-response-error) - (callback (get (js->clj (.parse js/JSON response)) "result")))))) + (json-rpc/call + {:method "eth_call" + :params [params "latest"] + :on-success callback})) (defn call-params [contract method-sig & params] (let [data (apply format-call-params (sig->method-id method-sig) params)] {:to contract :data data})) -(defn send-transaction [web3 params cb] - (.sendTransaction (.-eth web3) (clj->js params) cb)) - (def default-transaction-gas (money/bignumber 21000)) -(defn gas-price [web3 cb] - (.getGasPrice (.-eth web3) cb)) - -(defn estimate-gas-web3 [web3 obj cb] - (try - (.estimateGas (.-eth web3) obj cb) - (catch :default _))) - (defn estimate-gas [symbol] (if (tokens/ethereum? symbol) 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-not error - (cb result) - (handle-error error))))) - -(defn get-block-info [web3 number cb] - (.getBlock (.-eth web3) number (fn [error result] - (if-not error - (cb (js->clj result :keywordize-keys true)) - (handle-error error))))) - -(defn get-transaction [transaction-hash callback] - (status/call-private-rpc - (.stringify js/JSON (clj->js {: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 - (update :gasPrice decode/uint) - (update :value decode/uint) - (update :gas decode/uint))))))) - -(defn get-transaction-receipt [web3 number cb] - (.getTransactionReceipt (.-eth web3) number (fn [error result] - (if-not error - (cb (js->clj result :keywordize-keys true)) - (handle-error error))))) - (defn address= [address1 address2] (and address1 address2 (= (normalized-address address1) diff --git a/src/status_im/utils/ethereum/erc20.cljs b/src/status_im/utils/ethereum/erc20.cljs deleted file mode 100644 index 46021820616..00000000000 --- a/src/status_im/utils/ethereum/erc20.cljs +++ /dev/null @@ -1,174 +0,0 @@ -(ns status-im.utils.ethereum.erc20 - " - Helper functions to interact with [ERC20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md) smart contract - - Example - - Contract: https://ropsten.etherscan.io/address/0x29b5f6efad2ad701952dfde9f29c960b5d6199c5#readContract - Owner: https://ropsten.etherscan.io/token/0x29b5f6efad2ad701952dfde9f29c960b5d6199c5?a=0xa7cfd581060ec66414790691681732db249502bd - - With a running node on Ropsten: - (let [web3 (:web3 @re-frame.db/app-db) - contract \"0x29b5f6efad2ad701952dfde9f29c960b5d6199c5\" - address \"0xa7cfd581060ec66414790691681732db249502bd\"] - (erc20/balance-of web3 contract address println)) - - => 29166666 - " - (:require [clojure.string :as string] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.abi-spec :as abi-spec] - [status-im.native-module.core :as status] - [status-im.utils.security :as security] - [status-im.js-dependencies :as dependencies] - [status-im.utils.types :as types]) - (:refer-clojure :exclude [name symbol])) - -(def utils dependencies/web3-utils) - -(def snt-contracts - {:mainnet "0x744d70fdbe2ba4cf95131626614a1763df805b9e" - :testnet "0xc55cf4b03948d7ebc8b9e8bad92643703811d162" - :rinkeby nil}) - -(def abi - (clj->js - [{:constant true - :inputs [] - :name "name" - :outputs [{:name "" - :type "string"}] - :payable false - :stateMutability "view" - :type "function"} - {:constant true - :inputs [] - :name "symbol" - :outputs [{:name "" - :type "string"}] - :payable false - :stateMutability "view" - :type "function"} - {:constant true - :inputs [] - :name "decimals" - :outputs [{:name "" - :type "uint8"}] - :payable false - :stateMutability "view" - :type "function"} - {:constant true - :inputs [{:name "_who" - :type "address"}] - :name "balanceOf" - :outputs [{:name "" - :type "uint256"}] - :payable false - :stateMutability "view" - :type "function"} - {:constant true - :inputs [] - :name "totalSupply" - :outputs [{:name "" - :type "uint256"}], - :payable false - :stateMutability "view" - :type "function"} - {:constant false - :inputs [{:name "_to" - :type "address"} - {:name "_value" - :type "uint256"}] - :name "transfer" - :outputs [{:name "" - :type "bool"}], - :payable false - :stateMutability "nonpayable" - :type "function"} - {:anonymous false - :inputs [{:indexed true - :name "from" - :type "address"}, - {:indexed true - :name "to" - :type "address"}, - {:indexed false - :name "value" - :type "uint256"}] - :name "Transfer" - :type "event"}])) - -(defn get-instance* [web3 contract] - (.at (.contract (.-eth web3) abi) contract)) - -(def get-instance - (memoize get-instance*)) - -(defn name [web3 contract cb] - (.name (get-instance web3 contract) cb)) - -(defn symbol [web3 contract cb] - (.symbol (get-instance web3 contract) cb)) - -(defn decimals [web3 contract cb] - (.decimals (get-instance web3 contract) cb)) - -(defn total-supply [web3 contract cb] - (.totalSupply (get-instance web3 contract) cb)) - -(defn balance-of [web3 contract address cb] - (.balanceOf (get-instance web3 contract) address cb)) - -(defn transfer [contract from to value gas gas-price masked-password on-completed] - (status/send-transaction (types/clj->json - (merge (ethereum/call-params contract "transfer(address,uint256)" to value) - {:from from - :gas gas - :gasPrice gas-price})) - (security/safe-unmask-data masked-password) - on-completed)) - -(defn transfer-from [web3 contract from-address to-address value cb] - (ethereum/call (ethereum/call-params contract - "transferFrom(address,address,uint256)" - (ethereum/normalized-address from-address) - (ethereum/normalized-address to-address) - (ethereum/int->hex value)) - #(cb (ethereum/hex->boolean %)))) - -(defn approve [web3 contract address value cb] - (ethereum/call (ethereum/call-params contract - "approve(address,uint256)" - (ethereum/normalized-address address) - (ethereum/int->hex value)) - #(cb (ethereum/hex->boolean %)))) - -(defn allowance [web3 contract owner-address spender-address cb] - (ethereum/call (ethereum/call-params contract - "allowance(address,address)" - (ethereum/normalized-address owner-address) - (ethereum/normalized-address spender-address)) - #(cb (ethereum/hex->bignumber %)))) - -(defn is-transfer? - [input-data] - (string/starts-with? input-data "0xa9059cbb")) - -(defn is-snt-contract? - [contract] - ((into #{} (vals snt-contracts)) contract)) - -(defn get-transaction - "only supports SNT for now" - [transaction-hash callback] - (ethereum/get-transaction - transaction-hash - (fn [{:keys [to input] :as result}] - (when (and result - (is-snt-contract? to) - (is-transfer? input)) - (let [[recipient snt-value] - (abi-spec/decode (subs input 10) ["address" "uint"])] - (callback (assoc result - :recipient recipient - :snt-value snt-value))))))) diff --git a/src/status_im/wallet/core.cljs b/src/status_im/wallet/core.cljs index 92373e0f98f..f1a7adba800 100644 --- a/src/status_im/wallet/core.cljs +++ b/src/status_im/wallet/core.cljs @@ -3,13 +3,13 @@ [re-frame.core :as re-frame] [status-im.accounts.update.core :as accounts.update] [status-im.constants :as constants] + [status-im.ethereum.json-rpc :as json-rpc] [status-im.i18n :as i18n] [status-im.ui.screens.navigation :as navigation] [status-im.ui.screens.wallet.utils :as wallet.utils] [status-im.utils.config :as config] [status-im.utils.core :as utils.core] [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.erc20 :as erc20] [status-im.utils.ethereum.tokens :as tokens] [status-im.utils.fx :as fx] [status-im.utils.hex :as utils.hex] @@ -18,30 +18,6 @@ [status-im.utils.utils :as utils.utils] [taoensso.timbre :as log])) -(defn get-balance [{:keys [web3 account-id on-success on-error]}] - (if (and web3 account-id) - (.getBalance - (.-eth web3) - account-id - (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))) - (on-error "web3 or account-id not available"))) - -(defn get-token-balance - [{:keys [web3 contract account-id on-success on-error]}] - (if (and web3 contract account-id) - (erc20/balance-of - web3 - contract - (ethereum/normalized-address account-id) - (fn [err resp] - (if-not err - (on-success resp) - (on-error err)))) - (on-error "web3, contract or account-id not available"))) - (defn assoc-error-message [db error-type err] (assoc-in db [:wallet :errors error-type] (or err :unknown-error))) @@ -83,27 +59,18 @@ ;; FX (re-frame/reg-fx - :get-balance - (fn [{:keys [web3 account-id success-event error-event]}] - (get-balance {:web3 web3 - :account-id account-id - :on-success #(re-frame/dispatch [success-event %]) - :on-error #(re-frame/dispatch [error-event %])}))) - -(re-frame/reg-fx - :get-tokens-balance - (fn [{:keys [web3 symbols all-tokens chain account-id success-event error-event]}] - (doseq [symbol symbols] - (let [contract (:address (tokens/symbol->token all-tokens chain symbol))] - (get-token-balance {:web3 web3 - :contract contract - :account-id account-id - :on-success #(re-frame/dispatch [success-event symbol %]) - :on-error #(re-frame/dispatch [error-event symbol %])}))))) - -;; TODO(oskarth): At some point we want to get list of relevant assets to get prices for + :wallet/get-balance + (fn [{:keys [account-id on-success on-error]}] + (json-rpc/call + {:method "eth_getBalance" + :params [account-id "latest"] + :on-success on-success + :on-error on-error}))) + +;; TODO(oskarth): At some point we want to get list of relevant +;; assets to get prices for (re-frame/reg-fx - :get-prices + :wallet/get-prices (fn [{:keys [from to mainnet? success-event error-event chaos-mode?]}] (prices/get-prices from to @@ -113,91 +80,99 @@ chaos-mode?))) (re-frame/reg-fx - :update-gas-price - (fn [{:keys [web3 success-event edit?]}] - (ethereum/gas-price web3 #(re-frame/dispatch [success-event %2 edit?])))) + :wallet/update-gas-price + (fn [{:keys [success-event edit?]}] + (json-rpc/call + {:method "eth_gasPrice" + :on-success + #(re-frame/dispatch [success-event % edit?])}))) (re-frame/reg-fx - :update-estimated-gas - (fn [{:keys [web3 obj success-event]}] - (ethereum/estimate-gas-web3 web3 (clj->js obj) #(re-frame/dispatch [success-event %2])))) - -(defn- validate-token-name! [web3 {:keys [address symbol name]}] - (erc20/name web3 address #(when (and (seq %2) ;;NOTE(goranjovic): skipping check if field not set in contract - (not= name %2)) - (let [message (i18n/label :t/token-auto-validate-name-error - {:symbol symbol - :expected name - :actual %2 - :address address})] - (log/warn message) - (utils.utils/show-popup (i18n/label :t/warning) message))))) - -(defn- validate-token-symbol! [web3 {:keys [address symbol]}] - (erc20/symbol web3 address #(when (and (seq %2) ;;NOTE(goranjovic): skipping check if field not set in contract - (not= (clojure.core/name symbol) %2)) - (let [message (i18n/label :t/token-auto-validate-symbol-error - {:symbol symbol - :expected (clojure.core/name symbol) - :actual %2 - :address address})] - (log/warn message) - (utils.utils/show-popup (i18n/label :t/warning) message))))) - -(defn- validate-token-decimals! [web3 {:keys [address symbol decimals nft? skip-decimals-check?]}] - ;;NOTE(goranjovic): only skipping check if skip-decimals-check? flag is present because we can't differentiate - ;;between unset decimals and 0 decimals. - (when-not skip-decimals-check? - (erc20/decimals web3 address #(when (and %2 - (not nft?) - (not= decimals (int %2))) - (let [message (i18n/label :t/token-auto-validate-decimals-error - {:symbol symbol - :expected decimals - :actual %2 - :address address})] - (log/warn message) - (utils.utils/show-popup (i18n/label :t/warning) message)))))) + :wallet/update-estimated-gas + (fn [{:keys [obj success-event]}] + (json-rpc/call + {:method "eth_estimateGas" + :params [obj] + :on-success + #(re-frame/dispatch [success-event %])}))) + +(defn- validate-token-name! + [{:keys [address symbol name]}] + (json-rpc/eth-call + {:contract address + :method "name()" + :outputs ["string"] + :on-success + (fn [[contract-name]] + (when (and (not (empty? contract-name)) + (not= name contract-name)) + (let [message (i18n/label :t/token-auto-validate-name-error + {:symbol symbol + :expected name + :actual contract-name + :address address})] + (log/warn message) + (utils.utils/show-popup (i18n/label :t/warning) message))))})) + +(defn- validate-token-symbol! + [{:keys [address symbol]}] + (json-rpc/eth-call + {:contract address + :method "symbol()" + :outputs ["string"] + :on-success + (fn [[contract-symbol]] + ;;NOTE(goranjovic): skipping check if field not set in contract + (when (and (not (empty? contract-symbol)) + (not= (clojure.core/name symbol) contract-symbol)) + (let [message (i18n/label :t/token-auto-validate-symbol-error + {:symbol symbol + :expected (clojure.core/name symbol) + :actual contract-symbol + :address address})] + (log/warn message) + (utils.utils/show-popup (i18n/label :t/warning) message))))})) + +(defn- validate-token-decimals! + [{:keys [address symbol decimals nft?]}] + (when-not nft? + (json-rpc/eth-call + {:contract address + :method "decimals()" + :outputs ["uint256"] + :on-success + (fn [[contract-decimals]] + (when (and (not (nil? contract-decimals)) + (not= decimals contract-decimals)) + (let [message (i18n/label :t/token-auto-validate-decimals-error + {:symbol symbol + :expected decimals + :actual contract-decimals + :address address})] + (log/warn message) + (utils.utils/show-popup (i18n/label :t/warning) message))))}))) (re-frame/reg-fx :wallet/validate-tokens - (fn [{:keys [web3 tokens]}] + (fn [tokens] (doseq [token tokens] - (validate-token-decimals! web3 token) - (validate-token-symbol! web3 token) - (validate-token-name! web3 token)))) + (validate-token-decimals! token) + (validate-token-symbol! token) + (validate-token-name! token)))) (re-frame/reg-fx - :wallet/check-all-known-tokens-balance - (fn [{:keys [web3 contracts account]}] - (doseq [{:keys [address symbol]} contracts] - ;;TODO(goranjovic): move `get-token-balance` function to wallet models - (get-token-balance {:web3 web3 - :contract address - :account-id (:address account) - :on-error #(re-frame/dispatch [:update-token-balance-fail symbol %]) - :on-success #(when (> % 0) - (re-frame/dispatch [:wallet/token-found symbol %]))})))) - -(fx/defn wallet-autoconfig-tokens - [{:keys [db] :as cofx}] - (let [{:keys [account/account web3 network-status] :wallet/keys [all-tokens]} db - network (get (:networks account) (:network account)) - chain (ethereum/network->chain-keyword network) - contracts (->> (tokens/tokens-for all-tokens chain) - (remove :hidden?)) - settings (:settings account) - assets (get-in settings [:wallet :visible-tokens chain])] - (when-not (or (= network-status :offline) - assets) - (let [new-settings (assoc-in settings - [:wallet :visible-tokens chain] - #{})] - (fx/merge cofx - {:wallet/check-all-known-tokens-balance {:web3 web3 - :contracts contracts - :account account}} - (accounts.update/update-settings new-settings {})))))) + :wallet/get-tokens-balance + (fn [{:keys [wallet-address tokens on-success on-error]}] + (doseq [{:keys [address symbol]} tokens] + (json-rpc/eth-call + {:contract address + :method "balanceOf(address)" + :params [wallet-address] + :outputs ["uint256"] + :on-success + (fn [[balance]] + (on-success symbol (money/bignumber balance))) + :on-error #(on-error symbol %)})))) (def min-gas-price-wei (money/bignumber 1)) @@ -277,7 +252,8 @@ (assoc :nonce nonce)))) ;; SEND TRANSACTION -> RPC TRANSACTION -(defn prepare-send-transaction [from {:keys [amount to gas gas-price data nonce]}] +(defn prepare-send-transaction + [from {:keys [amount to gas gas-price data nonce]}] (cond-> {:from (ethereum/normalized-address from) :to (ethereum/normalized-address to) :value (ethereum/int->hex amount) @@ -288,9 +264,10 @@ nonce (assoc :nonce nonce))) -;; NOTE (andrey) we need this function, because params may be mixed up, so we need to figure out which one is address -;; and which message -(defn normalize-sign-message-params [params] +(defn normalize-sign-message-params + "NOTE (andrey) we need this function, because params may be mixed up, + so we need to figure out which one is address and which message" + [params] (let [first_param (first params) second_param (second params) first-param-address? (ethereum/address? first_param) @@ -300,23 +277,29 @@ [first_param second_param] [second_param first_param])))) -(defn web3-error-callback [fx {:keys [webview-bridge]} message-id message] - (assoc fx :browser/send-to-bridge {:message {:type constants/web3-send-async-callback - :messageId message-id - :error message} - :webview webview-bridge})) - -(defn dapp-complete-transaction [id result method message-id webview keycard?] - (cond-> {:browser/send-to-bridge {:message {:type constants/web3-send-async-callback - :messageId message-id - :result {:jsonrpc "2.0" - :id (int id) - :result result}} - :webview webview} - :dispatch [:navigate-back]} +(defn web3-error-callback + [fx {:keys [webview-bridge]} message-id message] + (assoc fx :browser/send-to-bridge + {:message {:type constants/web3-send-async-callback + :messageId message-id + :error message} + :webview webview-bridge})) + +(defn dapp-complete-transaction + [id result method message-id webview keycard?] + (cond-> {:browser/send-to-bridge + {:message {:type constants/web3-send-async-callback + :messageId message-id + :result {:jsonrpc "2.0" + :id (int id) + :result result}} + :webview webview} + :dispatch [:navigate-back]} (constants/web3-sign-message? method) - (assoc :dispatch (if keycard? [:navigate-to :browser] [:navigate-back])) + (assoc :dispatch (if keycard? + [:navigate-to :browser] + [:navigate-back])) (= method constants/web3-send-transaction) (assoc :dispatch [:navigate-to-clean :wallet-transaction-sent-modal]))) @@ -350,6 +333,9 @@ (fx/defn handle-transaction-error [{:keys [db] :as cofx} {:keys [code message]}] (let [{:keys [on-error]} (get-in db [:wallet :send-transaction])] + (log/warn :wallet/transaction-error + :code code + :message message) (case code ;;WRONG PASSWORD constants/send-transaction-err-decrypt @@ -368,75 +354,110 @@ (defn clear-error-message [db error-type] (update-in db [:wallet :errors] dissoc error-type)) -(defn tokens-symbols [visible-token-symbols all-tokens chain] - (set/difference (set visible-token-symbols) (set (map :symbol (tokens/nfts-for all-tokens chain))))) +(defn tokens-symbols + [visible-token-symbols all-tokens chain] + (set/difference (set visible-token-symbols) + (set (map :symbol (tokens/nfts-for all-tokens chain))))) (fx/defn initialize-tokens [{:keys [db] :as cofx}] - (let [network-id (get-in db [:account/account :network]) - network (get-in db [:account/account :networks network-id]) - custom-tokens (get-in db [:account/account :settings :wallet :custom-tokens]) - chain (ethereum/network->chain-keyword network) + (let [custom-tokens (get-in db [:account/account :settings :wallet :custom-tokens]) + chain (ethereum/chain-keyword db) all-tokens (merge-with merge (utils.core/map-values #(utils.core/index-by :address %) tokens/all-default-tokens) custom-tokens)] - (fx/merge cofx - (merge - {:db (assoc db :wallet/all-tokens all-tokens)} - (when config/erc20-contract-warnings-enabled? - {:wallet/validate-tokens {:web3 (:web3 db) - :tokens (get tokens/all-default-tokens chain)}})) - wallet-autoconfig-tokens))) - -(fx/defn update-wallet - [{{:keys [web3 network network-status] - {:keys [address settings]} :account/account :as db} :db}] - (let [all-tokens (:wallet/all-tokens db) - network (get-in db [:account/account :networks network]) - chain (ethereum/network->chain-keyword network) + (fx/merge + cofx + (merge + {:db (assoc db :wallet/all-tokens all-tokens)} + (when config/erc20-contract-warnings-enabled? + {:wallet/validate-tokens (get tokens/all-default-tokens chain)}))))) + +(fx/defn update-balances + [{{:keys [network-status :wallet/all-tokens] + {:keys [address settings]} :account/account :as db} :db :as cofx}] + (let [normalized-address (ethereum/normalized-address address) + chain (ethereum/chain-keyword db) + assets (get-in settings [:wallet :visible-tokens chain]) + tokens (->> (tokens/tokens-for all-tokens chain) + (remove #(or (:hidden? %))) + (filter #((or assets identity) (:symbol %))))] + (when (not= network-status :offline) + (fx/merge + cofx + {:wallet/get-balance + {:account-id normalized-address + :on-success #(re-frame/dispatch + [:wallet.callback/update-balance-success %]) + :on-error #(re-frame/dispatch + [:wallet.callback/update-balance-fail %])} + + :wallet/get-tokens-balance + {:wallet-address normalized-address + :tokens tokens + :on-success + (fn [symbol balance] + (if assets + (re-frame/dispatch + [:wallet.callback/update-token-balance-success symbol balance]) + ;; NOTE: when there is no visible assets set, + ;; we make an initialization round + (when (> balance 0) + (re-frame/dispatch + [:wallet/token-found symbol balance])))) + :on-error + (fn [symbol error] + (re-frame/dispatch + [:wallet.callback/update-token-balance-fail symbol error]))} + + :db + (-> db + (clear-error-message :balance-update) + (assoc-in [:wallet :balance-loading?] true))} + (when-not assets + (accounts.update/update-settings + (assoc-in settings + [:wallet :visible-tokens chain] + #{}) + {})))))) + +(fx/defn update-prices + [{{:keys [network network-status :wallet/all-tokens] + {:keys [address settings networks]} :account/account :as db} :db}] + (let [chain (ethereum/chain-keyword db) mainnet? (= :mainnet chain) assets (get-in settings [:wallet :visible-tokens chain]) - tokens (tokens-symbols (get-in settings [:wallet :visible-tokens chain]) all-tokens chain) + tokens (tokens-symbols assets all-tokens chain) currency-id (or (get-in settings [:wallet :currency]) :usd) currency (get constants/currencies currency-id)] (when (not= network-status :offline) - {:get-balance {:web3 web3 - :account-id address - :success-event :wallet.callback/update-balance-success - :error-event :wallet.callback/update-balance-fail} - :get-tokens-balance {:web3 web3 - :account-id address - :symbols assets - :chain chain - :all-tokens all-tokens - :success-event :wallet.callback/update-token-balance-success - :error-event :wallet.callback/update-token-balance-fail} - :get-prices {:from (if mainnet? - (conj tokens "ETH") - [(-> (tokens/native-currency chain) - (wallet.utils/exchange-symbol))]) - :to [(:code currency)] - :mainnet? mainnet? - :success-event :wallet.callback/update-prices-success - :error-event :wallet.callback/update-prices-fail - :chaos-mode? (:chaos-mode? settings)} - :db (-> db - (clear-error-message :prices-update) - (clear-error-message :balance-update) - (assoc-in [:wallet :balance-loading?] true) - (assoc :prices-loading? true))}))) - -(defn open-modal-wallet-for-transaction [db transaction tx-object] + {:wallet/get-prices + {:from (if mainnet? + (conj tokens "ETH") + [(-> (tokens/native-currency chain) + (wallet.utils/exchange-symbol))]) + :to [(:code currency)] + :mainnet? mainnet? + :success-event :wallet.callback/update-prices-success + :error-event :wallet.callback/update-prices-fail + :chaos-mode? (:chaos-mode? settings)} + + :db + (-> db + (clear-error-message :prices-update) + (assoc :prices-loading? true))}))) + +(defn open-modal-wallet-for-transaction + [db transaction tx-object] (let [{:keys [gas gas-price]} transaction {:keys [wallet-set-up-passed?]} (:account/account db)] {:db (-> db (assoc-in [:navigation/screen-params :wallet-send-modal-stack :modal?] true) (assoc-in [:wallet :send-transaction] transaction) (assoc-in [:wallet :send-transaction :original-gas] gas)) - :dispatch-n [[:TODO.remove/update-wallet] - (when-not gas + :dispatch-n [(when-not gas [:TODO.remove/update-estimated-gas tx-object]) (when-not gas-price [:wallet/update-gas-price]) @@ -444,9 +465,10 @@ (if wallet-set-up-passed? :wallet-send-modal-stack :wallet-send-modal-stack-with-onboarding)]]})) -2 + (fx/defn open-sign-transaction-flow - [{:keys [db] :as cofx} {:keys [gas gas-price] :as transaction}] + [{:keys [db] :as cofx} + {:keys [gas gas-price] :as transaction}] (let [go-to-view-id (if (get-in db [:account/account :wallet-set-up-passed?]) :wallet-send-modal-stack :wallet-send-modal-stack-with-onboarding)] @@ -458,17 +480,14 @@ (assoc-in [:wallet :send-transaction :original-gas] gas))} (not gas) - (assoc :update-estimated-gas - {:web3 (:web3 db) - :obj (select-keys transaction [:to :data]) + (assoc :wallet/update-estimated-gas + {:obj (select-keys transaction [:to :data]) :success-event :wallet/update-estimated-gas-success}) (not gas-price) - (assoc :update-gas-price - {:web3 (:web3 db) - :success-event :wallet/update-gas-price-success + (assoc :wallet/update-gas-price + {:success-event :wallet/update-gas-price-success :edit? false})) - (update-wallet) (navigation/navigate-to-cofx go-to-view-id {})))) (defn send-transaction-screen-did-load @@ -482,7 +501,7 @@ (conj (or ids #{}) id) (disj ids id))) -(fx/defn update-prices +(fx/defn on-update-prices-success [{:keys [db]} prices] {:db (assoc db :prices prices @@ -491,13 +510,13 @@ (fx/defn update-balance [{:keys [db]} balance] {:db (-> db - (assoc-in [:wallet :balance :ETH] balance) + (assoc-in [:wallet :balance :ETH] (money/bignumber balance)) (assoc-in [:wallet :balance-loading?] false))}) (fx/defn update-token-balance [{:keys [db]} symbol balance] {:db (-> db - (assoc-in [:wallet :balance symbol] balance) + (assoc-in [:wallet :balance symbol] (money/bignumber balance)) (assoc-in [:wallet :balance-loading?] false))}) (fx/defn update-gas-price @@ -550,7 +569,8 @@ new-settings (update-in settings [:wallet :custom-tokens chain] dissoc address)] (accounts.update/update-settings cofx new-settings {}))) -(fx/defn configure-token-balance-and-visibility [cofx symbol balance] +(fx/defn configure-token-balance-and-visibility + [cofx symbol balance] (fx/merge cofx (toggle-visible-token symbol true) ;;TODO(goranjovic): move `update-token-balance-success` function to wallet models diff --git a/src/status_im/web3/core.cljs b/src/status_im/web3/core.cljs index 25f5e9177ec..514e81ac98f 100644 --- a/src/status_im/web3/core.cljs +++ b/src/status_im/web3/core.cljs @@ -39,13 +39,6 @@ (fn [error sync] (re-frame/dispatch [:web3.callback/get-syncing-success error sync]))))) -(defn get-block-number-fx [web3] - (when web3 - (.getBlockNumber - (.-eth web3) - (fn [error block-number] - (re-frame/dispatch [:web3.callback/get-block-number error block-number]))))) - (defn set-default-account [web3 address] (set! (.-defaultAccount (.-eth web3)) diff --git a/src/status_im/web3/events.cljs b/src/status_im/web3/events.cljs index 332446f0cb6..b6b7ae51614 100644 --- a/src/status_im/web3/events.cljs +++ b/src/status_im/web3/events.cljs @@ -13,10 +13,6 @@ :web3/get-syncing web3/get-syncing) -(re-frame/reg-fx - :web3/get-block-number - web3/get-block-number-fx) - (re-frame/reg-fx :web3/set-default-account (fn [[web3 address]] diff --git a/test/cljs/status_im/test/sign_in/flow.cljs b/test/cljs/status_im/test/sign_in/flow.cljs index 7741d4f0871..44a3f5dac0e 100644 --- a/test/cljs/status_im/test/sign_in/flow.cljs +++ b/test/cljs/status_im/test/sign_in/flow.cljs @@ -199,12 +199,11 @@ (set (:utils/dispatch-later efx))))) (testing "Check the rest of effects." (is (contains? efx :web3/set-default-account)) - (is (contains? efx :web3/get-block-number)) (is (contains? efx :web3/fetch-node-version)) - (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 :wallet/get-balance)) + (is (contains? efx :wallet/get-tokens-balance)) + (is (contains? efx :wallet/get-prices)))))) (deftest login-failed (testing diff --git a/test/cljs/status_im/test/ui/screens/currency_settings/models.cljs b/test/cljs/status_im/test/ui/screens/currency_settings/models.cljs index db40e6f114a..3ff599bbb54 100644 --- a/test/cljs/status_im/test/ui/screens/currency_settings/models.cljs +++ b/test/cljs/status_im/test/ui/screens/currency_settings/models.cljs @@ -11,7 +11,7 @@ (deftest set-currency (let [cofx (models/set-currency {:db {:account/account {:settings {:wallet {}}}}} :usd)] - (is (= [:db :get-balance :get-tokens-balance :get-prices :data-store/base-tx] (keys cofx))) + (is (= [:db :wallet/get-prices :data-store/base-tx] (keys cofx))) (is (= :usd (get-in cofx [:db :account/account :settings :wallet :currency])))) (is (= :jpy (get-in (models/set-currency {:db {:account/account {:settings {:wallet {}}}}} :jpy) [:db :account/account :settings :wallet :currency])))) From b6fecd4e1c9f6f4ee62ec71bc49aa35e5fa7c637 Mon Sep 17 00:00:00 2001 From: yenda Date: Mon, 20 May 2019 02:21:38 +0200 Subject: [PATCH 7/8] [refactor] remove ethereum `call` and `call-params` - use `json-rpc/eth-call` and `json-rpc/eth-transaction-call` everywhere - move all conversions to abi-spec --- src/status_im/ethereum/contracts.cljs | 64 +---- src/status_im/stickers/core.cljs | 258 ++++++++++-------- src/status_im/subs.cljs | 9 +- src/status_im/tribute_to_talk/core.cljs | 66 +++-- .../ui/screens/add_new/new_chat/events.cljs | 5 +- .../ui/screens/chat/stickers/views.cljs | 6 +- src/status_im/ui/screens/db.cljs | 6 +- .../ui/screens/wallet/send/events.cljs | 22 +- src/status_im/utils/ethereum/abi_spec.cljs | 3 + src/status_im/utils/ethereum/core.cljs | 70 +---- src/status_im/utils/ethereum/eip165.cljs | 17 +- src/status_im/utils/ethereum/ens.cljs | 120 ++++---- src/status_im/utils/ethereum/erc721.cljs | 31 ++- src/status_im/utils/ethereum/resolver.cljs | 1 - src/status_im/utils/ethereum/stickers.cljs | 34 --- src/status_im/wallet/core.cljs | 70 ++--- src/status_im/wallet/custom_tokens/core.cljs | 202 +++++++++----- .../status_im/test/utils/ethereum/core.cljs | 12 +- 18 files changed, 511 insertions(+), 485 deletions(-) delete mode 100644 src/status_im/utils/ethereum/stickers.cljs diff --git a/src/status_im/ethereum/contracts.cljs b/src/status_im/ethereum/contracts.cljs index 517ca76fbbf..47fa87a71c4 100644 --- a/src/status_im/ethereum/contracts.cljs +++ b/src/status_im/ethereum/contracts.cljs @@ -1,61 +1,17 @@ (ns status-im.ethereum.contracts - (:require [re-frame.core :as re-frame] - [status-im.utils.ethereum.abi-spec :as abi-spec] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.fx :as fx] - [status-im.utils.money :as money] - [status-im.wallet.core :as wallet])) + (:require [status-im.utils.ethereum.core :as ethereum])) (def contracts - {:status/tribute-to-talk - {:address - {:mainnet nil - :testnet "0x3da3fc53e24707f36c5b4433b442e896c4955f0e" - :rinkeby nil} - :methods - {:get-manifest - {:signature "getManifest(address)" - :outputs ["bytes"]} - :set-manifest - {:signature "setManifest(bytes)" - :write? true}}}}) + {:status/snt + {:mainnet "0x744d70fdbe2ba4cf95131626614a1763df805b9e" + :testnet "0xc55cf4b03948d7ebc8b9e8bad92643703811d162"} + :status/tribute-to-talk + {:testnet "0x3da3fc53e24707f36c5b4433b442e896c4955f0e"} + :status/stickers + {:testnet "0x39d16CdB56b5a6a89e1A397A13Fe48034694316E"}}) -(re-frame/reg-fx - ::call - (fn [{:keys [address data callback]}] - (ethereum/call {:to address - :data data} - callback))) - -(defn get-contract-address +(defn get-address [db contract] (let [chain-keyword (-> (get-in db [:account/account :networks (:network db)]) ethereum/network->chain-keyword)] - (get-in contracts [contract :address chain-keyword]))) - -(fx/defn call - [{:keys [db] :as cofx} - {:keys [contract contract-address method params - callback on-result on-error details]}] - (when-let [contract-address (or contract-address - (get-contract-address db contract))] - (let [{:keys [signature outputs write?]} - (get-in contracts [contract :methods method]) - data (abi-spec/encode signature params)] - (if write? - (wallet/open-sign-transaction-flow - cofx - (merge {:to contract-address - :data data - :id "approve" - :symbol :ETH - :method "eth_sendTransaction" - :amount (money/bignumber 0) - :on-result on-result - :on-error on-error} - details)) - {::call {:address contract-address - :data data - :callback #(callback (if (empty? outputs) - % - (abi-spec/decode % outputs)))}})))) + (get-in contracts [contract chain-keyword]))) diff --git a/src/status_im/stickers/core.cljs b/src/status_im/stickers/core.cljs index 8d83f36890e..8a7ec3ee010 100644 --- a/src/status_im/stickers/core.cljs +++ b/src/status_im/stickers/core.cljs @@ -3,22 +3,94 @@ [re-frame.core :as re-frame] [status-im.accounts.core :as accounts] [status-im.constants :as constants] + [status-im.ethereum.contracts :as contracts] + [status-im.ethereum.json-rpc :as json-rpc] [status-im.ui.screens.navigation :as navigation] [status-im.utils.ethereum.abi-spec :as abi-spec] [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.stickers :as ethereum.stickers] [status-im.utils.fx :as fx] - [status-im.utils.money :as money] [status-im.utils.multihash :as multihash] + [status-im.utils.utils :as utils] [status-im.wallet.core :as wallet])) -(fx/defn init-stickers-packs [{:keys [db]}] +(defn pack-data-callback + [id open?] + (fn [[category owner mintable timestamp price contenthash]] + (let [proto-code (subs contenthash 2 4) + hash (when contenthash + (multihash/base58 (multihash/create :sha2-256 (subs contenthash 12))))] + (when (and (#{constants/swarm-proto-code constants/ipfs-proto-code} + proto-code) hash) + (re-frame/dispatch [:stickers/load-pack proto-code hash id price open?]))))) + +(re-frame/reg-fx + :stickers/set-pending-timout-fx + (fn [] + (utils/set-timeout #(re-frame/dispatch [:stickers/pending-timout]) + 10000))) + +(defn eth-call-pack-data + [contract id open?] + (json-rpc/eth-call + {:contract contract + ;; Returns vector of pack data parameters by pack id: + ;; [category owner mintable timestamp price contenthash] + :method "getPackData(uint256)" + :params [id] + :outputs ["bytes4[]" "address" "bool" "uint256" "uint256" "bytes"] + :on-success (pack-data-callback id open?)})) + +(re-frame/reg-fx + :stickers/pack-data-fx + (fn [[contract id]] + (eth-call-pack-data contract id true))) + +(re-frame/reg-fx + :stickers/load-packs-fx + (fn [[contract]] + (json-rpc/eth-call + {:contract contract + ;; Returns number of packs registered in the contract + :method "packCount()" + :outputs ["uint256"] + :on-success + (fn [[count]] + (dotimes [id count] + (eth-call-pack-data contract id false)))}))) + +(re-frame/reg-fx + :stickers/owned-packs-fx + (fn [[contract address]] + (json-rpc/eth-call + {:contract contract + ;; Returns vector of owned tokens ids in the contract by address + :method "tokensOwnedBy(address)" + :params [address] + :outputs ["uint256[]"] + :on-success + (fn [[tokens]] + (doseq [id tokens] + (json-rpc/eth-call + {:contract contract + ;; Returns pack id in the contract by token id + :method "tokenPackId(uint256)" + :params [id] + :outputs ["uint256"] + :on-success + (fn [[pack-id]] + (re-frame/dispatch [:stickers/pack-owned pack-id]))})))}))) + +(fx/defn init-stickers-packs + [{:keys [db]}] (let [sticker-packs (into {} (map #(let [pack (edn/read-string %)] (vector (:id pack) pack)) (get-in db [:account/account :stickers])))] - {:db (assoc db :stickers/packs-installed sticker-packs :stickers/packs sticker-packs)})) + {:db (assoc db + :stickers/packs-installed sticker-packs + :stickers/packs sticker-packs)})) -(fx/defn install-stickers-pack [{{:account/keys [account] :as db} :db :as cofx} id] +(fx/defn install-stickers-pack + [{{:account/keys [account] :as db} :db :as cofx} id] (let [pack (get-in db [:stickers/packs id])] (fx/merge cofx @@ -27,125 +99,91 @@ (assoc :stickers/selected-pack id))} (accounts/update-stickers (conj (:stickers account) (pr-str pack)))))) -(fx/defn load-sticker-pack-success [{:keys [db] :as cofx} edn-string id price open?] +(fx/defn load-sticker-pack-success + [{:keys [db] :as cofx} edn-string id price open?] (let [pack (assoc (get (edn/read-string edn-string) 'meta) :id id :price price)] (fx/merge cofx - {:db (-> db (assoc-in [:stickers/packs id] pack))} - #(when open? (navigation/navigate-to-cofx % :stickers-pack-modal pack))))) - -(defn pack-data-callback [id open?] - (fn [[category owner mintable timestamp price contenthash]] - (let [proto-code (subs contenthash 2 4) - hash (when contenthash (multihash/base58 (multihash/create :sha2-256 (subs contenthash 12))))] - (when (and (#{constants/swarm-proto-code constants/ipfs-proto-code} proto-code) hash) - (re-frame/dispatch [:stickers/load-pack proto-code hash id price open?]))))) + {:db (assoc-in db [:stickers/packs id] pack)} + #(when open? + (navigation/navigate-to-cofx % :stickers-pack-modal pack))))) (fx/defn open-sticker-pack - [{{:keys [network] :stickers/keys [packs packs-installed] :as db} :db :as cofx} id] + [{{:stickers/keys [packs packs-installed] :as db} :db :as cofx} id] (when id - (let [pack (or (get packs-installed id) (get packs id)) - network (get-in db [:account/account :networks network])] + (let [pack (or (get packs-installed id) + (get packs id)) + contract-address (contracts/get-address db :status/stickers)] (if pack (navigation/navigate-to-cofx cofx :stickers-pack-modal pack) - {:stickers/pack-data-fx [network id true]})))) - -(fx/defn load-pack [cofx proto-code hash id price open?] - {:http-get {:url (str (if (= constants/swarm-proto-code proto-code) - "https://swarm-gateways.net/bzz:/" - "https://ipfs.infura.io/ipfs/") - hash) - :success-event-creator (fn [o] - [:stickers/load-sticker-pack-success o id price open?]) - :failure-event-creator (constantly nil)}}) - -(fx/defn load-packs [{{:keys [network] :as db} :db}] - (let [network (get-in db [:account/account :networks network]) - address (ethereum/normalized-address (get-in db [:account/account :address]))] - {:stickers/owned-packs-fx [network address] - :stickers/load-packs-fx [network]})) - -(defn prepare-transaction [id tx on-result] - (merge {:id id - :symbol :ETH - :method constants/web3-send-transaction - :amount (money/bignumber 0)} - (when on-result {:on-result on-result}) - tx)) - -(def snt-contracts - {:mainnet "0x744d70fdbe2ba4cf95131626614a1763df805b9e" - :testnet "0xc55cf4b03948d7ebc8b9e8bad92643703811d162" - :rinkeby nil}) - -(fx/defn approve-pack [{db :db} pack-id price] - (let [network (get-in db [:account/account :networks (:network db)]) - address (ethereum/normalized-address (get-in db [:account/account :address])) - chain (ethereum/network->chain-keyword network) - stickers-contract (get ethereum.stickers/contracts chain) - data (abi-spec/encode "buyToken(uint256,address)" [pack-id address]) - tx-object {:to (get snt-contracts chain) - :data (abi-spec/encode "approveAndCall(address,uint256,bytes)" [stickers-contract price data])}] - (wallet/open-modal-wallet-for-transaction - db - (prepare-transaction "approve" tx-object [:stickers/pending-pack pack-id]) - tx-object))) + (when contract-address + {:stickers/pack-data-fx [contract-address id]}))))) + +(fx/defn load-pack + [cofx proto-code hash id price open?] + {:http-get {:url (str (if (= constants/swarm-proto-code proto-code) + "https://swarm-gateways.net/bzz:/" + "https://ipfs.infura.io/ipfs/") + hash) + :success-event-creator + (fn [o] + [:stickers/load-sticker-pack-success o id price open?]) + :failure-event-creator + (constantly nil)}}) + +(fx/defn load-packs + [{:keys [db]}] + (let [contract (contracts/get-address db :status/stickers) + address (ethereum/current-address db)] + (when contract + {:stickers/owned-packs-fx [contract address] + :stickers/load-packs-fx [contract]}))) + +(fx/defn approve-pack + [{db :db :as cofx} pack-id price] + (let [address (ethereum/current-address db) + chain (ethereum/chain-keyword db) + stickers-contract (contracts/get-address db :status/stickers) + snt-contract (contracts/get-address db :status/snt)] + (wallet/eth-transaction-call + cofx + {:contract snt-contract + :method "approveAndCall(address,uint256,bytes)" + :params [stickers-contract + price + (abi-spec/encode "buyToken(uint256,address)" + [pack-id address])] + :on-result [:stickers/pending-pack pack-id]}))) (fx/defn pending-pack - [{{:keys [network] :as db} :db :as cofx} id] - (let [network (get-in db [:account/account :networks network]) - address (ethereum/normalized-address (get-in db [:account/account :address]))] - (fx/merge cofx - {:db (update db :stickers/packs-pendning conj id) - :stickers/owned-packs-fx [network address]} - (navigation/navigate-to-clean :wallet-transaction-sent-modal {}) - #(when (zero? (count (:stickers/packs-pendning db))) - {:stickers/set-pending-timout-fx nil})))) + [{:keys [db] :as cofx} id] + (let [contract (contracts/get-address db :status/stickers) + address (ethereum/current-address db)] + (when contract + (fx/merge cofx + {:db (update db :stickers/packs-pending conj id) + :stickers/owned-packs-fx [contract address]} + (navigation/navigate-to-clean :wallet-transaction-sent-modal {}) + #(when (zero? (count (:stickers/packs-pending db))) + {:stickers/set-pending-timout-fx nil}))))) (fx/defn pending-timeout - [{{:keys [network] :stickers/keys [packs-pendning packs-owned] :as db} :db}] - (let [packs-diff (clojure.set/difference packs-pendning packs-owned) - network (get-in db [:account/account :networks network]) - address (ethereum/normalized-address (get-in db [:account/account :address]))] - (merge {:db (assoc db :stickers/packs-pendning packs-diff)} - (when-not (zero? (count packs-diff)) - {:stickers/owned-packs-fx [network address] - :stickers/set-pending-timout-fx nil})))) + [{{:stickers/keys [packs-pending packs-owned] :as db} :db}] + (let [packs-diff (clojure.set/difference packs-pending packs-owned) + contract (contracts/get-address db :status/stickers) + address (ethereum/current-address db)] + (when contract + (merge {:db (assoc db :stickers/packs-pending packs-diff)} + (when-not (zero? (count packs-diff)) + {:stickers/owned-packs-fx [contract address] + :stickers/set-pending-timout-fx nil}))))) (fx/defn pack-owned [{db :db} id] {:db (update db :stickers/packs-owned conj id)}) (fx/defn get-owned-pack - [{{:keys [network] :as db} :db}] - (let [address (ethereum/normalized-address (get-in db [:account/account :address]))] - {:stickers/owned-packs-fx [network address]})) - -(re-frame/reg-fx - :stickers/pack-data-fx - (fn [[network id open?]] - (when-let [contract (get ethereum.stickers/contracts (ethereum/network->chain-keyword network))] - (ethereum.stickers/pack-data contract id (pack-data-callback id open?))))) - -(re-frame/reg-fx - :stickers/set-pending-timout-fx - (fn [] - (js/setTimeout #(re-frame/dispatch [:stickers/pending-timout]) 10000))) - -(re-frame/reg-fx - :stickers/load-packs-fx - (fn [[network]] - (when-let [contract (get ethereum.stickers/contracts (ethereum/network->chain-keyword network))] - (ethereum.stickers/pack-count contract - (fn [count] - (dotimes [n count] - (ethereum.stickers/pack-data contract n (pack-data-callback n false)))))))) - -(re-frame/reg-fx - :stickers/owned-packs-fx - (fn [[network address]] - (when-let [contract (get ethereum.stickers/contracts (ethereum/network->chain-keyword network))] - (ethereum.stickers/owned-tokens contract address - (fn [tokens] - (doseq [n tokens] - (ethereum.stickers/token-pack-id contract n - #(re-frame/dispatch [:stickers/pack-owned %])))))))) + [{:keys [db]}] + (let [contract (contracts/get-address db :status/stickers) + address (ethereum/current-address db)] + (when contract + {:stickers/owned-packs-fx [contract address]}))) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index acd1fcbec3b..02331eedb13 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -126,7 +126,7 @@ (reg-root-key-sub :stickers/packs :stickers/packs) (reg-root-key-sub :stickers/installed-packs :stickers/packs-installed) (reg-root-key-sub :stickers/packs-owned :stickers/packs-owned) -(reg-root-key-sub :stickers/packs-pendning :stickers/packs-pendning) +(reg-root-key-sub :stickers/packs-pending :stickers/packs-pending) ;;mailserver (reg-root-key-sub :mailserver/current-id :mailserver/current-id) @@ -768,7 +768,7 @@ :<- [:stickers/packs] :<- [:stickers/installed-packs] :<- [:stickers/packs-owned] - :<- [:stickers/packs-pendning] + :<- [:stickers/packs-pending] (fn [[packs installed owned pending]] (map (fn [{:keys [id] :as pack}] (cond-> pack @@ -1167,9 +1167,8 @@ (let [{:keys [gas-used gas-price hash timestamp type token value] :as transaction} (get transactions current-transaction) - native-currency-text (-> native-currency - :symbol-display - name)] + native-currency-text (name (or (:symbol-display native-currency) + (:symbol native-currency)))] (when transaction (merge transaction {:gas-price-eth (if gas-price diff --git a/src/status_im/tribute_to_talk/core.cljs b/src/status_im/tribute_to_talk/core.cljs index 50afcd941ae..cf8bd80cfc7 100644 --- a/src/status_im/tribute_to_talk/core.cljs +++ b/src/status_im/tribute_to_talk/core.cljs @@ -5,12 +5,14 @@ [status-im.accounts.update.core :as accounts.update] [status-im.contact.db :as contact.db] [status-im.ethereum.contracts :as contracts] + [status-im.ethereum.json-rpc :as json-rpc] [status-im.ipfs.core :as ipfs] [status-im.tribute-to-talk.db :as tribute-to-talk.db] [status-im.ui.screens.navigation :as navigation] [status-im.utils.contenthash :as contenthash] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.fx :as fx] + [status-im.wallet.core :as wallet] [taoensso.timbre :as log])) (fx/defn update-settings @@ -195,25 +197,35 @@ [:tribute-to-talk.callback/fetch-manifest-success identity manifest])))})) +(re-frame/reg-fx + :tribute-to-talk/get-manifest + (fn [{:keys [contract address on-success]}] + (json-rpc/eth-call + {:contract contract + :method "getManifest(address)" + :params [address] + :outputs ["bytes"] + :on-success on-success}))) + (fx/defn check-manifest - [{:keys [db] :as cofx} identity] - (or (contracts/call cofx - {:contract :status/tribute-to-talk - :method :get-manifest - :params [(contact.db/public-key->address identity)] - :return-params ["bytes"] - :callback - #(re-frame/dispatch - (if-let [contenthash (first %)] - [:tribute-to-talk.callback/check-manifest-success - identity - contenthash] - [:tribute-to-talk.callback/no-manifest-found identity]))}) - ;; `contracts/call` returns nil if there is no contract for the current network - ;; update settings if checking own manifest or do nothing otherwise - (when-let [me? (= identity - (get-in cofx [:db :account/account :public-key]))] - (update-settings cofx nil)))) + [{:keys [db] :as cofx} public-key] + (if-let [contract (contracts/get-address db :status/tribute-to-talk)] + (let [address (contact.db/public-key->address public-key)] + {:tribute-to-talk/get-manifest + {:contract contract + :address address + :on-success + (fn [[contenthash]] + (re-frame/dispatch + (if contenthash + [:tribute-to-talk.callback/check-manifest-success + public-key + contenthash] + [:tribute-to-talk.callback/no-manifest-found public-key])))}}) + ;; update settings if checking own manifest or do nothing otherwise + (when-let [me? (= identity + (get-in cofx [:db :account/account :public-key]))] + (update-settings cofx nil)))) (fx/defn check-own-manifest [cofx] @@ -224,13 +236,17 @@ (let [contenthash (when hash (contenthash/encode {:hash hash :namespace :ipfs}))] - (or (contracts/call cofx - {:contract :status/tribute-to-talk - :method :set-manifest - :params [contenthash] - :on-result [:tribute-to-talk.callback/set-manifest-transaction-completed] - :on-error [:tribute-to-talk.callback/set-manifest-transaction-failed]}) - {:db (assoc-in db [:navigation/screen-params :tribute-to-talk :state] :transaction-failed)}))) + (if-let [contract (contracts/get-address db :status/tribute-to-talk)] + (wallet/eth-transaction-call + cofx + {:contract contract + :method "setManifest(bytes)" + :params [contenthash] + :on-result [:tribute-to-talk.callback/set-manifest-transaction-completed] + :on-error [:tribute-to-talk.callback/set-manifest-transaction-failed]}) + {:db (assoc-in db + [:navigation/screen-params :tribute-to-talk :state] + :transaction-failed)}))) (defn remove [{:keys [db] :as cofx}] diff --git a/src/status_im/ui/screens/add_new/new_chat/events.cljs b/src/status_im/ui/screens/add_new/new_chat/events.cljs index 2c0c869b5d2..c2c4edc6c4f 100644 --- a/src/status_im/ui/screens/add_new/new_chat/events.cljs +++ b/src/status_im/ui/screens/add_new/new_chat/events.cljs @@ -14,15 +14,14 @@ (handlers/register-handler-fx :new-chat/set-new-identity - (fn [{{:keys [network network-status] :as db} :db} [_ new-identity]] + (fn [{{:keys [network-status] :as db} :db} [_ new-identity]] (let [is-public-key? (and (string? new-identity) (string/starts-with? new-identity "0x"))] (merge {:db (assoc db :contacts/new-identity new-identity :contacts/new-identity-error (db/validate-pub-key db new-identity))} (when-not is-public-key? - (let [network (get-in db [:account/account :networks network]) - chain (ethereum/network->chain-keyword network)] + (let [chain (ethereum/chain-keyword db)] {:resolve-public-key {:registry (get ens/ens-registries chain) :ens-name (if (ens/is-valid-eth-name? new-identity) new-identity diff --git a/src/status_im/ui/screens/chat/stickers/views.cljs b/src/status_im/ui/screens/chat/stickers/views.cljs index 80221720086..b261ab8cfed 100644 --- a/src/status_im/ui/screens/chat/stickers/views.cljs +++ b/src/status_im/ui/screens/chat/stickers/views.cljs @@ -93,7 +93,8 @@ [stickers-panel (map #(assoc % :pack id) stickers) window-width])])) (defn pack-icon [{:keys [id on-press background-color] - :or {on-press #(re-frame/dispatch [:stickers/select-pack id])}} icon] + :or {on-press #(re-frame/dispatch [:stickers/select-pack id])}} + icon] [react/touchable-highlight {:on-press on-press} [react/view {:style {:align-items :center}} [react/view {:style (styles/pack-icon background-color icon-size icon-horizontal-margin)} @@ -148,7 +149,8 @@ [vector-icons/icon :stickers-icons/recent {:color colors/gray}]] (for [{:keys [id thumbnail]} installed-packs] ^{:key id} - [pack-icon {:id id} + [pack-icon {:id id + :background-color colors/white} [react/image {:style {:width icon-size :height icon-size :border-radius (/ icon-size 2)} :source {:uri thumbnail}}]])] [scroll-indicator]]]]])) diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index 4bbb6a34e54..c2c0041d5ba 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -61,7 +61,7 @@ :push-notifications/stored {} :registry {} :stickers/packs-owned #{} - :stickers/packs-pendning #{} + :stickers/packs-pending #{} :hardwallet {:nfc-supported? false :nfc-enabled? false :pin {:original [] @@ -192,7 +192,7 @@ (spec/def :stickers/packs (spec/nilable map?)) (spec/def :stickers/packs-owned (spec/nilable set?)) -(spec/def :stickers/packs-pendning (spec/nilable set?)) +(spec/def :stickers/packs-pending (spec/nilable set?)) (spec/def :stickers/packs-installed (spec/nilable map?)) (spec/def :stickers/selected-pack (spec/nilable any?)) (spec/def :stickers/recent (spec/nilable vector?)) @@ -269,7 +269,7 @@ :stickers/selected-pack :stickers/recent :stickers/packs-owned - :stickers/packs-pendning + :stickers/packs-pending :bottom-sheet/show? :bottom-sheet/view :bottom-sheet/options diff --git a/src/status_im/ui/screens/wallet/send/events.cljs b/src/status_im/ui/screens/wallet/send/events.cljs index 51ac18a9245..cec87137a7c 100644 --- a/src/status_im/ui/screens/wallet/send/events.cljs +++ b/src/status_im/ui/screens/wallet/send/events.cljs @@ -6,6 +6,7 @@ [status-im.native-module.core :as status] [status-im.transport.utils :as transport.utils] [status-im.ui.screens.navigation :as navigation] + [status-im.utils.ethereum.abi-spec :as abi-spec] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.ethereum.tokens :as tokens] [status-im.utils.fx :as fx] @@ -29,10 +30,13 @@ {:keys [from to value gas gasPrice]} on-completed masked-password] (let [contract (:address (tokens/symbol->token all-tokens (keyword chain) symbol))] (status/send-transaction (types/clj->json - (merge (ethereum/call-params contract "transfer(address,uint256)" to value) - {:from from - :gas gas - :gasPrice gasPrice})) + {:to contract + :from from + :data (abi-spec/encode + "transfer(address,uint256)" + [to value]) + :gas gas + :gasPrice gasPrice}) (security/safe-unmask-data masked-password) on-completed))) @@ -66,9 +70,10 @@ (handlers/register-handler-fx :wallet/send-transaction (fn [{{:keys [chain] :as db} :db} _] - (let [{:keys [password symbol in-progress?] :as transaction} (get-in db [:wallet :send-transaction]) + (let [{:keys [password symbol in-progress?] :as transaction} + (get-in db [:wallet :send-transaction]) all-tokens (:wallet/all-tokens db) - from (get-in db [:account/account :address])] + from (ethereum/current-address db)] (when-not in-progress? {:db (-> db (assoc-in [:wallet :send-transaction :wrong-password?] false) @@ -374,8 +379,9 @@ (wallet/prepare-send-transaction from transaction) (let [contract (:address (tokens/symbol->token all-tokens (keyword chain) symbol)) {:keys [gas gasPrice to from value]} (wallet/prepare-send-transaction from transaction)] - (merge (ethereum/call-params contract "transfer(address,uint256)" to value) - {:from from + (merge (abi-spec/encode "transfer(address,uint256)" [to value]) + {:to contract + :from from :gas gas :gasPrice gasPrice})))) diff --git a/src/status_im/utils/ethereum/abi_spec.cljs b/src/status_im/utils/ethereum/abi_spec.cljs index fd671a1015d..2fe587909ea 100644 --- a/src/status_im/utils/ethereum/abi_spec.cljs +++ b/src/status_im/utils/ethereum/abi_spec.cljs @@ -28,6 +28,9 @@ (when x (subs (.fromUtf8 utils x) 2))) +(defn hex-to-boolean [x] + (= x "0x0")) + (defn bytes-to-hex [x] (when x (subs (.bytesToHex utils x) 2))) diff --git a/src/status_im/utils/ethereum/core.cljs b/src/status_im/utils/ethereum/core.cljs index 6d719a78c20..55cb8125413 100644 --- a/src/status_im/utils/ethereum/core.cljs +++ b/src/status_im/utils/ethereum/core.cljs @@ -1,6 +1,5 @@ (ns status-im.utils.ethereum.core (:require [clojure.string :as string] - [status-im.ethereum.json-rpc :as json-rpc] [status-im.js-dependencies :as dependencies] [status-im.utils.ethereum.tokens :as tokens] [status-im.utils.money :as money])) @@ -40,6 +39,10 @@ address (str hex-prefix address)))) +(defn current-address [db] + (-> (get-in db [:account/account :address]) + normalized-address)) + (defn naked-address [s] (when s (string/replace s hex-prefix ""))) @@ -74,71 +77,6 @@ ([s opts] (.sha3 dependencies/Web3.prototype (str s) (clj->js opts)))) -(defn hex->string [s] - (when s - (let [hex (.toString s)] - (loop [res "" i (if (string/starts-with? hex hex-prefix) 2 0)] - (if (and (< i (.-length hex))) - (recur - (if (= (.substr hex i 2) "00") - res - (str res (.fromCharCode js/String (js/parseInt (.substr hex i 2) 16)))) - (+ i 2)) - res))))) - -(defn hex->boolean [s] - (= s "0x0")) - -(defn boolean->hex [b] - (if b "0x0" "0x1")) - -(defn hex->int [s] - (if (= s hex-prefix) - 0 - (js/parseInt s 16))) - -(defn int->hex [i] - (.toHex dependencies/Web3.prototype i)) - -(defn hex->bignumber [s] - (money/bignumber (if (= s hex-prefix) 0 s))) - -(defn hex->address - "When hex value is 66 char in length (2 for 0x, 64 for - the 32 bytes used by abi-spec for an address), only keep - the part that constitute the address and normalize it," - [s] - (when (= 66 (count s)) - (normalized-address (subs s 26)))) - -(defn zero-pad-64 [s] - (str (apply str (drop (count s) (repeat 64 "0"))) s)) - -(defn string->hex [i] - (.fromAscii dependencies/Web3.prototype i)) - -(defn format-param [param] - (if (number? param) - (zero-pad-64 (str (hex->int param))) - (zero-pad-64 (subs param 2)))) - -(defn format-call-params [method-id & params] - (let [params (string/join (map format-param params))] - (str method-id params))) - -(defn- sig->method-id [signature] - (apply str (take 10 (sha3 signature)))) - -(defn call [params callback] - (json-rpc/call - {:method "eth_call" - :params [params "latest"] - :on-success callback})) - -(defn call-params [contract method-sig & params] - (let [data (apply format-call-params (sig->method-id method-sig) params)] - {:to contract :data data})) - (def default-transaction-gas (money/bignumber 21000)) (defn estimate-gas [symbol] diff --git a/src/status_im/utils/ethereum/eip165.cljs b/src/status_im/utils/ethereum/eip165.cljs index 6785cb6da2b..4286ffdddef 100644 --- a/src/status_im/utils/ethereum/eip165.cljs +++ b/src/status_im/utils/ethereum/eip165.cljs @@ -1,13 +1,18 @@ (ns status-im.utils.ethereum.eip165 "Utility function related to [EIP165](https://eips.ethereum.org/EIPS/eip-165)" - (:require [status-im.utils.ethereum.core :as ethereum])) + (:require [status-im.ethereum.json-rpc :as json-rpc] + [status-im.utils.ethereum.abi-spec :as abi-spec])) (def supports-interface-hash "0x01ffc9a7") (def marker-hash "0xffffffff") -(defn supports-interface? [contract hash cb] - (ethereum/call (ethereum/call-params contract "supportsInterface(bytes4)" hash) - #(cb %))) +(defn supports-interface? + [contract hash cb] + (json-rpc/eth-call + {:contract contract + :method "supportsInterface(bytes4)" + :params [hash] + :on-success cb})) (defn supports? "Calls cb with true if `supportsInterface` is supported by this contract. @@ -16,9 +21,9 @@ (supports-interface? contract supports-interface-hash - #(if (true? (ethereum/hex->boolean %)) + #(if (true? (abi-spec/hex-to-boolean %)) (supports-interface? contract marker-hash (fn [response] - (cb (false? (ethereum/hex->boolean response))))) + (cb (false? (abi-spec/hex-to-boolean response))))) (cb false)))) diff --git a/src/status_im/utils/ethereum/ens.cljs b/src/status_im/utils/ethereum/ens.cljs index 3edc6c1db75..e008d443cac 100644 --- a/src/status_im/utils/ethereum/ens.cljs +++ b/src/status_im/utils/ethereum/ens.cljs @@ -6,8 +6,8 @@ " (:refer-clojure :exclude [name]) (:require [clojure.string :as string] - [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.abi-spec :as abi-spec])) + [status-im.ethereum.json-rpc :as json-rpc] + [status-im.utils.ethereum.core :as ethereum])) ;; this is the addresses of ens registries for the different networks (def ens-registries @@ -17,7 +17,7 @@ (def default-namehash "0000000000000000000000000000000000000000000000000000000000000000") (def default-address "0x0000000000000000000000000000000000000000") -(def default-key "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") +(def default-key "0x0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") (defn namehash [s] @@ -35,28 +35,37 @@ (defn resolver [registry ens-name cb] - (ethereum/call (ethereum/call-params registry - "resolver(bytes32)" - (namehash ens-name)) - (fn [address] - (let [address (ethereum/hex->address address)] - (cb (if (and address (not= address default-address)) address "")))))) + (json-rpc/eth-call + {:contract registry + :method "resolver(bytes32)" + :params [(namehash ens-name)] + :outputs ["address"] + :on-success + (fn [[address]] + (when-not (= address default-address) + (cb address)))})) (defn owner [registry ens-name cb] - (ethereum/call (ethereum/call-params registry - "owner(bytes32)" - (namehash ens-name)) - (fn [address] - (cb address)))) + (json-rpc/eth-call + {:contract registry + :method "owner(bytes32)" + :params [(namehash ens-name)] + :outputs ["address"] + :on-success + (fn [[address]] + (cb address))})) (defn ttl [registry ens-name cb] - (ethereum/call (ethereum/call-params registry - "ttl(bytes32)" - (namehash ens-name)) - (fn [ttl] - (cb (ethereum/hex->int ttl))))) + (json-rpc/eth-call + {:contract registry + :method "ttl(bytes32)" + :params [(namehash ens-name)] + :outputs ["uint256"] + :on-success + (fn [[ttl]] + (cb ttl))})) ;; Resolver contract ;; Resolver must implement EIP65 (supportsInterface). When interacting with an unknown resolver it's safer to rely on it. @@ -65,9 +74,14 @@ (defn addr [resolver ens-name cb] - (ethereum/call (ethereum/call-params resolver "addr(bytes32)" (namehash ens-name)) - (fn [address] - (cb (ethereum/hex->address address))))) + (json-rpc/eth-call + {:contract resolver + :method "addr(bytes32)" + :params [(namehash ens-name)] + :outputs ["address"] + :on-success + (fn [[address]] + (cb address))})) (def name-hash "0x691f3431") @@ -75,37 +89,41 @@ (defn name [resolver ens-name cb] - (ethereum/call (ethereum/call-params resolver - "name(bytes32)" - (namehash ens-name)) - (fn [address] - (cb (ethereum/hex->address address))))) + (json-rpc/eth-call + {:contract resolver + :method "name(bytes32)" + :params [(namehash ens-name)] + :outputs ["string"] + :on-success + (fn [[name]] + (cb name))})) (defn contenthash [resolver ens-name cb] - (ethereum/call (ethereum/call-params resolver - "contenthash(bytes32)" - (namehash ens-name)) - (fn [hash] - (cb (first (abi-spec/decode hash ["bytes"])))))) + (json-rpc/eth-call + {:contract resolver + :method "contenthash(bytes32)" + :params [(namehash ens-name)] + :on-success + (fn [hash] + (cb hash))})) (defn content [resolver ens-name cb] - (ethereum/call (ethereum/call-params resolver - "content(bytes32)" - (namehash ens-name)) - (fn [hash] - (cb hash)))) + (json-rpc/eth-call + {:contract resolver + :method "content(bytes32)" + :params [(namehash ens-name)] + :on-success + (fn [hash] + (cb hash))})) (def ABI-hash "0x2203ab56") (def pubkey-hash "0xc8690233") -(defn add-uncompressed-public-key-prefix - [key] - (when (and key - (not= "0x" key) - (not= default-key key)) - (str "0x04" (subs key 2)))) +(defn uncompressed-public-key + [x y] + (str "0x04" x y)) (defn is-valid-eth-name? [ens-name] @@ -115,12 +133,16 @@ (defn pubkey [resolver ens-name cb] - (ethereum/call (ethereum/call-params resolver - "pubkey(bytes32)" - (namehash ens-name)) - (fn [key] - (when-let [public-key (add-uncompressed-public-key-prefix key)] - (cb public-key))))) + (json-rpc/eth-call + {:contract resolver + :method "pubkey(bytes32)" + :params [(namehash ens-name)] + :outputs ["bytes32" "bytes32"] + :on-success + (fn [[x y]] + (when-let [public-key (uncompressed-public-key x y)] + (when-not (= public-key default-key) + (cb public-key))))})) (defn get-addr [registry ens-name cb] diff --git a/src/status_im/utils/ethereum/erc721.cljs b/src/status_im/utils/ethereum/erc721.cljs index 3dc5d402992..3e05685c3f8 100644 --- a/src/status_im/utils/ethereum/erc721.cljs +++ b/src/status_im/utils/ethereum/erc721.cljs @@ -2,19 +2,22 @@ " Helper functions to interact with [ERC721](https://eips.ethereum.org/EIPS/eip-721) smart contract " - (:require [status-im.utils.ethereum.core :as ethereum])) + (:require [status-im.ethereum.json-rpc :as json-rpc])) -(defn token-of-owner-by-index [contract address index cb] - (ethereum/call (ethereum/call-params - contract - "tokenOfOwnerByIndex(address,uint256)" - (ethereum/normalized-address address) - (ethereum/int->hex index)) - #(cb (ethereum/hex->bignumber %)))) +(defn token-of-owner-by-index + [contract address index cb] + (json-rpc/eth-call + {:contract contract + :method "tokenOfOwnerByIndex(address,uint256)" + :params [address index] + :outputs ["uint256"] + :on-success (fn [[token]] (cb token))})) -(defn token-uri [contract tokenId cb] - (ethereum/call (ethereum/call-params - contract - "tokenURI(uint256)" - (ethereum/int->hex tokenId)) - #(cb (ethereum/hex->string %)))) +(defn token-uri + [contract tokenId cb] + (json-rpc/eth-call + {:contract contract + :method "tokenURI(uint256)" + :params [tokenId] + :outputs ["string"] + :on-success (fn [[uri]] (cb uri))})) diff --git a/src/status_im/utils/ethereum/resolver.cljs b/src/status_im/utils/ethereum/resolver.cljs index 291409f6acc..18357ed8453 100644 --- a/src/status_im/utils/ethereum/resolver.cljs +++ b/src/status_im/utils/ethereum/resolver.cljs @@ -3,7 +3,6 @@ (:refer-clojure :exclude [name])) (def default-hash "0x0000000000000000000000000000000000000000000000000000000000000000") - (defn contenthash [registry ens-name cb] (ens/resolver registry ens-name diff --git a/src/status_im/utils/ethereum/stickers.cljs b/src/status_im/utils/ethereum/stickers.cljs deleted file mode 100644 index fbd39a49b27..00000000000 --- a/src/status_im/utils/ethereum/stickers.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.utils.ethereum.stickers - (:require [status-im.utils.ethereum.core :as ethereum] - [status-im.utils.ethereum.abi-spec :as abi-spec])) - -(def contracts - {:mainnet nil - :testnet "0x39d16CdB56b5a6a89e1A397A13Fe48034694316E" - :rinkeby nil}) - -(defn pack-count - "Returns number of packs rigestered in the contract" - [contract cb] - (ethereum/call (ethereum/call-params contract "packCount()") - (fn [count] (cb (ethereum/hex->int count))))) - -(defn pack-data - "Returns vector of pack data parameters by pack id: [category owner mintable timestamp price contenthash]" - [contract pack-id cb] - (ethereum/call (ethereum/call-params contract "getPackData(uint256)" (ethereum/int->hex pack-id)) - (fn [data] - (cb (abi-spec/decode data ["bytes4[]" "address" "bool" "uint256" "uint256" "bytes"]))))) - -(defn owned-tokens - "Returns vector of owned tokens ids in the contract by address" - [contract address cb] - (ethereum/call (ethereum/call-params contract "tokensOwnedBy(address)" (ethereum/normalized-address address)) - (fn [data] - (cb (first (abi-spec/decode data ["uint256[]"])))))) - -(defn token-pack-id - "Returns pack id in the contract by token id" - [contract token cb] - (ethereum/call (ethereum/call-params contract "tokenPackId(uint256)" (ethereum/int->hex token)) - (fn [data] (cb (ethereum/hex->int data))))) diff --git a/src/status_im/wallet/core.cljs b/src/status_im/wallet/core.cljs index f1a7adba800..27fb8a1d04f 100644 --- a/src/status_im/wallet/core.cljs +++ b/src/status_im/wallet/core.cljs @@ -9,6 +9,7 @@ [status-im.ui.screens.wallet.utils :as wallet.utils] [status-im.utils.config :as config] [status-im.utils.core :as utils.core] + [status-im.utils.ethereum.abi-spec :as abi-spec] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.ethereum.tokens :as tokens] [status-im.utils.fx :as fx] @@ -256,9 +257,9 @@ [from {:keys [amount to gas gas-price data nonce]}] (cond-> {:from (ethereum/normalized-address from) :to (ethereum/normalized-address to) - :value (ethereum/int->hex amount) - :gas (ethereum/int->hex gas) - :gasPrice (ethereum/int->hex gas-price)} + :value (str "0x" (abi-spec/number-to-hex amount)) + :gas (str "0x" (abi-spec/number-to-hex gas)) + :gasPrice (str "0x" (abi-spec/number-to-hex gas-price))} data (assoc :data data) nonce @@ -314,15 +315,15 @@ (when on-error {:dispatch (conj on-error "transaction was cancelled by user")})))) -(defn prepare-unconfirmed-transaction [db now hash] +(defn prepare-unconfirmed-transaction + [db now hash] (let [transaction (get-in db [:wallet :send-transaction]) all-tokens (:wallet/all-tokens db)] (let [chain (:chain db) token (tokens/symbol->token all-tokens (keyword chain) (:symbol transaction))] (-> transaction - (assoc :confirmations "0" - :timestamp (str now) - :type :outbound + (assoc :timestamp (str now) + :type :pending :hash hash :value (:amount transaction) :token token @@ -466,30 +467,6 @@ :wallet-send-modal-stack :wallet-send-modal-stack-with-onboarding)]]})) -(fx/defn open-sign-transaction-flow - [{:keys [db] :as cofx} - {:keys [gas gas-price] :as transaction}] - (let [go-to-view-id (if (get-in db [:account/account :wallet-set-up-passed?]) - :wallet-send-modal-stack - :wallet-send-modal-stack-with-onboarding)] - (fx/merge cofx - (cond-> {:db (-> db - (assoc-in [:navigation/screen-params :wallet-send-modal-stack :modal?] true) - (assoc-in [:wallet :send-transaction] - transaction) - (assoc-in [:wallet :send-transaction :original-gas] - gas))} - (not gas) - (assoc :wallet/update-estimated-gas - {:obj (select-keys transaction [:to :data]) - :success-event :wallet/update-estimated-gas-success}) - - (not gas-price) - (assoc :wallet/update-gas-price - {:success-event :wallet/update-gas-price-success - :edit? false})) - (navigation/navigate-to-cofx go-to-view-id {})))) - (defn send-transaction-screen-did-load [{:keys [db]}] {:db (assoc-in db @@ -575,3 +552,34 @@ (toggle-visible-token symbol true) ;;TODO(goranjovic): move `update-token-balance-success` function to wallet models (update-token-balance symbol balance))) + +(fx/defn eth-transaction-call + [{:keys [db] :as cofx} + {:keys [contract method params on-success on-error details] :as transaction}] + (let [current-address (ethereum/current-address db) + transaction (merge {:to contract + :from current-address + :data (abi-spec/encode method params) + :id "approve" + :symbol :ETH + :method "eth_sendTransaction" + :amount (money/bignumber 0) + :on-success on-success + :on-error on-error} + details) + go-to-view-id (if (get-in db [:account/account :wallet-set-up-passed?]) + :wallet-send-modal-stack + :wallet-send-modal-stack-with-onboarding)] + (fx/merge cofx + {:db (-> db + (assoc-in [:navigation/screen-params :wallet-send-modal-stack :modal?] true) + (assoc-in [:wallet :send-transaction] + transaction)) + :wallet/update-estimated-gas + {:obj (select-keys transaction [:to :from :data]) + :success-event :wallet/update-estimated-gas-success} + + :wallet/update-gas-price + {:success-event :wallet/update-gas-price-success + :edit? false}} + (navigation/navigate-to-cofx go-to-view-id {})))) diff --git a/src/status_im/wallet/custom_tokens/core.cljs b/src/status_im/wallet/custom_tokens/core.cljs index 85fa604a15e..7abb66fc4e2 100644 --- a/src/status_im/wallet/custom_tokens/core.cljs +++ b/src/status_im/wallet/custom_tokens/core.cljs @@ -1,136 +1,210 @@ (ns status-im.wallet.custom-tokens.core - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] + (:require [clojure.string :as string] + [re-frame.core :as re-frame] + [status-im.ethereum.decode :as decode] + [status-im.ethereum.json-rpc :as json-rpc] [status-im.i18n :as i18n] [status-im.ui.components.colors :as colors] [status-im.ui.components.react :as react] [status-im.utils.ethereum.core :as ethereum] - [clojure.string :as string] - [status-im.ethereum.decode :as decode] + [status-im.utils.money :as money] [status-im.utils.fx :as fx] [status-im.wallet.core :as wallet])) (re-frame/reg-fx :wallet.custom-token/get-decimals (fn [contract] - (ethereum/call - (ethereum/call-params contract "decimals()") - #(re-frame/dispatch [:wallet.custom-token/decimals-result %])))) + (json-rpc/eth-call + {:contract contract + :method "decimals()" + :outputs ["uint256"] + :on-success + (fn [[contract-decimals]] + (re-frame/dispatch [:wallet.custom-token/decimals-result + contract-decimals]))}))) (re-frame/reg-fx :wallet.custom-token/get-symbol (fn [contract] - (ethereum/call - (ethereum/call-params contract "symbol()") - #(re-frame/dispatch [:wallet.custom-token/symbol-result contract %])))) + (json-rpc/eth-call + {:contract contract + :method "symbol()" + :outputs ["string"] + :on-success + (fn [[contract-symbol]] + (re-frame/dispatch [:wallet.custom-token/symbol-result + contract + contract-symbol]))}))) (re-frame/reg-fx :wallet.custom-token/get-balance - (fn [[contract address]] - (ethereum/call - (ethereum/call-params contract "balanceOf(address)" (ethereum/normalized-address address)) - #(re-frame/dispatch [:wallet.custom-token/balance-result contract %])))) + (fn [[contract wallet-address]] + (json-rpc/eth-call + {:contract contract + :method "balanceOf(address)" + :params [wallet-address] + :outputs ["uint256"] + :on-success + (fn [[balance]] + (re-frame/dispatch [:wallet.custom-token/balance-result + contract + (money/bignumber balance)]))}))) (re-frame/reg-fx :wallet.custom-token/get-name (fn [contract] - (ethereum/call - (ethereum/call-params contract "name()") - #(re-frame/dispatch [:wallet.custom-token/name-result contract %])))) + (json-rpc/eth-call + {:contract contract + :method "name()" + :outputs ["string"] + :on-success + (fn [[contract-name]] + (re-frame/dispatch [:wallet.custom-token/name-result + contract + contract-name]))}))) (re-frame/reg-fx :wallet.custom-token/get-total-supply (fn [contract] - (ethereum/call - (ethereum/call-params contract "totalSupply()") - #(re-frame/dispatch [:wallet.custom-token/total-supply-result contract %])))) + (json-rpc/eth-call + {:contract contract + :method "totalSupply()" + :outputs ["uint256"] + :on-success + (fn [[contract-total-supply]] + (re-frame/dispatch [:wallet.custom-token/total-supply-result + contract + (money/bignumber contract-total-supply)]))}))) (re-frame/reg-fx :wallet.custom-token/contract-address-paste (fn [] - (react/get-from-clipboard #(re-frame/dispatch [:wallet.custom-token/contract-address-is-pasted (string/trim %)])))) + (react/get-from-clipboard + #(re-frame/dispatch [:wallet.custom-token/contract-address-is-pasted + (string/trim %)])))) -(defn field-exists? [{:wallet/keys [all-tokens] :as db} field-key field-value] +(defn field-exists? + [{:wallet/keys [all-tokens] :as db} field-key field-value] (let [chain-key (ethereum/get-chain-keyword db)] - (some #(= field-value (get % field-key)) (vals (get all-tokens chain-key))))) - -(fx/defn total-supply-result [{:keys [db]} contract result] - (if (and (string? result) (string/starts-with? result "0x") (> (count result) 2)) - {:wallet.custom-token/get-balance [contract (get-in db [:account/account :address])]} - {:db (update db :wallet/custom-token-screen merge {:in-progress? nil :error (i18n/label :t/wrong-contract)})})) - -(defn token-in-list? [{:wallet/keys [all-tokens] :as db} contract] + (some #(= field-value (get % field-key)) + (vals (get all-tokens chain-key))))) + +(fx/defn total-supply-result + [{:keys [db]} contract total-supply] + (if (money/valid? total-supply) + {:wallet.custom-token/get-balance + [contract (ethereum/current-address db)]} + {:db (update db + :wallet/custom-token-screen + merge {:in-progress? nil + :error (i18n/label :t/wrong-contract)})})) + +(defn token-in-list? + [{:wallet/keys [all-tokens] :as db} contract] (let [chain-key (ethereum/get-chain-keyword db) addresses (set (map string/lower-case (keys (get all-tokens chain-key))))] (not (nil? (get addresses (string/lower-case contract)))))) -(fx/defn contract-address-is-changed [{:keys [db]} contract] +(fx/defn contract-address-is-changed + [{:keys [db]} contract] (if (ethereum/address? contract) (if (token-in-list? db contract) - {:db (assoc db :wallet/custom-token-screen {:contract contract :error (i18n/label :t/already-have-asset)})} - {:db (assoc db :wallet/custom-token-screen {:contract contract :in-progress? true}) + {:db (assoc db + :wallet/custom-token-screen + {:contract contract :error (i18n/label :t/already-have-asset)})} + {:db (assoc db + :wallet/custom-token-screen + {:contract contract :in-progress? true}) :wallet.custom-token/get-total-supply contract}) - {:db (assoc db :wallet/custom-token-screen {:contract contract :error (i18n/label :t/wrong-address)})})) - -(fx/defn decimals-result [{:keys [db]} result] - {:db (update db :wallet/custom-token-screen merge {:decimals (str (decode/uint result)) - :in-progress? nil})}) - -(fx/defn symbol-result [{:keys [db]} contract result] - (let [token-symbol (decode/string result) - symbol-exists? (field-exists? db :symbol (keyword token-symbol))] + {:db (assoc db + :wallet/custom-token-screen + {:contract contract + :error (i18n/label :t/wrong-address)})})) + +(fx/defn decimals-result + [{:keys [db]} result] + {:db (update db + :wallet/custom-token-screen + merge + {:decimals (str (decode/uint result)) + :in-progress? nil})}) + +(fx/defn symbol-result + [{:keys [db]} contract token-symbol] + (let [symbol-exists? (field-exists? db :symbol (keyword token-symbol))] {:db - (update db :wallet/custom-token-screen merge + (update db + :wallet/custom-token-screen merge {:symbol token-symbol :error-symbol (when symbol-exists? (i18n/label :t/you-already-have-an-asset {:value token-symbol}))}) :wallet.custom-token/get-decimals contract})) -(fx/defn name-result [{:keys [db]} contract result] - (let [token-name (decode/string result) - name-exists? (field-exists? db :name token-name)] +(fx/defn name-result + [{:keys [db]} contract token-name] + (let [name-exists? (field-exists? db :name token-name)] {:db (update db :wallet/custom-token-screen merge {:name token-name :error-name (when name-exists? - (i18n/label :t/you-already-have-an-asset {:value token-name}))}) - :wallet.custom-token/get-symbol - contract})) - -(fx/defn balance-result [{:keys [db]} contract result] - (if (and (string? result) (string/starts-with? result "0x") (> (count result) 2)) - {:db (assoc-in db [:wallet/custom-token-screen :balance] (str (decode/uint result))) + (i18n/label :t/you-already-have-an-asset + {:value token-name}))}) + :wallet.custom-token/get-symbol contract})) + +(fx/defn balance-result + [{:keys [db]} contract balance] + (if (money/valid? balance) + {:db (assoc-in db + [:wallet/custom-token-screen :balance] + (str balance)) :wallet.custom-token/get-name contract} - {:db (update db :wallet/custom-token-screen merge {:in-progress? nil :error (i18n/label :t/wrong-contract)})})) - -(fx/defn add-custom-token [{:keys [db] :as cofx}] + {:db (update db + :wallet/custom-token-screen + merge + {:in-progress? nil + :error (i18n/label :t/wrong-contract)})})) + +(fx/defn add-custom-token + [{:keys [db] :as cofx}] (let [{:keys [contract name symbol decimals]} (get db :wallet/custom-token-screen) chain-key (ethereum/get-chain-keyword db) symbol (keyword symbol) - new-token {:address contract :name name :symbol symbol :custom? true - :decimals (int decimals) :color (rand-nth colors/chat-colors)}] - (fx/merge (assoc-in cofx [:db :wallet/all-tokens chain-key contract] new-token) + new-token {:address contract + :name name + :symbol symbol + :custom? true + :decimals (int decimals) + :color (rand-nth colors/chat-colors)}] + (fx/merge (assoc-in cofx + [:db :wallet/all-tokens chain-key contract] + new-token) (wallet/add-custom-token new-token)))) -(fx/defn remove-custom-token [{:keys [db] :as cofx} {:keys [address] :as token}] +(fx/defn remove-custom-token + [{:keys [db] :as cofx} {:keys [address] :as token}] (let [chain-key (ethereum/get-chain-keyword db)] (fx/merge (update-in cofx [:db :wallet/all-tokens chain-key] dissoc address) (wallet/remove-custom-token token)))) -(fx/defn field-is-edited [{:keys [db] :as cofx} field-key value] +(fx/defn field-is-edited + [{:keys [db] :as cofx} field-key value] (case field-key :contract (contract-address-is-changed cofx value) - :name {:db (update db :wallet/custom-token-screen merge + :name {:db (update db + :wallet/custom-token-screen merge {field-key value :error-name (when (field-exists? db field-key value) - (i18n/label :t/you-already-have-an-asset {:value value}))})} + (i18n/label :t/you-already-have-an-asset + {:value value}))})} :symbol {:db (update db :wallet/custom-token-screen merge {field-key value :error-symbol (when (field-exists? db field-key (keyword value)) (i18n/label :t/you-already-have-an-asset {:value value}))})} - :decimals {:db (assoc-in db [:wallet/custom-token-screen :decimals] value)})) + :decimals {:db (assoc-in db + [:wallet/custom-token-screen :decimals] + value)})) diff --git a/test/cljs/status_im/test/utils/ethereum/core.cljs b/test/cljs/status_im/test/utils/ethereum/core.cljs index 733ed8baeb3..629ceb6070e 100644 --- a/test/cljs/status_im/test/utils/ethereum/core.cljs +++ b/test/cljs/status_im/test/utils/ethereum/core.cljs @@ -1,17 +1,9 @@ (ns status-im.test.utils.ethereum.core - (:require [cljs.test :refer-macros [deftest is testing]] + (:require [cljs.test :refer-macros [deftest is]] [status-im.utils.ethereum.core :as ethereum])) -(deftest call-params - (testing "ERC20 balance-of params" - (let [contract "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5" - address "0xa7cfd581060ec66414790691681732db249502bd"] - (is (= (ethereum/call-params contract "balanceOf(address)" address) - {:to "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5" - :data "0x70a08231000000000000000000000000a7cfd581060ec66414790691681732db249502bd"}))))) - (deftest chain-id->chain-keyword (is (= (ethereum/chain-id->chain-keyword 1) :mainnet)) (is (= (ethereum/chain-id->chain-keyword 3) :testnet)) (is (= (ethereum/chain-id->chain-keyword 4) :rinkeby)) - (is (= (ethereum/chain-id->chain-keyword 5777) :custom))) \ No newline at end of file + (is (= (ethereum/chain-id->chain-keyword 5777) :custom))) From 4103591fe169a96a6c2066b1c9b51c9ebeba8247 Mon Sep 17 00:00:00 2001 From: yenda Date: Tue, 21 May 2019 19:08:48 +0200 Subject: [PATCH 8/8] [fix] always show tokens that have a balance - temporary fix before visible assets are fixed - the problem is that you can't hide an asset with the way it currently works, it will come back every time as long as there is a balance for it Signed-off-by: yenda --- src/status_im/wallet/core.cljs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/status_im/wallet/core.cljs b/src/status_im/wallet/core.cljs index 27fb8a1d04f..afd1e8a0224 100644 --- a/src/status_im/wallet/core.cljs +++ b/src/status_im/wallet/core.cljs @@ -383,8 +383,7 @@ chain (ethereum/chain-keyword db) assets (get-in settings [:wallet :visible-tokens chain]) tokens (->> (tokens/tokens-for all-tokens chain) - (remove #(or (:hidden? %))) - (filter #((or assets identity) (:symbol %))))] + (remove #(or (:hidden? %))))] (when (not= network-status :offline) (fx/merge cofx @@ -400,10 +399,11 @@ :tokens tokens :on-success (fn [symbol balance] - (if assets + (if (and assets + (assets symbol)) (re-frame/dispatch [:wallet.callback/update-token-balance-success symbol balance]) - ;; NOTE: when there is no visible assets set, + ;; NOTE: when there it is not a visible assets ;; we make an initialization round (when (> balance 0) (re-frame/dispatch