diff --git a/src/status_im/contexts/chat/messenger/messages/transport/events.cljs b/src/status_im/contexts/chat/messenger/messages/transport/events.cljs index 9416ae3769..1594ddbe10 100644 --- a/src/status_im/contexts/chat/messenger/messages/transport/events.cljs +++ b/src/status_im/contexts/chat/messenger/messages/transport/events.cljs @@ -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") diff --git a/src/status_im/contexts/wallet/data_store.cljs b/src/status_im/contexts/wallet/data_store.cljs index 4ad71b6281..98c3291a3f 100644 --- a/src/status_im/contexts/wallet/data_store.cljs +++ b/src/status_im/contexts/wallet/data_store.cljs @@ -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})) diff --git a/src/status_im/contexts/wallet/data_store_test.cljs b/src/status_im/contexts/wallet/data_store_test.cljs new file mode 100644 index 0000000000..fd305ab7c5 --- /dev/null +++ b/src/status_im/contexts/wallet/data_store_test.cljs @@ -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]))))) diff --git a/src/status_im/contexts/wallet/events.cljs b/src/status_im/contexts/wallet/events.cljs index 735c549d38..e00cc1a2ec 100644 --- a/src/status_im/contexts/wallet/events.cljs +++ b/src/status_im/contexts/wallet/events.cljs @@ -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) diff --git a/src/status_im/contexts/wallet/events_test.cljs b/src/status_im/contexts/wallet/events_test.cljs index 15bed73bce..febd4e017e 100644 --- a/src/status_im/contexts/wallet/events_test.cljs +++ b/src/status_im/contexts/wallet/events_test.cljs @@ -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)