feat: add support for updating key-pairs and accounts from app signal (#20550)

This change adds support for processing app signals for syncing changes when adding, editing, or removing keypairs, accounts, and watch-only accounts.
This commit is contained in:
Sean Hagstrom 2024-07-04 16:25:30 +01:00 committed by GitHub
parent 66f77e1467
commit eaa5016094
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 549 additions and 5 deletions

View File

@ -55,9 +55,11 @@
^js cleared-histories (.-clearedHistories response-js)
^js identity-images (.-identityImages response-js)
^js accounts (.-accounts response-js)
^js keypairs (.-keypairs response-js)
^js ens-username-details-js (.-ensUsernameDetails response-js)
^js customization-color-js (.-customizationColor response-js)
^js saved-addresses-js (.-savedAddresses response-js)
^js watch-only-accounts (.-watchOnlyAccounts response-js)
sync-handler (when-not process-async process-response)]
(cond
@ -179,6 +181,22 @@
(rf/merge cofx
(process-next response-js sync-handler)))
(seq watch-only-accounts)
(do
(js-delete response-js "watchOnlyAccounts")
(rf/merge cofx
{:fx [[:dispatch
[:wallet/reconcile-watch-only-accounts
(types/js->clj watch-only-accounts)]]]}
(process-next response-js sync-handler)))
(seq keypairs)
(do
(js-delete response-js "keypairs")
(rf/merge cofx
{:fx [[:dispatch [:wallet/reconcile-keypairs (types/js->clj keypairs)]]]}
(process-next response-js sync-handler)))
(seq settings)
(do
(js-delete response-js "settings")

View File

@ -5,6 +5,7 @@
[clojure.string :as string]
[status-im.constants :as constants]
[status-im.contexts.wallet.common.utils.networks :as network-utils]
[utils.collection :as utils.collection]
[utils.money :as money]
[utils.number :as utils.number]
[utils.transforms :as transforms]))
@ -160,3 +161,29 @@
(defn rpc->saved-addresses
[saved-addresses]
(map rpc->saved-address saved-addresses))
(defn reconcile-keypairs
[keypairs]
(let [received-keypairs (rpc->keypairs keypairs)
keypair-label #(if % :removed-keypairs :updated-keypairs)
{:keys [removed-keypairs
updated-keypairs]
:or {updated-keypairs []
removed-keypairs []}} (group-by (comp keypair-label :removed) received-keypairs)
updated-keypairs-by-id (utils.collection/index-by :key-uid updated-keypairs)
updated-accounts-by-address (transduce (comp (mapcat :accounts)
(filter (comp not :chat))
(map #(vector (:address %) %)))
conj
{}
updated-keypairs)
removed-keypairs-ids (set (map :key-uid removed-keypairs))
removed-account-addresses (transduce (comp (mapcat :accounts)
(map :address))
conj
#{}
removed-keypairs)]
{:removed-keypair-ids removed-keypairs-ids
:removed-account-addresses removed-account-addresses
:updated-keypairs-by-id updated-keypairs-by-id
:updated-accounts-by-address updated-accounts-by-address}))

View File

@ -0,0 +1,240 @@
(ns status-im.contexts.wallet.data-store-test
(:require
[cljs.test :refer-macros [deftest is testing]]
[matcher-combinators.matchers :as matchers]
matcher-combinators.test
[status-im.contexts.wallet.data-store :as sut]))
(def raw-account
{:path "m/44'/60'/0'/0/0"
:name "Account name"
:wallet true
:chat false
:emoji "🍙"
:colorId "blue"
:type "generated"
:createdAt 1716548742000
:prodPreferredChainIds "1:42161"
:testPreferredChainIds "11155111:421614"
:removed false
:operable "fully"})
(def account
{:path "m/44'/60'/0'/0/0"
:emoji "🍙"
:color :blue
:wallet true
:default-account? true
:name "Account name"
:type :generated
:chat false
:test-preferred-chain-ids #{11155111 421614}
:watch-only? false
:prod-preferred-chain-ids #{1 42161}
:created-at 1716548742000
:operable :fully
:removed false})
(defn make-raw-account
[overrides]
(merge raw-account overrides))
(def raw-keypair-profile
{:type "profile"
:key-uid "0x000"
:accounts [(make-raw-account {:key-uid "0x000"
:address "1x000"
:wallet false
:chat true})
(make-raw-account {:key-uid "0x000"
:address "2x000"})]})
(def raw-keypair-seed-phrase
{:type "seed"
:key-uid "0x123"
:accounts [(make-raw-account {:key-uid "0x123"
:address "1x123"})]})
(def raw-keypair-private-key
{:type "key"
:key-uid "0x456"
:accounts [(make-raw-account {:key-uid "0x456"
:address "1x456"
:operable "no"})]})
(deftest chain-ids-string->set-test
(testing "chaind-ids-string->set splits and parses chain-ids from string"
(is (match? #{1 42161}
(sut/chain-ids-string->set (:prodPreferredChainIds raw-account))))
(is (match? #{11155111 421614}
(sut/chain-ids-string->set (:testPreferredChainIds raw-account))))))
(deftest rpc->keypair-test
(testing "rpc->keypair transforms a profile keypair"
(is
(match? {:type :profile
:key-uid "0x000"
:lowest-operability :fully
:accounts [{:key-uid "0x000"
:operable :fully
:chat true
:wallet false
:address "1x000"
:path (:path raw-account)
:name (:name raw-account)
:emoji (:emoji raw-account)
:removed (:removed raw-account)
:color (keyword (:colorId raw-account))
:created-at (:createdAt raw-account)
:prod-preferred-chain-ids (sut/chain-ids-string->set
(:prodPreferredChainIds raw-account))
:test-preferred-chain-ids (sut/chain-ids-string->set
(:testPreferredChainIds raw-account))}
{:key-uid "0x000"
:operable :fully
:chat false
:wallet true
:address "2x000"
:path (:path raw-account)
:name (:name raw-account)
:emoji (:emoji raw-account)
:removed (:removed raw-account)
:color (keyword (:colorId raw-account))
:created-at (:createdAt raw-account)
:prod-preferred-chain-ids (sut/chain-ids-string->set
(:prodPreferredChainIds raw-account))
:test-preferred-chain-ids (sut/chain-ids-string->set
(:testPreferredChainIds raw-account))}]}
(sut/rpc->keypair raw-keypair-profile))))
(testing "rpc->keypair transforms a seed-phrase keypair"
(is
(match? {:type :seed
:key-uid "0x123"
:lowest-operability :fully
:accounts [{:key-uid "0x123"
:address "1x123"
:operable :fully
:path (:path raw-account)
:name (:name raw-account)
:emoji (:emoji raw-account)
:wallet (:wallet raw-account)
:removed (:removed raw-account)
:color (keyword (:colorId raw-account))
:created-at (:createdAt raw-account)
:prod-preferred-chain-ids (sut/chain-ids-string->set
(:prodPreferredChainIds raw-account))
:test-preferred-chain-ids (sut/chain-ids-string->set
(:testPreferredChainIds raw-account))}]}
(sut/rpc->keypair raw-keypair-seed-phrase))))
(testing "rpc->keypair transforms a raw private-key keypair with inoperable accounts"
(is
(match? {:type :key
:key-uid "0x456"
:lowest-operability :no
:accounts [{:key-uid "0x456"
:address "1x456"
:operable :no
:path (:path raw-account)
:name (:name raw-account)
:emoji (:emoji raw-account)
:wallet (:wallet raw-account)
:removed (:removed raw-account)
:color (keyword (:colorId raw-account))
:created-at (:createdAt raw-account)
:prod-preferred-chain-ids (sut/chain-ids-string->set
(:prodPreferredChainIds raw-account))
:test-preferred-chain-ids (sut/chain-ids-string->set
(:testPreferredChainIds
raw-account))}]}
(sut/rpc->keypair raw-keypair-private-key)))))
(deftest rpc->keypairs-test
(testing "rpc->keypairs transforms and sorts raw keypairs"
(is
(match? [(sut/rpc->keypair raw-keypair-profile)
(sut/rpc->keypair raw-keypair-seed-phrase)
(sut/rpc->keypair raw-keypair-private-key)]
(sut/rpc->keypairs [raw-keypair-seed-phrase
raw-keypair-private-key
raw-keypair-profile])))))
(deftest reconcile-keypairs-test
(testing "reconcile-keypairs represents updated key pairs and accounts"
(is
(match?
(matchers/match-with
[set? matchers/set-equals
map? matchers/equals]
{:removed-keypair-ids #{}
:removed-account-addresses #{}
:updated-accounts-by-address {"1x123" (merge account
{:key-uid "0x123"
:address "1x123"})
"1x456" (merge account
{:key-uid "0x456"
:address "1x456"
:operable :no})}
:updated-keypairs-by-id {"0x123" {:key-uid "0x123"
:type :seed
:lowest-operability :fully
:accounts [(merge account
{:key-uid "0x123"
:address "1x123"})]}
"0x456" {:key-uid "0x456"
:type :key
:lowest-operability :no
:accounts [(merge account
{:key-uid "0x456"
:address "1x456"
:operable :no})]}}})
(sut/reconcile-keypairs [raw-keypair-seed-phrase
raw-keypair-private-key]))))
(testing "reconcile-keypairs represents removed key pairs and accounts"
(is
(match?
(matchers/match-with
[set? matchers/set-equals
map? matchers/equals]
{:removed-keypair-ids #{"0x456"}
:removed-account-addresses #{"1x456"}
:updated-accounts-by-address {"1x123" (merge account
{:key-uid "0x123"
:address "1x123"})}
:updated-keypairs-by-id {"0x123" {:key-uid "0x123"
:type :seed
:lowest-operability :fully
:accounts [(merge account
{:key-uid "0x123"
:address "1x123"})]}}})
(sut/reconcile-keypairs [raw-keypair-seed-phrase
(assoc raw-keypair-private-key :removed true)]))))
(testing "reconcile-keypairs ignores chat accounts inside updated accounts"
(is
(match?
(matchers/match-with
[set? matchers/set-equals
map? matchers/equals]
{:removed-keypair-ids #{}
:removed-account-addresses #{}
:updated-accounts-by-address {"2x000" (merge account
{:key-uid "0x000"
:address "2x000"
:chat false
:wallet true
:default-account? true})}
:updated-keypairs-by-id {"0x000" {:key-uid "0x000"
:type :profile
:lowest-operability :fully
:accounts [(merge account
{:key-uid "0x000"
:address "1x000"
:chat true
:wallet false
:default-account? false})
(merge account
{:key-uid "0x000"
:address "2x000"
:chat false
:wallet true
:default-account? true})]}}})
(sut/reconcile-keypairs [raw-keypair-profile])))))

View File

@ -1,6 +1,7 @@
(ns status-im.contexts.wallet.events
(:require
[camel-snake-kebab.extras :as cske]
[clojure.set]
[clojure.string :as string]
[react-native.platform :as platform]
[status-im.constants :as constants]
@ -81,6 +82,11 @@
(rf/reg-event-fx :wallet/log-rpc-error log-rpc-error)
(def refresh-accounts-fx-dispatches
[[:dispatch [:wallet/get-wallet-token-for-all-accounts]]
[:dispatch [:wallet/request-collectibles-for-all-accounts {:new-request? true}]]
[:dispatch [:wallet/check-recent-history-for-all-accounts]]])
(rf/reg-event-fx
:wallet/get-accounts-success
(fn [{:keys [db]} [accounts]]
@ -91,11 +97,9 @@
{:db (assoc-in db
[:wallet :accounts]
(utils.collection/index-by :address wallet-accounts))
:fx [[:dispatch [:wallet/get-wallet-token-for-all-accounts]]
[:dispatch [:wallet/request-collectibles-for-all-accounts {:new-request? true}]]
[:dispatch [:wallet/check-recent-history-for-all-accounts]]
(when new-account?
[:dispatch [:wallet/navigate-to-new-account navigate-to-account]])]})))
:fx (concat refresh-accounts-fx-dispatches
[(when new-account?
[:dispatch [:wallet/navigate-to-new-account navigate-to-account]])])})))
(rf/reg-event-fx
:wallet/get-accounts
@ -545,3 +549,73 @@
:wallet/process-watch-only-account-from-backup
(fn [_ [{:keys [backedUpWatchOnlyAccount]}]]
{:fx [[:dispatch [:wallet/process-account-from-signal backedUpWatchOnlyAccount]]]}))
(defn reconcile-watch-only-accounts
[{:keys [db]} [watch-only-accounts]]
(let [existing-accounts-by-address (get-in db [:wallet :accounts])
group-label #(if % :removed-accounts :updated-accounts)
{:keys [removed-accounts
updated-accounts]} (->> watch-only-accounts
(map data-store/rpc->account)
(group-by (comp group-label :removed)))
existing-account-addresses (set (keys existing-accounts-by-address))
removed-account-addresses (set (map :address removed-accounts))
updated-account-addresses (set (map :address updated-accounts))
new-account-addresses (clojure.set/difference updated-account-addresses
existing-account-addresses)]
{:db (update-in db
[:wallet :accounts]
(fn [existing-accounts]
(merge-with merge
(apply dissoc existing-accounts removed-account-addresses)
(utils.collection/index-by :address updated-accounts))))
:fx (mapcat (fn [address]
[[:dispatch [:wallet/get-wallet-token-for-account address]]
[:dispatch
[:wallet/request-new-collectibles-for-account-from-signal address]]
[:dispatch [:wallet/check-recent-history-for-account address]]])
new-account-addresses)}))
(rf/reg-event-fx :wallet/reconcile-watch-only-accounts reconcile-watch-only-accounts)
(defn reconcile-keypairs
[{:keys [db]} [keypairs]]
(let [existing-keypairs-by-id (get-in db [:wallet :keypairs])
existing-accounts-by-address (get-in db [:wallet :accounts])
{:keys [removed-keypair-ids
removed-account-addresses
updated-keypairs-by-id
updated-accounts-by-address]} (data-store/reconcile-keypairs keypairs)
updated-keypair-ids (set (keys updated-keypairs-by-id))
updated-account-addresses (set (keys updated-accounts-by-address))
existing-account-addresses (set (keys existing-accounts-by-address))
new-account-addresses (clojure.set/difference updated-account-addresses
existing-account-addresses)
old-account-addresses (->> (vals existing-accounts-by-address)
(filter (fn [{:keys [address key-uid]}]
(and (contains? updated-keypair-ids key-uid)
(not (contains?
updated-accounts-by-address
address)))))
(map :address))]
(cond-> {:db (-> db
(assoc-in [:wallet :keypairs]
(-> (apply dissoc existing-keypairs-by-id removed-keypair-ids)
(merge updated-keypairs-by-id)))
(assoc-in [:wallet :accounts]
(merge-with merge
(apply dissoc
existing-accounts-by-address
(into removed-account-addresses
old-account-addresses))
updated-accounts-by-address)))}
(seq new-account-addresses)
(assoc :fx
(mapcat (fn [address]
[[:dispatch [:wallet/get-wallet-token-for-account address]]
[:dispatch
[:wallet/request-new-collectibles-for-account-from-signal address]]
[:dispatch [:wallet/check-recent-history-for-account address]]])
new-account-addresses)))))
(rf/reg-event-fx :wallet/reconcile-keypairs reconcile-keypairs)

