[#5138 #4851 #4865] automatic periodic tx state updates

Fetch transactions state from etherscan every 15 seconds
if there are any unconfirmed transactions

Signed-off-by: Dmitry Novotochinov <dmitry.novot@gmail.com>
This commit is contained in:
Dmitry Novotochinov 2018-07-18 16:29:04 +03:00
parent 0e502ac5b8
commit 2d6fdc0ecc
No known key found for this signature in database
GPG Key ID: 43D1DAF5AD39C927
11 changed files with 278 additions and 41 deletions

View File

@ -6,7 +6,8 @@
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.utils.clocks :as utils.clocks] [status-im.utils.clocks :as utils.clocks]
[status-im.utils.handlers :as handlers] [status-im.utils.handlers :as handlers]
[status-im.utils.handlers-macro :as handlers-macro])) [status-im.utils.handlers-macro :as handlers-macro]
[status-im.wallet.transactions :as wallet.transactions]))
;;;; Handlers ;;;; Handlers
@ -46,7 +47,7 @@
(let [{:keys [command params]} content (let [{:keys [command params]} content
tx-hash (:tx-hash params)] tx-hash (:tx-hash params)]
(handlers-macro/merge-fx cofx (handlers-macro/merge-fx cofx
(message-model/update-transactions command tx-hash {:with-delay? true}) (wallet.transactions/store-chat-transaction-hash tx-hash)
(request-command-message-data message))) (request-command-message-data message)))
;; regular non command message, we can add it right away ;; regular non command message, we can add it right away
(message-model/receive message cofx)))) (message-model/receive message cofx))))

View File

