From d7530dfbee64aa6e042b63c402682dbe10b95b43 Mon Sep 17 00:00:00 2001 From: Sean Hagstrom Date: Thu, 6 Jun 2024 10:33:34 +0100 Subject: [PATCH] Feature: Implement import of key-pair with recovery phrase inside wallet settings (#20181) * fix: ensure missing key pairs scroll with list * chore: add translations for labels * chore: promisify validate-mnemonic native module function * chore: add events for validating seed-phrases and making key-pairs fully operable with seed-phrase * chore: define re-usable effects for validate-mnemonic and make-seed-phrase-keypair-fully-operable and import-keypair-by-seedphrase * tweak: refactor error handling logic when importing key-pair by seed-phrase * tweak: handle vectors and functions between events and effects * tweak: refactor recover-phrase form to support custom title, children, and navigation icon * tweak: add support for accessing input-ref in recovery-phrase form * tweak: always mask seed-phrase when passing to render-controls * feature: add initial screen for importing key-pair with seed phrase * tweak: conditionally render keypair context tag inside recovery-phrase form * tidy: rename on-input-ref to ref * tidy: remove unused property * tidy: refactor to use case expression * tidy: format --- src/native_module/core.cljs | 8 +- .../inputs/recovery_phrase/view.cljs | 5 +- .../common/enter_seed_phrase/view.cljs | 139 ++++++++++++------ src/status_im/contexts/onboarding/events.cljs | 15 +- .../contexts/settings/wallet/events.cljs | 55 ++++++- .../contexts/settings/wallet/events_test.cljs | 34 ++++- .../keypairs_and_accounts/actions/view.cljs | 55 ++++--- .../import_seed_phrase/view.cljs | 65 ++++++++ .../wallet/keypairs_and_accounts/view.cljs | 57 +++---- src/status_im/contexts/wallet/effects.cljs | 73 ++++++++- src/status_im/navigation/screens.cljs | 6 + translations/en.json | 4 + 12 files changed, 397 insertions(+), 119 deletions(-) create mode 100644 src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_seed_phrase/view.cljs diff --git a/src/native_module/core.cljs b/src/native_module/core.cljs index 89b70351ad..a8bfbdc8f6 100644 --- a/src/native_module/core.cljs +++ b/src/native_module/core.cljs @@ -527,9 +527,11 @@ (defn validate-mnemonic "Validate that a mnemonic conforms to BIP39 dictionary/checksum standards" - [mnemonic callback] - (log/debug "[native-module] validate-mnemonic") - (.validateMnemonic ^js (utils) mnemonic callback)) + ([mnemonic] + (native-utils/promisify-native-module-call validate-mnemonic mnemonic)) + ([mnemonic callback] + (log/debug "[native-module] validate-mnemonic") + (.validateMnemonic ^js (utils) mnemonic callback))) (defn delete-multiaccount "Delete multiaccount from database, deletes multiaccount's database and diff --git a/src/quo/components/inputs/recovery_phrase/view.cljs b/src/quo/components/inputs/recovery_phrase/view.cljs index 7d482b5897..8449e44a5a 100644 --- a/src/quo/components/inputs/recovery_phrase/view.cljs +++ b/src/quo/components/inputs/recovery_phrase/view.cljs @@ -40,7 +40,7 @@ (defn recovery-phrase-input [{:keys [customization-color blur? on-focus on-blur mark-errors? error-pred-current-word error-pred-written-words word-limit - container-style placeholder-text-color] + container-style placeholder-text-color on-input-ref] :or {customization-color :blue word-limit ##Inf error-pred-current-word (constantly false) @@ -60,7 +60,8 @@ extra-props (apply dissoc props custom-props)] [rn/view {:style (style/container container-style)} [rn/text-input - (merge {:accessibility-label :recovery-phrase-input + (merge {:ref on-input-ref + :accessibility-label :recovery-phrase-input :style (style/input theme) :placeholder-text-color (or placeholder-text-color (style/placeholder-color state theme blur?)) diff --git a/src/status_im/common/enter_seed_phrase/view.cljs b/src/status_im/common/enter_seed_phrase/view.cljs index 82ece635ab..ffe6dabcef 100644 --- a/src/status_im/common/enter_seed_phrase/view.cljs +++ b/src/status_im/common/enter_seed_phrase/view.cljs @@ -31,10 +31,10 @@ (comp not word-in-dictionary?)) (defn- header - [seed-phrase-count] + [text seed-phrase-count] [rn/view {:style style/header-container} [quo/text {:weight :semi-bold :size :heading-1} - (i18n/label :t/use-recovery-phrase)] + text] [rn/view {:style style/word-count-container} [quo/text {:style {:color colors/white-opa-40} @@ -51,34 +51,42 @@ (string/replace #"\s+" " ") (string/trim))) -(defn- recovery-form - [{:keys [seed-phrase word-count error-state? all-words-valid? on-change-seed-phrase - keyboard-shown? on-submit]}] - (let [button-disabled? (or error-state? - (not (constants/seed-phrase-valid-length word-count)) - (not all-words-valid?))] - [rn/view {:style style/form-container} - [header word-count] - [rn/view {:style style/input-container} - [quo/recovery-phrase-input - {:accessibility-label :passphrase-input - :placeholder (i18n/label :t/seed-phrase-placeholder) - :placeholder-text-color colors/white-opa-30 - :auto-capitalize :none - :auto-correct false - :auto-focus true - :mark-errors? true - :word-limit max-seed-phrase-length - :error-pred-current-word partial-word-not-in-dictionary? - :error-pred-written-words word-not-in-dictionary? - :on-change-text on-change-seed-phrase} - seed-phrase]] - [quo/button - {:container-style (style/continue-button keyboard-shown?) - :type :primary - :disabled? button-disabled? - :on-press on-submit} - (i18n/label :t/continue)]])) +(defn- secure-clean-seed-phrase + [seed-phrase] + (-> seed-phrase + security/safe-unmask-data + clean-seed-phrase + security/mask-data)) + +(defn- recovery-phrase-form + [{:keys [keypair title seed-phrase word-count on-change-seed-phrase ref]} & children] + (->> children + (into + [rn/view {:style style/form-container} + [header title word-count] + (when keypair + [quo/context-tag + {:type :icon + :container-style {:padding-top 8} + :icon :i/seed-phrase + :size 24 + :blur? true + :context (:name keypair)}]) + [rn/view {:style style/input-container} + [quo/recovery-phrase-input + {:accessibility-label :passphrase-input + :ref ref + :placeholder (i18n/label :t/seed-phrase-placeholder) + :placeholder-text-color colors/white-opa-30 + :auto-capitalize :none + :auto-correct false + :auto-focus true + :mark-errors? true + :word-limit max-seed-phrase-length + :error-pred-current-word partial-word-not-in-dictionary? + :error-pred-written-words word-not-in-dictionary? + :on-change-text on-change-seed-phrase} + seed-phrase]]]))) (defn keyboard-suggestions [current-word] @@ -86,8 +94,8 @@ (filter #(string/starts-with? % current-word)) (take 7))) -(defn screen - [recovering-keypair?] +(defn recovery-phrase-screen + [{:keys [keypair title recovering-keypair? render-controls]}] (reagent/with-let [keyboard-shown? (reagent/atom false) keyboard-show-listener (.addListener rn/keyboard "keyboardDidShow" @@ -96,6 +104,11 @@ "keyboardDidHide" #(reset! keyboard-shown? false)) invalid-seed-phrase? (reagent/atom false) + input-ref (reagent/atom nil) + focus-input (fn [] + (let [ref @input-ref] + (when ref + (.focus ref)))) set-invalid-seed-phrase #(reset! invalid-seed-phrase? true) seed-phrase (reagent/atom "") on-change-seed-phrase (fn [new-phrase] @@ -139,16 +152,33 @@ words-exceeded? (i18n/label :t/seed-phrase-words-exceeded) error-in-words? (i18n/label :t/seed-phrase-error) @invalid-seed-phrase? (i18n/label :t/seed-phrase-invalid) - :else (i18n/label :t/seed-phrase-info))] + :else (i18n/label :t/seed-phrase-info)) + error-state? (= suggestions-state :error) + button-disabled? (or error-state? + (not (constants/seed-phrase-valid-length word-count)) + (not all-words-valid?))] [:<> - [recovery-form - {:seed-phrase @seed-phrase - :error-state? (= suggestions-state :error) - :all-words-valid? all-words-valid? + [recovery-phrase-form + {:title title + :keypair keypair + :seed-phrase @seed-phrase :on-change-seed-phrase on-change-seed-phrase :word-count word-count - :on-submit on-submit - :keyboard-shown? @keyboard-shown?}] + :ref #(reset! input-ref %)} + (if (fn? render-controls) + (render-controls {:submit-disabled? button-disabled? + :keyboard-shown? @keyboard-shown? + :container-style (style/continue-button @keyboard-shown?) + :prepare-seed-phrase secure-clean-seed-phrase + :focus-input focus-input + :seed-phrase (security/mask-data @seed-phrase) + :set-invalid-seed-phrase set-invalid-seed-phrase}) + [quo/button + {:container-style (style/continue-button @keyboard-shown?) + :type :primary + :disabled? button-disabled? + :on-press on-submit} + (i18n/label :t/continue)])] (when @keyboard-shown? [rn/view {:style style/keyboard-container} [quo/predictive-keyboard @@ -161,16 +191,29 @@ (.remove keyboard-show-listener) (.remove keyboard-hide-listener)))) +(defn screen + [{:keys [initial-insets title keypair navigation-icon recovering-keypair? render-controls]}] + (let [{navigation-bar-top :top} initial-insets] + [rn/view {:style style/full-layout} + [rn/keyboard-avoiding-view {:style style/page-container} + [quo/page-nav + {:margin-top navigation-bar-top + :background :blur + :icon-name (or navigation-icon + (if recovering-keypair? :i/close :i/arrow-left)) + :on-press #(rf/dispatch [:navigate-back])}] + [recovery-phrase-screen + {:title title + :keypair keypair + :render-controls render-controls + :recovering-keypair? recovering-keypair?}]]])) + (defn view [] - (let [{navigation-bar-top :top} (safe-area/get-insets)] + (let [insets (safe-area/get-insets)] (fn [] (let [{:keys [recovering-keypair?]} (rf/sub [:get-screen-params])] - [rn/view {:style style/full-layout} - [rn/keyboard-avoiding-view {:style style/page-container} - [quo/page-nav - {:margin-top navigation-bar-top - :background :blur - :icon-name (if recovering-keypair? :i/close :i/arrow-left) - :on-press #(rf/dispatch [:navigate-back])}] - [screen recovering-keypair?]]])))) + [screen + {:title (i18n/label :t/use-recovery-phrase) + :initial-insets insets + :recovering-keypair? recovering-keypair?}])))) diff --git a/src/status_im/contexts/onboarding/events.cljs b/src/status_im/contexts/onboarding/events.cljs index 420edd7da5..b2bfc22b36 100644 --- a/src/status_im/contexts/onboarding/events.cljs +++ b/src/status_im/contexts/onboarding/events.cljs @@ -1,6 +1,5 @@ (ns status-im.contexts.onboarding.events (:require - [native-module.core :as native-module] [re-frame.core :as re-frame] status-im.common.biometric.events [status-im.constants :as constants] @@ -9,19 +8,7 @@ [taoensso.timbre :as log] [utils.i18n :as i18n] [utils.re-frame :as rf] - [utils.security.core :as security] - [utils.transforms :as transforms])) - -(re-frame/reg-fx - :multiaccount/validate-mnemonic - (fn [[mnemonic on-success on-error]] - (native-module/validate-mnemonic - (security/safe-unmask-data mnemonic) - (fn [result] - (let [{:keys [error keyUID]} (transforms/json->clj result)] - (if (seq error) - (when on-error (on-error error)) - (on-success mnemonic keyUID))))))) + [utils.security.core :as security])) (rf/defn profile-data-set {:events [:onboarding/profile-data-set]} diff --git a/src/status_im/contexts/settings/wallet/events.cljs b/src/status_im/contexts/settings/wallet/events.cljs index 834d314289..a9347dcdb8 100644 --- a/src/status_im/contexts/settings/wallet/events.cljs +++ b/src/status_im/contexts/settings/wallet/events.cljs @@ -1,9 +1,11 @@ (ns status-im.contexts.settings.wallet.events (:require + [native-module.core :as native-module] [status-im.contexts.settings.wallet.data-store :as data-store] [taoensso.timbre :as log] [utils.i18n :as i18n] - [utils.re-frame :as rf])) + [utils.re-frame :as rf] + [utils.security.core :as security])) (rf/reg-event-fx :wallet/rename-keypair-success @@ -117,3 +119,54 @@ :sha3-pwd password}]))}]]]}) (rf/reg-event-fx :wallet/success-keypair-qr-scan success-keypair-qr-scan) + +(defn wallet-validate-seed-phrase + [_ [seed-phrase on-success on-error]] + {:fx [[:multiaccount/validate-mnemonic [seed-phrase on-success on-error]]]}) + +(rf/reg-event-fx :wallet/validate-seed-phrase wallet-validate-seed-phrase) + +(defn make-seed-phrase-keypair-fully-operable + [_ [mnemonic password on-success on-error]] + {:fx [[:json-rpc/call + [{:method "accounts_makeSeedPhraseKeypairFullyOperable" + :params [(security/safe-unmask-data mnemonic) + (-> password security/safe-unmask-data native-module/sha3)] + :on-success on-success + :on-error on-error}]]]}) + +(rf/reg-event-fx :wallet/make-seed-phrase-keypair-fully-operable make-seed-phrase-keypair-fully-operable) + +(defn import-keypair-by-seed-phrase + [_ [{:keys [keypair-key-uid seed-phrase password on-success on-error]}]] + {:fx [[:import-keypair-by-seed-phrase + {:keypair-key-uid keypair-key-uid + :seed-phrase seed-phrase + :password password + :on-success (fn [] + (rf/dispatch [:wallet/make-keypairs-accounts-fully-operable + #{keypair-key-uid}]) + (cond + (vector? on-success) (rf/dispatch (conj on-success)) + (fn? on-success) (on-success))) + :on-error (fn [error] + (rf/dispatch [:wallet/import-keypair-by-seed-phrase-failed error]) + (cond + (vector? on-error) (rf/dispatch (conj on-error error)) + (fn? on-error) (on-error error)))}]]}) + +(rf/reg-event-fx :wallet/import-keypair-by-seed-phrase import-keypair-by-seed-phrase) + +(defn import-keypair-by-seed-phrase-failed + [_ [error]] + (let [error-type (-> error ex-message keyword) + error-data (ex-data error)] + (when-not (and (= error-type :import-keypair-by-seed-phrase/import-error) + (= (:hint error-data) :incorrect-seed-phrase-for-keypair)) + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text (:error error-data)}]]]}))) + +(rf/reg-event-fx :wallet/import-keypair-by-seed-phrase-failed import-keypair-by-seed-phrase-failed) diff --git a/src/status_im/contexts/settings/wallet/events_test.cljs b/src/status_im/contexts/settings/wallet/events_test.cljs index 978c6e3df0..dafced1e67 100644 --- a/src/status_im/contexts/settings/wallet/events_test.cljs +++ b/src/status_im/contexts/settings/wallet/events_test.cljs @@ -2,7 +2,9 @@ (:require [cljs.test :refer-macros [deftest is testing]] matcher-combinators.test - [status-im.contexts.settings.wallet.events :as sut])) + [native-module.core :as native-module] + [status-im.contexts.settings.wallet.events :as sut] + [utils.security.core :as security])) (def mock-key-uid "key-1") (defn mock-db @@ -95,3 +97,33 @@ (let [effects (sut/success-keypair-qr-scan nil [connection-string keypairs-key-uids]) fx (:fx effects)] (is (some? fx)))))) + +(deftest wallet-validate-seed-phrase-test + (let [cofx {:db {}} + seed-phrase-masked (security/mask-data "seed phrase") + on-success #(prn "success") + on-error #(prn "error") + expected {:fx [[:multiaccount/validate-mnemonic + [seed-phrase-masked on-success on-error]]]}] + (is (= expected + (sut/wallet-validate-seed-phrase + cofx + [seed-phrase-masked on-success on-error]))))) + +(deftest make-seed-phrase-keypair-fully-operable-test + (let [cofx {:db {}} + mnemonic "seed phrase" + password "password" + mnemonic-masked (security/mask-data mnemonic) + password-masked (security/mask-data password) + on-success #(prn "success") + on-error #(prn "error") + expected {:fx [[:json-rpc/call + [{:method "accounts_makeSeedPhraseKeypairFullyOperable" + :params [mnemonic (native-module/sha3 password)] + :on-success fn? + :on-error fn?}]]]}] + (is (match? expected + (sut/make-seed-phrase-keypair-fully-operable + cofx + [mnemonic-masked password-masked on-success on-error]))))) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs index 9faeb0f161..1a113ceacb 100644 --- a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/actions/view.cljs @@ -6,27 +6,31 @@ [utils.re-frame :as rf])) (defn view - [props keypair] - (let [has-paired-device (rf/sub [:pairing/has-paired-devices]) - missing-keypair? (= (:stored props) :missing) - on-scan-qr (rn/use-callback #(rf/dispatch [:open-modal :screen/settings.scan-keypair-qr - [(:key-uid keypair)]]) - [keypair]) - on-show-qr (rn/use-callback #(rf/dispatch [:open-modal - :screen/settings.encrypted-key-pair-qr - keypair]) - [keypair]) - on-remove-keypair (rn/use-callback #(rf/dispatch - [:show-bottom-sheet - {:theme :dark - :content (fn [] - [remove-key-pair/view keypair])}]) - [keypair]) - on-rename-keypair (rn/use-callback #(rf/dispatch [:open-modal :screen/settings.rename-keypair - keypair]) - [keypair])] + [{:keys [drawer-props keypair]}] + (let [has-paired-device (rf/sub [:pairing/has-paired-devices]) + missing-keypair? (= (:stored drawer-props) :missing) + on-scan-qr (rn/use-callback #(rf/dispatch [:open-modal + :screen/settings.scan-keypair-qr + [(:key-uid keypair)]]) + [keypair]) + on-show-qr (rn/use-callback #(rf/dispatch [:open-modal + :screen/settings.encrypted-key-pair-qr + keypair]) + [keypair]) + on-remove-keypair (rn/use-callback #(rf/dispatch + [:show-bottom-sheet + {:theme :dark + :content (fn [] + [remove-key-pair/view keypair])}]) + [keypair]) + on-rename-keypair (rn/use-callback #(rf/dispatch [:open-modal :screen/settings.rename-keypair + keypair]) + [keypair]) + on-import-seed-phrase (rn/use-callback + #(rf/dispatch [:open-modal :screen/settings.import-seed-phrase keypair]) + [keypair])] [:<> - [quo/drawer-top props] + [quo/drawer-top drawer-props] [quo/action-drawer [(when has-paired-device (if-not missing-keypair? @@ -38,8 +42,15 @@ :accessibility-label :import-by-scan-qr :label (i18n/label :t/import-by-scanning-encrypted-qr) :on-press on-scan-qr}])) - (when (= (:type props) :keypair) - [{:icon :i/edit + (when (= (:type drawer-props) :keypair) + [(when missing-keypair? + (case (:type keypair) + :seed {:icon :i/seed + :accessibility-label :import-seed-phrase + :label (i18n/label :t/import-by-entering-recovery-phrase) + :on-press #(on-import-seed-phrase keypair)} + nil)) + {:icon :i/edit :accessibility-label :rename-key-pair :label (i18n/label :t/rename-key-pair) :on-press on-rename-keypair} diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_seed_phrase/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_seed_phrase/view.cljs new file mode 100644 index 0000000000..4b04768bcd --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_seed_phrase/view.cljs @@ -0,0 +1,65 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.import-seed-phrase.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [reagent.core :as reagent] + [status-im.common.enter-seed-phrase.view :as enter-seed-phrase] + [status-im.common.standard-authentication.core :as standard-auth] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn import-seed-phrase-controls + [{:keys [submit-disabled? + container-style + prepare-seed-phrase + seed-phrase + set-invalid-seed-phrase + focus-input]}] + (let [keypair (rf/sub [:get-screen-params]) + customization-color (rf/sub [:profile/customization-color]) + show-errors (rn/use-callback + #(js/setTimeout + (fn [] + (focus-input) + (reagent/next-tick set-invalid-seed-phrase)) + 600)) + on-import-error (rn/use-callback + (fn [_error] + (rf/dispatch [:hide-bottom-sheet]) + (show-errors))) + on-import-success (rn/use-callback + (fn [] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch [:navigate-back]))) + on-auth-success (rn/use-callback + (fn [password] + (rf/dispatch [:wallet/import-keypair-by-seed-phrase + {:keypair-key-uid (:key-uid keypair) + :seed-phrase (prepare-seed-phrase seed-phrase) + :password password + :on-success on-import-success + :on-error on-import-error}])) + [keypair seed-phrase on-import-success on-import-error])] + [standard-auth/slide-button + {:blur? true + :size :size-48 + :customization-color customization-color + :track-text (i18n/label :t/slide-to-import) + :on-auth-success on-auth-success + :auth-button-label (i18n/label :t/import-key-pair) + :auth-button-icon-left :i/seed + :container-style container-style + :disabled? submit-disabled? + :dependencies [on-auth-success]}])) + +(defn view + [] + (let [keypair (rf/sub [:get-screen-params])] + [quo/overlay {:type :shell} + [enter-seed-phrase/screen + {:keypair keypair + :navigation-icon :i/close + :render-controls import-seed-phrase-controls + :title (i18n/label :t/enter-recovery-phrase) + :initial-insets (safe-area/get-insets)}]])) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/view.cljs index 9178050c15..8dff0f45d8 100644 --- a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/view.cljs +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/view.cljs @@ -14,11 +14,12 @@ (rf/dispatch [:navigate-back])) (defn on-options-press - [{:keys [theme] - :as props} keypair] + [{:keys [drawer-props keypair]}] (rf/dispatch [:show-bottom-sheet - {:content (fn [] [actions/view props keypair]) - :theme theme}])) + {:content (fn [] [actions/view + {:drawer-props drawer-props + :keypair keypair}]) + :theme (:theme drawer-props)}])) (defn options-drawer-props [{{:keys [name]} :keypair @@ -48,15 +49,17 @@ on-press (rn/use-callback (fn [] (on-options-press - (options-drawer-props - {:theme theme - :keypair item - :type (if default-keypair? :default-keypair :keypair) - :stored :on-device - :shortened-key shortened-key - :customization-color customization-color - :profile-picture profile-picture}) - item)) + {:keypair item + :drawer-props (options-drawer-props + {:theme theme + :keypair item + :type (if default-keypair? + :default-keypair + :keypair) + :stored :on-device + :shortened-key shortened-key + :customization-color customization-color + :profile-picture profile-picture})})) [customization-color default-keypair? item profile-picture shortened-key theme])] [quo/keypair @@ -78,13 +81,13 @@ (rf/dispatch [:show-bottom-sheet {:theme :dark :content (fn [] [actions/view - (options-drawer-props - {:theme :dark - :type :keypair - :stored :missing - :blur? true - :keypair keypair-data}) - keypair-data])}])) + {:keypair keypair-data + :drawer-props (options-drawer-props + {:theme :dark + :type :keypair + :stored :missing + :blur? true + :keypair keypair-data})}])}])) (defn view [] @@ -112,16 +115,16 @@ :accessibility-label :keypairs-and-accounts-header :customization-color customization-color}] [rn/view {:style style/settings-keypairs-container} - (when (seq missing-keypairs) - [quo/missing-keypairs - {:blur? true - :keypairs missing-keypairs - :on-import-press on-import-press - :container-style style/missing-keypairs-container-style - :on-options-press on-missing-keypair-options-press}]) [rn/flat-list {:data operable-keypairs :render-fn keypair + :header (when (seq missing-keypairs) + [quo/missing-keypairs + {:blur? true + :keypairs missing-keypairs + :on-import-press on-import-press + :container-style style/missing-keypairs-container-style + :on-options-press on-missing-keypair-options-press}]) :render-data {:profile-picture profile-picture :compressed-key compressed-key :customization-color customization-color} diff --git a/src/status_im/contexts/wallet/effects.cljs b/src/status_im/contexts/wallet/effects.cljs index f11b84d9d4..5f0d0d914b 100644 --- a/src/status_im/contexts/wallet/effects.cljs +++ b/src/status_im/contexts/wallet/effects.cljs @@ -2,8 +2,16 @@ (:require [clojure.string :as string] [native-module.core :as native-module] + [promesa.core :as promesa] [re-frame.core :as rf] - [taoensso.timbre :as log])) + [status-im.common.json-rpc.events :as json-rpc] + [taoensso.timbre :as log] + [utils.security.core :as security] + [utils.transforms :as transforms])) + +(defn- error-message + [kw] + (-> kw symbol str)) (rf/reg-fx :effects.wallet/create-account-from-mnemonic @@ -17,3 +25,66 @@ {:MnemonicPhrase phrase :paths paths} on-success)))) + +(defn validate-mnemonic + [mnemonic] + (-> mnemonic + (security/safe-unmask-data) + (native-module/validate-mnemonic) + (promesa/then (fn [result] + (let [{:keys [keyUID]} (transforms/json->clj result)] + {:key-uid keyUID}))))) + +(rf/reg-fx + :multiaccount/validate-mnemonic + (fn [[mnemonic on-success on-error]] + (-> (validate-mnemonic mnemonic) + (promesa/then (fn [{:keys [key-uid]}] + (when (fn? on-success) + (on-success mnemonic key-uid)))) + (promesa/catch (fn [error] + (when (and error (fn? on-error)) + (on-error error))))))) + +(defn make-seed-phrase-fully-operable + [mnemonic password] + (promesa/create + (fn [resolver rejecter] + (json-rpc/call {:method "accounts_makeSeedPhraseKeypairFullyOperable" + :params [(security/safe-unmask-data mnemonic) + (-> password security/safe-unmask-data native-module/sha3)] + :on-error (fn [error] + (rejecter (ex-info (str error) {:error error}))) + :on-success (fn [value] + (resolver {:value value}))})))) + +(defn import-keypair-by-seed-phrase + [keypair-key-uid seed-phrase password] + (-> (validate-mnemonic seed-phrase) + (promesa/then + (fn [{:keys [key-uid]}] + (if (not= keypair-key-uid key-uid) + (promesa/rejected + (ex-info + (error-message :import-keypair-by-seed-phrase/import-error) + {:hint :incorrect-seed-phrase-for-keypair})) + (make-seed-phrase-fully-operable seed-phrase password)))) + (promesa/catch + (fn [error] + (promesa/rejected + (ex-info + (error-message :import-keypair-by-seed-phrase/import-error) + (ex-data error))))))) + +(rf/reg-fx + :import-keypair-by-seed-phrase + (fn [{:keys [keypair-key-uid seed-phrase password on-success on-error]}] + (-> (import-keypair-by-seed-phrase keypair-key-uid seed-phrase password) + (promesa/then (fn [_result] + (cond + (vector? on-success) (rf/dispatch on-success) + (fn? on-success) (on-success)))) + (promesa/catch (fn [error] + (cond + (vector? on-error) (rf/dispatch (conj on-error error)) + (fn? on-error) (on-error error))))))) diff --git a/src/status_im/navigation/screens.cljs b/src/status_im/navigation/screens.cljs index dea050acb4..e804cb7750 100644 --- a/src/status_im/navigation/screens.cljs +++ b/src/status_im/navigation/screens.cljs @@ -59,6 +59,8 @@ [status-im.contexts.profile.settings.view :as settings] [status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.view :as encrypted-key-pair-qr] + [status-im.contexts.settings.wallet.keypairs-and-accounts.import-seed-phrase.view :as + import-seed-phrase] [status-im.contexts.settings.wallet.keypairs-and-accounts.rename.view :as keypair-rename] [status-im.contexts.settings.wallet.keypairs-and-accounts.scan-qr.view :as scan-keypair-qr] [status-im.contexts.settings.wallet.keypairs-and-accounts.view :as keypairs-and-accounts] @@ -547,6 +549,10 @@ :options options/transparent-modal-screen-options :component scan-keypair-qr/view} + {:name :screen/settings.import-seed-phrase + :options options/transparent-screen-options + :component import-seed-phrase/view} + {:name :screen/settings.network-settings :options options/transparent-modal-screen-options :component network-settings/view} diff --git a/translations/en.json b/translations/en.json index cb7bedb71e..ce5ffa48a0 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1018,6 +1018,9 @@ "generate-keys": "Generate keys", "generate-keys-subtitle": "Create your new self-sovereign identity", "experienced-web3": "Experienced in Web3?", + "enter-recovery-phrase": "Enter recovery phrase", + "import-key-pair": "Import key pair", + "import-by-entering-recovery-phrase": "Import by entering recovery phrase", "use-recovery-phrase": "Use recovery phrase", "use-recovery-phrase-subtitle": "If you already have an Ethereum address", "use-keycard": "Use Keycard", @@ -1980,6 +1983,7 @@ "select-token-to-swap": "Select token to Swap", "select-token-to-receive": "Select token to receive", "slide-to-request-to-join": "Slide to request to join", + "slide-to-import": "Slide to import", "slide-to-reveal-code": "Slide to reveal code", "slide-to-create-account": "Slide to create account", "slide-to-remove-key-pair": "Slide to remove key pair",