View File

@ -1,6 +1,7 @@
(ns status-im.contexts.wallet.events-test
(:require
[cljs.test :refer-macros [is testing]]
[matcher-combinators.matchers :as matchers]
matcher-combinators.test
[re-frame.db :as rf-db]
[status-im.constants :as constants]
@ -150,3 +151,187 @@
[:dispatch [:wallet/check-recent-history-for-account address]]]}]
(reset! rf-db/app-db {:wallet {:accounts {}}})
(is (match? expected-effects (dispatch [event-id raw-account])))))
(h/deftest-event :wallet/reconcile-keypairs
[event-id dispatch]
(let [keypair-key-uid (:key-uid raw-account)]
(testing "event adds new key pairs"
(reset! rf-db/app-db {:wallet {:accounts {}
:keypairs {}}})
(is
(match?
(matchers/match-with
[set? matchers/set-equals
vector? matchers/equals
map? matchers/equals]
{:db {:wallet {:accounts {(:address account) account}
:keypairs {keypair-key-uid {:key-uid keypair-key-uid
:type :seed
:lowest-operability :fully
:accounts [account]}}}}
:fx [[:dispatch [:wallet/get-wallet-token-for-account address]]
[:dispatch [:wallet/request-new-collectibles-for-account-from-signal address]]
[:dispatch [:wallet/check-recent-history-for-account address]]]})
(dispatch [event-id
[{:key-uid keypair-key-uid
:type "seed"
:accounts [raw-account]}]]))))
(testing "event removes key pairs and accounts that are marked as removed"
(reset! rf-db/app-db {:wallet {:accounts {(:address account) account}
:keypairs {keypair-key-uid
{:key-uid keypair-key-uid
:type :seed
:lowest-operability :fully
:accounts [account]}}}})
(is
(match?
(matchers/match-with
[set? matchers/set-equals
vector? matchers/equals
map? matchers/equals]
{:db {:wallet {:accounts {}
:keypairs {}}}})
(dispatch [event-id
[{:key-uid keypair-key-uid
:type "seed"
:removed true
:accounts [raw-account]}]]))))
(testing "event removes accounts not present with key pair"
(reset! rf-db/app-db {:wallet {:accounts {(:address account) account
"1x001" (assoc account
:address "1x001"
:key-uid "0x001")}
:keypairs {keypair-key-uid
{:key-uid keypair-key-uid
:type :seed
:lowest-operability :fully
:accounts [account]}
"0x001"
{:key-uid "0x001"
:type :seed
:lowest-operability :fully
:accounts [(assoc account
:address "1x001"
:key-uid "0x001")]}}}})
(is
(match?
(matchers/match-with
[set? matchers/set-equals
vector? matchers/equals
map? matchers/equals]
{:db {:wallet {:accounts {(:address account) account}
:keypairs {keypair-key-uid
{:key-uid keypair-key-uid
:type :seed
:lowest-operability :fully
:accounts [account]}
"0x001"
{:key-uid "0x001"
:type :seed
:lowest-operability :fully
:accounts []}}}}})
(dispatch [event-id
[{:key-uid "0x001"
:type "seed"
:accounts []}]]))))
(testing "event updates existing key pairs"
(reset! rf-db/app-db {:wallet
{:accounts {(:address account)
(assoc account :operable :no)}
:keypairs {keypair-key-uid
{:key-uid keypair-key-uid
:type :seed
:lowest-operability :no
:accounts [(assoc account :operable :no)]}}}})
(is
(match?
(matchers/match-with
[set? matchers/set-equals
vector? matchers/equals
map? matchers/equals]
{:db {:wallet {:accounts {(:address account) account}
:keypairs {keypair-key-uid
{:key-uid keypair-key-uid
:type :seed
:lowest-operability :fully
:accounts [account]}}}}})
(dispatch [event-id
[{:key-uid keypair-key-uid
:type "seed"
:accounts [raw-account]}]]))))
(testing "event ignores chat accounts for key pairs"
(reset! rf-db/app-db {:wallet {:accounts {(:address account) account}
:keypairs {keypair-key-uid
{:key-uid keypair-key-uid
:type :profile
:lowest-operability :fully
:accounts [account
(assoc account
:address "1x001"
:chat true)]}}}})
(is
(match?
(matchers/match-with
[set? matchers/set-equals
vector? matchers/equals
map? matchers/equals]
{:db {:wallet {:accounts {(:address account) account}
:keypairs {keypair-key-uid
{:key-uid keypair-key-uid
:type :profile
:lowest-operability :fully
:accounts [account
(assoc account
:address "1x001"
:chat true)]}}}}})
(dispatch [event-id
[{:key-uid keypair-key-uid
:type "profile"
:accounts [raw-account
(assoc raw-account
:address "1x001"
:chat true)]}]]))))))
(h/deftest-event :wallet/reconcile-watch-only-accounts
[event-id dispatch]
(testing "event adds new watch-only accounts"
(reset! rf-db/app-db {:wallet {:accounts {}}})
(is
(match?
(matchers/match-with
[set? matchers/set-equals
vector? matchers/equals
map? matchers/equals]
{:db {:wallet {:accounts {(:address account) account}}}
:fx [[:dispatch [:wallet/get-wallet-token-for-account address]]
[:dispatch
[:wallet/request-new-collectibles-for-account-from-signal address]]
[:dispatch [:wallet/check-recent-history-for-account address]]]})
(dispatch [event-id [raw-account]]))))
(testing "event removes watch-only accounts that are marked as removed"
(reset! rf-db/app-db {:wallet {:accounts {(:address account) account}}})
(is
(match?
(matchers/match-with
[set? matchers/set-equals
vector? matchers/equals
map? matchers/equals]
{:db {:wallet {:accounts {}}}
:fx []})
(dispatch [event-id [(assoc raw-account :removed true)]]))))
(testing "event updates existing watch-only accounts"
(reset! rf-db/app-db {:wallet
{:accounts {address account}}})
(is
(match?
(matchers/match-with
[set? matchers/set-equals
vector? matchers/equals
map? matchers/equals]
{:db {:wallet {:accounts {address (assoc account :name "Test")}}}
:fx []})
(dispatch [event-id
[(assoc raw-account
:address address
:name "Test")]])))))
(cljs.test/run-tests)