@ -433,16 +433,6 @@
:show? true} :show? true}
chat))) chat)))
;; dispatch :update-transactions to update confirmations count
;; to verify tx initiated with /send command is confirmed
(defn update-transactions [command-name tx-hash {:keys [with-delay?]} _]
(when (and tx-hash
(= command-name constants/command-send))
(cond-> {:dispatch [:update-transactions]}
with-delay?
(assoc :dispatch-later [{:ms constants/command-send-status-update-interval-ms
:dispatch [:update-transactions]}]))))
(defn send-command (defn send-command
[{{:keys [current-public-key chats chain prices] :as db} :db :keys [now] :as cofx} params] [{{:keys [current-public-key chats chain prices] :as db} :db :keys [now] :as cofx} params]
(let [{{:keys [handler-data to-message command] :as content} :command chat-id :chat-id} params (let [{{:keys [handler-data to-message command] :as content} :command chat-id :chat-id} params
@ -456,8 +446,7 @@
(upsert-and-send (prepare-command-message current-public-key chat now request content (upsert-and-send (prepare-command-message current-public-key chat now request content
chain currency prices tx-hash)) chain currency prices tx-hash))
(console-events/console-respond-command-messages command handler-data) (console-events/console-respond-command-messages command handler-data)
(requests-events/request-answered chat-id to-message) (requests-events/request-answered chat-id to-message))))
(update-transactions command-name tx-hash {:with-delay? false}))))
(defn invoke-console-command-handler (defn invoke-console-command-handler
[{:keys [db] :as cofx} {:keys [command] :as command-params}] [{:keys [db] :as cofx} {:keys [command] :as command-params}]

View File

@ -10,7 +10,8 @@
[status-im.utils.platform :as platform] [status-im.utils.platform :as platform]
[status-im.utils.gfycat.core :as gfycat] [status-im.utils.gfycat.core :as gfycat]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.constants :as const])) [status-im.constants :as const]
[status-im.wallet.transactions :as transactions]))
(reg-sub :get-chats :chats) (reg-sub :get-chats :chats)
@ -405,7 +406,7 @@
(fn [db [_ tx-hash]] (fn [db [_ tx-hash]]
(-> (get-in db [:wallet :transactions tx-hash :confirmations] "0") (-> (get-in db [:wallet :transactions tx-hash :confirmations] "0")
(js/parseInt) (js/parseInt)
(pos?)))) (>= transactions/confirmations-count-threshold))))
(reg-sub (reg-sub
:wallet-transaction-exists? :wallet-transaction-exists?

View File

@ -60,7 +60,6 @@
(letsubs [confirmed? [:transaction-confirmed? tx-hash] (letsubs [confirmed? [:transaction-confirmed? tx-hash]
tx-exists? [:wallet-transaction-exists? tx-hash]] tx-exists? [:wallet-transaction-exists? tx-hash]]
[react/touchable-highlight {:on-press #(when tx-exists? [react/touchable-highlight {:on-press #(when tx-exists?
(re-frame/dispatch [:update-transactions])
(re-frame/dispatch [:show-transaction-details tx-hash]))} (re-frame/dispatch [:show-transaction-details tx-hash]))}
[react/view style/command-send-status-container [react/view style/command-send-status-container
[vector-icons/icon (if confirmed? :icons/check :icons/dots) [vector-icons/icon (if confirmed? :icons/check :icons/dots)

View File

@ -35,6 +35,7 @@
:discover-current-dapp {} :discover-current-dapp {}
:tags [] :tags []
:sync-state :done :sync-state :done
:app-state "active"
:wallet.transactions constants/default-wallet-transactions :wallet.transactions constants/default-wallet-transactions
:wallet-selected-asset {} :wallet-selected-asset {}
:prices {} :prices {}
@ -77,6 +78,8 @@
(spec/def ::mailserver-status (spec/nilable keyword?)) (spec/def ::mailserver-status (spec/nilable keyword?))
(spec/def ::app-state string?)
;;;;NODE ;;;;NODE
(spec/def ::sync-listening-started (spec/nilable boolean?)) (spec/def ::sync-listening-started (spec/nilable boolean?))
@ -231,6 +234,7 @@
::sync-data ::sync-data
::network ::network
::chain ::chain
::app-state
:navigation/view-id :navigation/view-id
:navigation/navigation-stack :navigation/navigation-stack
:navigation/prev-tab-view-id :navigation/prev-tab-view-id

View File

@ -377,6 +377,7 @@
[:process-pending-messages] [:process-pending-messages]
[:update-wallet] [:update-wallet]
[:update-transactions] [:update-transactions]
[:sync-wallet-transactions]
[:get-fcm-token] [:get-fcm-token]
[:update-sign-in-time]] [:update-sign-in-time]]
(seq events-after) (into events-after))})) (seq events-after) (into events-after))}))
@ -489,10 +490,11 @@
(handlers/register-handler-fx (handlers/register-handler-fx
:app-state-change :app-state-change
(fn [{{:keys [network-status mailserver-status]} :db :as cofx} [_ state]] (fn [{:keys [db] :as cofx} [_ state]]
(let [app-coming-from-background? (= state "active")] (let [app-coming-from-background? (= state "active")]
(handlers-macro/merge-fx cofx (handlers-macro/merge-fx cofx
{::app-state-change-fx state} {::app-state-change-fx state
:db (assoc db :app-state state)}
(inbox/request-messages app-coming-from-background?))))) (inbox/request-messages app-coming-from-background?)))))
(handlers/register-handler-fx (handlers/register-handler-fx

View File

@ -14,7 +14,8 @@
status-im.ui.screens.wallet.request.events status-im.ui.screens.wallet.request.events
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.ui.screens.navigation :as navigation] [status-im.ui.screens.navigation :as navigation]
[status-im.utils.money :as money])) [status-im.utils.money :as money]
[status-im.wallet.transactions :as wallet.transactions]))
(defn get-balance [{:keys [web3 account-id on-success on-error]}] (defn get-balance [{:keys [web3 account-id on-success on-error]}]
(if (and web3 account-id) (if (and web3 account-id)
@ -141,22 +142,8 @@
(handlers/register-handler-fx (handlers/register-handler-fx
:update-transactions :update-transactions
(fn [{{:keys [network network-status web3] :as db} :db} _] (fn [cofx _]
(when-not (= network-status :offline) (wallet.transactions/run-update cofx)))
(let [network (get-in db [:account/account :networks network])
chain (ethereum/network->chain-keyword network)]
(when-not (= :custom chain)
(let [all-tokens (tokens/tokens-for chain)
token-addresses (map :address all-tokens)]
{:get-transactions {:account-id (get-in db [:account/account :address])
:token-addresses token-addresses
:chain chain
:web3 web3
:success-event :update-transactions-success
:error-event :update-transactions-fail}
:db (-> db
(clear-error-message :transactions-update)
(assoc-in [:wallet :transactions-loading?] true))}))))))
(defn combine-entries [transaction token-transfer] (defn combine-entries [transaction token-transfer]
(merge transaction (select-keys token-transfer [:symbol :from :to :value :type :token :transfer]))) (merge transaction (select-keys token-transfer [:symbol :from :to :value :type :token :transfer])))

View File

@ -20,7 +20,8 @@
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.transport.utils :as transport.utils] [status-im.transport.utils :as transport.utils]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[status-im.ui.screens.navigation :as navigation])) [status-im.ui.screens.navigation :as navigation]
[status-im.wallet.transactions :as wallet.transactions]))
;;;; FX ;;;; FX
@ -467,3 +468,10 @@
(fn [{:keys [db]} [_ chat-id]] (fn [{:keys [db]} [_ chat-id]]
{:dispatch [:navigate-back] {:dispatch [:navigate-back]
:dispatch-later [{:ms 400 :dispatch [:check-transactions-queue]}]})) :dispatch-later [{:ms 400 :dispatch [:check-transactions-queue]}]}))
(handlers/register-handler-fx
:sync-wallet-transactions
(fn [cofx _]
(handlers-macro/merge-fx cofx
(wallet.transactions/load-missing-chat-transactions)
(wallet.transactions/sync))))

View File

@ -1,6 +1,7 @@
(ns status-im.utils.transactions (ns status-im.utils.transactions
(:require [status-im.utils.http :as http] (:require [status-im.utils.http :as http]
[status-im.utils.types :as types])) [status-im.utils.types :as types]
[taoensso.timbre :as log]))
(defn- get-network-subdomain [chain] (defn- get-network-subdomain [chain]
(case chain (case chain
@ -55,6 +56,8 @@
{}))) {})))
(defn get-transactions [chain account on-success on-error] (defn get-transactions [chain account on-success on-error]
(http/get (get-transaction-url chain account) (let [url (get-transaction-url chain account)]
#(on-success (format-transactions-response % account)) (log/debug "HTTP GET" url)
on-error)) (http/get url
#(on-success (format-transactions-response % account))
on-error)))

View File

@ -0,0 +1,105 @@
(ns status-im.wallet.transactions
(:require [clojure.set :as set]
[status-im.utils.datetime :as time]
[status-im.utils.ethereum.tokens :as tokens]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.handlers-macro :as handlers-macro]
[taoensso.timbre :as log]))
(def sync-interval-ms 15000)
(def confirmations-count-threshold 12)
;; Detects if some of the transactions have less than 12 confirmations
(defn- have-unconfirmed-transactions? [cofx]
(->> (get-in cofx [:db :wallet :transactions])
vals
(map :confirmations)
(map int)
(some #(< % confirmations-count-threshold))))
(defn- wallet-transactions-set [db]
(-> db
(get-in [:wallet :transactions])
keys
set))
;; Detects if some of missing chat transactions are missing from wallet
(defn- have-missing-chat-transactions? [{:keys [db]}]
(let [chat-transactions (get-in db [:wallet :chat-transactions])]
(not= (count chat-transactions)
(count (set/intersection
chat-transactions
(wallet-transactions-set db))))))
(defn- schedule-sync [cofx]
{:utils/dispatch-later [{:ms sync-interval-ms
:dispatch [:sync-wallet-transactions]}]})
(defn store-chat-transaction-hash [tx-hash {:keys [db]}]
{:db (update-in db [:wallet :chat-transactions] conj tx-hash)})
(defn- missing-chat-transactions [{:keys [db] :as cofx}]
(let [chat-transactions (->> db
:chats
vals
(remove :public?)
(mapcat :messages)
vals
flatten
(filter #(= "command" (:content-type %)))
(map #(get-in % [:content :params :tx-hash]))
(filter identity)
set)]
(set/difference
chat-transactions
(wallet-transactions-set db))))
; Find missing chat transactions
; and store them at [:wallet :chat-transactions]
; to be used later by have-missing-chat-transactions? on every sync request
(defn load-missing-chat-transactions [{:keys [db] :as cofx}]
(when (nil? (get-in db [:wallet :chat-transactions]))
{:db (assoc-in db
[:wallet :chat-transactions]
(missing-chat-transactions cofx))}))
(defn run-update [{{:keys [network network-status web3] :as db} :db}]
(when (not= network-status :offline)
(let [network (get-in db [:account/account :networks network])
chain (ethereum/network->chain-keyword network)]
(when-not (= :custom chain)
(let [all-tokens (tokens/tokens-for chain)
token-addresses (map :address all-tokens)]
(log/debug "Syncing transactions data..")
{:get-transactions {:account-id (get-in db [:account/account :address])
:token-addresses token-addresses
:chain chain
:web3 web3
:success-event :update-transactions-success
:error-event :update-transactions-fail}
:db (-> db
(update-in [:wallet :errors] dissoc :transactions-update)
(assoc-in [:wallet :transactions-loading?] true)
(assoc-in [:wallet :transactions-last-updated-at] (time/timestamp)))})))))
(defn- time-to-sync? [cofx]
(let [last-updated-at (get-in cofx [:db :wallet :transactions-last-updated-at])]
(or (nil? last-updated-at)
(< sync-interval-ms
(- (time/timestamp) last-updated-at)))))
; Fetch updated data for any unconfirmed transactions or incoming chat transactions missing in wallet
; and schedule new recurring sync request
(defn sync [{:keys [db] :as cofx}]
(let [in-progress? (get-in db [:wallet :transactions-loading?])
{:keys [app-state network-status]} db]
(if (and (not= network-status :offline)
(= app-state "active")
(not in-progress?)
(time-to-sync? cofx)
(or (have-unconfirmed-transactions? cofx)
(have-missing-chat-transactions? cofx)))
(handlers-macro/merge-fx cofx
(run-update)
(schedule-sync))
(schedule-sync cofx))))

View File

@ -0,0 +1,138 @@
(ns status-im.test.wallet.transactions
(:require [cljs.test :refer-macros [deftest is testing]]
[status-im.utils.datetime :as time]
[status-im.wallet.transactions :as wallet.transactions]))
(deftest test-store-chat-transaction-hash
(is (= (wallet.transactions/store-chat-transaction-hash "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"
{:db {:wallet {:chat-transactions #{"0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b"}}}})
{:db {:wallet {:chat-transactions #{"0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b" "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"}}}})))
(deftest test-load-missing-chat-transactions
(is (= (wallet.transactions/load-missing-chat-transactions {:db {:wallet {:transactions {"0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"
{:confirmations "15"}}}
:chats {"0x0462203ba96495c4caed83b46b01fe9b8f27af300f46cc944c6c47141969337ecfc4566dd5910323763816d350d3256a3d8f2f2a6536cfa120733aeea2efb11860"
{:messages {"0xfc6e31d35f5a722b8aa185c8c7d1cd798e546e8dcd97adcc8305ca1a3c044f0d"
{:content-type "command"
:content {:params {:tx-hash "0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b"}}}}}
"0x04d9bbe8e00318a70108980b45ba45c91cd406cdd7ffeb043be0ce4ec026fed3854515345b4bbce4c1694cc99240ee5fda93101ca861c3ceef548dedb5ec2a983a"
{:messages {"0x1f8a9472997b58435a43776ec5a89a12e42891f71064fd890ef3d87dda41fabf"
{:content-type "command"
:content {:params {:tx-hash "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"}}}}}}}})
{:db {:wallet {:transactions {"0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"
{:confirmations "15"}}
:chat-transactions #{"0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b"}}
:chats {"0x0462203ba96495c4caed83b46b01fe9b8f27af300f46cc944c6c47141969337ecfc4566dd5910323763816d350d3256a3d8f2f2a6536cfa120733aeea2efb11860"
{:messages {"0xfc6e31d35f5a722b8aa185c8c7d1cd798e546e8dcd97adcc8305ca1a3c044f0d"
{:content-type "command"
:content {:params {:tx-hash "0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b"}}}}}
"0x04d9bbe8e00318a70108980b45ba45c91cd406cdd7ffeb043be0ce4ec026fed3854515345b4bbce4c1694cc99240ee5fda93101ca861c3ceef548dedb5ec2a983a"
{:messages {"0x1f8a9472997b58435a43776ec5a89a12e42891f71064fd890ef3d87dda41fabf"
{:content-type "command"
:content {:params {:tx-hash "0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"}}}}}}}})))
(deftest test-sync
(with-redefs [time/timestamp (constantly 1531734120829)]
(testing "update in progress"
(is (= (wallet.transactions/sync {:db {:app-state "active"
:network-status :online
:wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295"
{:confirmations "0"}}
:transactions-loading? true}}})
{:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms
:dispatch [:sync-wallet-transactions]}]})))
(testing "app is offline"
(is (= (wallet.transactions/sync {:db {:app-state "active"
:network-status :offline
:wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295"
{:confirmations "0"}}}}})
{:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms
:dispatch [:sync-wallet-transactions]}]})))
(testing "app is in background"
(is (= (wallet.transactions/sync {:db {:app-state "background"
:network-status :online
:wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295"
{:confirmations "0"}}}}})
{:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms
:dispatch [:sync-wallet-transactions]}]})))
(testing "last request was made recently"
(is (= (wallet.transactions/sync {:db {:app-state "active"
:network-status :online
:wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295"
{:confirmations "10"}}
:transactions-last-updated-at 1531734119729
:transactions-loading? false}}})
{:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms
:dispatch [:sync-wallet-transactions]}]})))
(testing "transaction has 0 confirmations"
(is (= (wallet.transactions/sync {:db {:app-state "active"
:network-status :online
:wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295"
{:confirmations "0"}}
:transactions-loading? false}}})
{:get-transactions {:account-id nil
:token-addresses (),
:chain nil
:web3 nil
:success-event :update-transactions-success
:error-event :update-transactions-fail}
:db {:app-state "active"
:network-status :online
:wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295"
{:confirmations "0"}}
:transactions-loading? true
:transactions-last-updated-at 1531734120829
:errors nil}}
:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms
:dispatch [:sync-wallet-transactions]}]})))
(testing "transaction has 10 confirmations"
(is (= (wallet.transactions/sync {:db {:app-state "active"
:network-status :online
:wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295"
{:confirmations "10"}}
:transactions-loading? false}}})
{:get-transactions {:account-id nil
:token-addresses (),
:chain nil
:web3 nil
:success-event :update-transactions-success
:error-event :update-transactions-fail}
:db {:app-state "active"
:network-status :online
:wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295"
{:confirmations "10"}}
:transactions-loading? true
:transactions-last-updated-at 1531734120829
:errors nil}}
:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms
:dispatch [:sync-wallet-transactions]}]})))
(testing "chat transaction is missing from wallet transactions"
(is (= (wallet.transactions/sync {:db {:app-state "active"
:network-status :online
:wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295"
{:confirmations "15"}
"0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"
{:confirmations "20"}}
:chat-transactions #{"0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b"
"0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"}
:transactions-loading? false}}})
{:get-transactions {:account-id nil
:token-addresses (),
:chain nil
:web3 nil
:success-event :update-transactions-success
:error-event :update-transactions-fail}
:db {:app-state "active"
:network-status :online
:wallet {:transactions {"0x59bd7cca850671c6bd2b6f2f75e001d8b7a1a254f0ba4a5660041ba7c2f19295"
{:confirmations "15"}
"0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"
{:confirmations "20"}}
:chat-transactions #{"0x0873923e4d8b39ccfeb8c1af9701a9da02fdc76947a48de7c5df1540f77fdc5b"
"0x318f566edd98eb29965067d3394c555050bf9f8e20183792c7f1a6bbc1bb34db"}
:transactions-loading? true
:transactions-last-updated-at 1531734120829
:errors nil}}
:utils/dispatch-later [{:ms wallet.transactions/sync-interval-ms
:dispatch [:sync-wallet-transactions]}]})))))