diff --git a/externs.js b/externs.js index 8a3f8b7c22..d49c0b0f23 100644 --- a/externs.js +++ b/externs.js @@ -620,5 +620,6 @@ var TopLevel = { "multiAccountLoadAccount" : function () {}, "multiAccountStoreAccount" : function () {}, "multiAccountImportMnemonic" : function () {}, + "multiAccountImportPrivateKey" : function () {}, "validateMnemonic" : function () {} } diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java index 9b7d3d8e07..78657ef934 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java @@ -776,6 +776,23 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL StatusThreadPoolExecutor.getInstance().execute(r); } + @ReactMethod + public void multiAccountImportPrivateKey(final String json, final Callback callback) { + Log.d(TAG, "multiAccountImportPrivateKey"); + if (!checkAvailability()) { + callback.invoke(false); + return; + } + Runnable r = new Runnable() { + @Override + public void run() { + String res = Statusgo.multiAccountImportPrivateKey(json); + callback.invoke(res); + } + }; + StatusThreadPoolExecutor.getInstance().execute(r); + } + @ReactMethod public void hashTransaction(final String txArgsJSON, final Callback callback) { Log.d(TAG, "hashTransaction"); diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m index 8071ce20c8..c17d4f74cf 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -266,6 +266,16 @@ RCT_EXPORT_METHOD(multiAccountStoreDerived:(NSString *)json callback(@[result]); } +//////////////////////////////////////////////////////////////////// multiAccountImportPrivateKey +RCT_EXPORT_METHOD(multiAccountImportPrivateKey:(NSString *)json + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"MultiAccountImportPrivateKey() method called"); +#endif + NSString *result = StatusgoMultiAccountImportPrivateKey(json); + callback(@[result]); +} + //////////////////////////////////////////////////////////////////// multiAccountImportMnemonic RCT_EXPORT_METHOD(multiAccountImportMnemonic:(NSString *)json callback:(RCTResponseSenderBlock)callback) { diff --git a/src/status_im/hardwallet/wallet.cljs b/src/status_im/hardwallet/wallet.cljs index 662f6d1325..3e729be837 100644 --- a/src/status_im/hardwallet/wallet.cljs +++ b/src/status_im/hardwallet/wallet.cljs @@ -18,13 +18,11 @@ (if card-connected? (fx/merge cofx {:db (assoc-in db [:hardwallet :on-export-success] - #(vector :wallet.accounts/account-generated - {:name (str "Account " path-num) - ;; Strip leading 04 prefix denoting uncompressed key format + #(vector :wallet.accounts/account-stored + {;; Strip leading 04 prefix denoting uncompressed key format :address (eip55/address->checksum (str "0x" (ethereum/public-key->address (subs % 2)))) :public-key (str "0x" %) - :path path - :color (rand-nth colors/account-colors)})) + :path path})) :hardwallet/export-key {:pin pin :pairing pairing :path path}} (navigation/navigate-to-cofx :keycard-processing nil) (common/set-on-card-connected :wallet.accounts/generate-new-keycard-account)) diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs index 87bb3dc5c8..2f98845bb3 100644 --- a/src/status_im/native_module/core.cljs +++ b/src/status_im/native_module/core.cljs @@ -157,9 +157,15 @@ (types/clj->json {:mnemonicPhrase mnemonic ;;NOTE this is not the multiaccount password :Bip39Passphrase password}) - callback)) +(defn multiaccount-import-private-key + [private-key callback] + (log/debug "[native-module] multiaccount-import-private-key") + (.multiAccountImportPrivateKey (status) + (types/clj->json {:privateKey private-key}) + callback)) + (defn verify "NOTE: beware, the password has to be sha3 hashed" [address hashed-password callback] diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 498b40f030..78bec37afd 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -538,15 +538,19 @@ :add-account-disabled? :<- [:multiaccount/accounts] :<- [:add-account] - (fn [[accounts {:keys [address]}]] - (or (not (ethereum/address? address)) - (some #(when (= (:address %) address) %) accounts)))) - -(re-frame/reg-sub - :add-account-scanned-address - :<- [:add-account] - (fn [add-account] - (get add-account :scanned-address))) + (fn [[accounts {:keys [address type account seed private-key]}]] + (or (string/blank? (:name account)) + (case type + :generate + false + :watch + (or (not (ethereum/address? address)) + (some #(when (= (:address %) address) %) accounts)) + :key + (string/blank? (security/safe-unmask-data private-key)) + :seed + (string/blank? (security/safe-unmask-data seed)) + false)))) ;;CHAT ============================================================================================================== diff --git a/src/status_im/ui/screens/routing/back_actions.cljs b/src/status_im/ui/screens/routing/back_actions.cljs index f8264e3e64..7f2e00e981 100644 --- a/src/status_im/ui/screens/routing/back_actions.cljs +++ b/src/status_im/ui/screens/routing/back_actions.cljs @@ -8,8 +8,6 @@ :new-public-chat :default :wallet-account :default :add-new-account :default - :add-watch-account :default - :add-new-account-password :default :add-new-account-pin :default :about-app :default :help-center :default diff --git a/src/status_im/ui/screens/routing/screens.cljs b/src/status_im/ui/screens/routing/screens.cljs index 9dadd52eaf..e3128d43b2 100644 --- a/src/status_im/ui/screens/routing/screens.cljs +++ b/src/status_im/ui/screens/routing/screens.cljs @@ -184,10 +184,7 @@ :welcome [:modal home/welcome] :keycard-welcome keycard/welcome :add-new-account add-account/add-account - :add-watch-account add-account/add-watch-account - :add-new-account-password add-account/password :add-new-account-pin add-account/pin - :account-added account-settings/account-added :account-settings account-settings/account-settings}) (defn get-screen [screen] diff --git a/src/status_im/ui/screens/routing/wallet_stack.cljs b/src/status_im/ui/screens/routing/wallet_stack.cljs index f746aa4bc5..edde9e5e85 100644 --- a/src/status_im/ui/screens/routing/wallet_stack.cljs +++ b/src/status_im/ui/screens/routing/wallet_stack.cljs @@ -6,10 +6,7 @@ :screens (cond-> [:wallet :wallet-account :add-new-account - :add-watch-account - :add-new-account-password :add-new-account-pin - :account-added :account-settings :collectibles-list :wallet-onboarding-setup diff --git a/src/status_im/ui/screens/wallet/account_settings/views.cljs b/src/status_im/ui/screens/wallet/account_settings/views.cljs index 318899522b..1f0736ba4e 100644 --- a/src/status_im/ui/screens/wallet/account_settings/views.cljs +++ b/src/status_im/ui/screens/wallet/account_settings/views.cljs @@ -6,8 +6,6 @@ [status-im.i18n :as i18n] [status-im.ui.components.icons.vector-icons :as icons] [status-im.ui.components.colors :as colors] - [status-im.ui.components.button :as button] - [clojure.string :as string] [status-im.ui.components.toolbar :as toolbar] [status-im.ui.components.copyable-text :as copyable-text] [reagent.core :as reagent] @@ -33,45 +31,6 @@ :label (i18n/label :t/cancel) :type :secondary}}]])) -(defview account-added [] - (letsubs [{:keys [account]} [:add-account]] - [react/keyboard-avoiding-view {:flex 1} - [react/scroll-view {:keyboard-should-persist-taps :handled - :style {:margin-top 70 :flex 1}} - [react/view {:align-items :center :padding-horizontal 40} - [react/view {:height 40 :width 40 :border-radius 20 :align-items :center :justify-content :center - :background-color (:color account)} - [icons/icon :main-icons/check {:color colors/white}]] - [react/text {:style {:typography :header :margin-top 16}} - (i18n/label :t/account-added)] - [react/text {:style {:color colors/gray :text-align :center :margin-top 16 :line-height 22}} - (i18n/label :t/you-can-change-account)]] - [react/view {:height 52}] - [react/view {:margin-horizontal 16} - [text-input/text-input-with-label - {:label (i18n/label :t/account-name) - :auto-focus false - :default-value (:name account) - :placeholder (i18n/label :t/account-name) - :on-change-text #(re-frame/dispatch [:set-in [:add-account :account :name] %])}] - [react/text {:style {:margin-top 30}} (i18n/label :t/account-color)] - [react/touchable-highlight - {:on-press #(re-frame/dispatch [:show-popover - {:view [colors-popover (:color account) - (fn [new-color] - (re-frame/dispatch [:set-in [:add-account :account :color] new-color]) - (re-frame/dispatch [:hide-popover]))] - :style {:max-height "60%"}}])} - [react/view {:height 52 :margin-top 12 :background-color (:color account) :border-radius 8 - :align-items :flex-end :justify-content :center :padding-right 12} - [icons/icon :main-icons/dropdown {:color colors/white}]]]]] - [toolbar/toolbar - {:show-border? true - :right {:type :next - :label (i18n/label :t/finish) - :on-press #(re-frame/dispatch [:wallet.accounts/save-generated-account]) - :disabled? (string/blank? (:name account))}}]])) - (defn property [label value] [react/view {:margin-top 28} [react/text {:style {:color colors/gray}} label] @@ -118,6 +77,7 @@ [property (i18n/label :t/type) (case type :watch (i18n/label :t/watch-only) + (:key :seed) (i18n/label :t/off-status-tree) (i18n/label :t/on-status-tree))] [property (i18n/label :t/wallet-address) [copyable-text/copyable-text-view diff --git a/src/status_im/ui/screens/wallet/accounts/sheets.cljs b/src/status_im/ui/screens/wallet/accounts/sheets.cljs index c7ca31efab..130fa0a8bd 100644 --- a/src/status_im/ui/screens/wallet/accounts/sheets.cljs +++ b/src/status_im/ui/screens/wallet/accounts/sheets.cljs @@ -15,25 +15,29 @@ :title :t/wallet-manage-assets :icon :main-icons/token :accessibility-label :wallet-manage-assets - :on-press #(hide-sheet-and-dispatch [:navigate-to :wallet-settings-assets])}] + :on-press #(hide-sheet-and-dispatch + [:navigate-to :wallet-settings-assets])}] [list-item/list-item {:theme :action :title :t/set-currency :icon :main-icons/language :accessibility-label :wallet-set-currency - :on-press #(hide-sheet-and-dispatch [:navigate-to :currency-settings])}] + :on-press #(hide-sheet-and-dispatch + [:navigate-to :currency-settings])}] [list-item/list-item {:theme :action :title :t/view-signing :icon :main-icons/info - :on-press #(hide-sheet-and-dispatch [:show-popover {:view :signing-phrase}])}] + :on-press #(hide-sheet-and-dispatch + [:show-popover {:view :signing-phrase}])}] (when mnemonic [list-item/list-item {:theme :action-destructive :title :t/wallet-backup-recovery-title :icon :main-icons/security :accessibility-label :wallet-backup-recovery-title - :on-press #(hide-sheet-and-dispatch [:navigate-to :backup-seed])}])])) + :on-press #(hide-sheet-and-dispatch + [:navigate-to :backup-seed])}])])) (defn send-receive [account type] [react/view @@ -43,7 +47,8 @@ :title :t/wallet-send :icon :main-icons/send :accessibility-label :send-transaction-button - :on-press #(hide-sheet-and-dispatch [:wallet/prepare-transaction-from-wallet account])}]) + :on-press #(hide-sheet-and-dispatch + [:wallet/prepare-transaction-from-wallet account])}]) [list-item/list-item {:theme :action :title :t/receive @@ -56,27 +61,40 @@ (defn add-account [] [react/view [list-item/list-item - {:theme :action - :title :t/add-an-account + {:title :t/generate-a-new-account + :theme :action :icon :main-icons/add - :on-press #(hide-sheet-and-dispatch [:navigate-to :add-new-account])}] + :on-press #(hide-sheet-and-dispatch + [:wallet.accounts/start-adding-new-account + {:type :generate}])}] [list-item/list-item - {:theme :action - :title :t/add-a-watch-account - :icon :main-icons/watch - :on-press #(hide-sheet-and-dispatch [:wallet.accounts/start-adding-new-account {:type :watch}])}]]) + {:theme :action + :title :t/add-a-watch-account + :icon :main-icons/watch + :on-press #(hide-sheet-and-dispatch + [:wallet.accounts/start-adding-new-account + {:type :watch}])}] + [list-item/list-item + {:title :t/enter-a-seed-phrase + :theme :action + :icon :main-icons/text + :on-press #(hide-sheet-and-dispatch + [:wallet.accounts/start-adding-new-account + {:type :seed}])}] + [list-item/list-item + {:title :t/enter-a-private-key + :theme :action + :icon :main-icons/address + :on-press #(hide-sheet-and-dispatch + [:wallet.accounts/start-adding-new-account + {:type :key}])}]]) (defn account-settings [] [react/view [list-item/list-item - {:theme :action - :title :t/account-settings + {:theme :action + :title :t/account-settings :accessibility-label :account-settings-bottom-sheet - :icon :main-icons/info - :on-press #(hide-sheet-and-dispatch [:navigate-to :account-settings])}] - ;; Commented out for v1 - #_[list-item/list-item - {:theme :action - :title :t/export-account - :icon :main-icons/copy - :disabled? true}]]) + :icon :main-icons/info + :on-press #(hide-sheet-and-dispatch + [:navigate-to :account-settings])}]]) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/accounts/views.cljs b/src/status_im/ui/screens/wallet/accounts/views.cljs index c2f4c4e9d4..2a3c21af27 100644 --- a/src/status_im/ui/screens/wallet/accounts/views.cljs +++ b/src/status_im/ui/screens/wallet/accounts/views.cljs @@ -45,7 +45,7 @@ (defn add-card [] [react/touchable-highlight {:on-press #(re-frame/dispatch [:bottom-sheet/show-sheet {:content sheets/add-account - :content-height 130}])} + :content-height 260}])} [react/view {:style styles/add-card} [react/view {:width 40 :height 40 :justify-content :center :border-radius 20 :align-items :center :background-color colors/blue-transparent-10 :margin-bottom 8} diff --git a/src/status_im/ui/screens/wallet/add_new/views.cljs b/src/status_im/ui/screens/wallet/add_new/views.cljs index 11edc42327..910678c505 100644 --- a/src/status_im/ui/screens/wallet/add_new/views.cljs +++ b/src/status_im/ui/screens/wallet/add_new/views.cljs @@ -5,62 +5,19 @@ [status-im.i18n :as i18n] [re-frame.core :as re-frame] [status-im.ui.components.colors :as colors] - [status-im.ui.components.list-item.views :as list-item] [reagent.core :as reagent] [cljs.spec.alpha :as spec] [status-im.multiaccounts.db :as multiaccounts.db] [status-im.ui.components.toolbar :as toolbar] - [status-im.ui.components.styles :as components.styles] [status-im.ui.components.topbar :as topbar] - [status-im.utils.utils :as utils.utils])) - -(defn add-account [] - [react/view {:flex 1} - [topbar/topbar] - [react/scroll-view {:keyboard-should-persist-taps :handled - :style {:flex 1}} - [react/view {:align-items :center :padding-horizontal 40 :margin-bottom 52} - [react/text {:style {:typography :header :margin-top 16}} - (i18n/label :t/add-an-account)] - [react/text {:style {:color colors/gray :text-align :center :margin-top 16 :line-height 22}} - (i18n/label :t/add-account-description)]] - [list-item/list-item - {:type :section-header - :title :t/default}] - [list-item/list-item - {:title :t/generate-a-new-account - :theme :action - :icon :main-icons/add - :accessories [:chevron] - :on-press #(re-frame/dispatch [:wallet.accounts/start-adding-new-account {:type :generate}])}] - ;;TODO: implement adding account by seedphrase and private key - #_[list-item/list-item - {:type :section-header - :container-margin-top 24 - :title (i18n/label :t/advanced)}] - #_[list-item/list-item - {:title (i18n/label :t/enter-a-seed-phrase) - :theme :action - :icon :main-icons/add - :accessories [:chevron] - :disabled? true - :on-press #(re-frame/dispatch [:wallet.accounts/start-adding-new-account {:type :seed}])}] - #_[list-item/list-item - {:title (i18n/label :t/enter-a-private-key) - :theme :action - :icon :main-icons/add - :accessories [:chevron] - :disabled? true - :on-press #(re-frame/dispatch [:wallet.accounts/start-adding-new-account {:type :key}])}]]]) - -(def input-container - {:flex-direction :row - :align-items :center - :border-radius components.styles/border-radius - :height 52 - :margin 16 - :padding-horizontal 16 - :background-color colors/gray-lighter}) + [status-im.utils.utils :as utils.utils] + [status-im.ui.components.text-input.view :as text-input] + [status-im.ui.components.icons.vector-icons :as icons] + [status-im.ui.screens.wallet.account-settings.views :as account-settings] + [status-im.ethereum.core :as ethereum] + [status-im.utils.security :as security] + [clojure.string :as string] + [status-im.utils.platform :as platform])) (defn- request-camera-permissions [] (let [options {:handler :wallet.add-new/qr-scanner-result}] @@ -76,37 +33,119 @@ (i18n/label :t/camera-access-error))) 50)}]))) -(defview add-watch-account [] - (letsubs [add-account-disabled? [:add-account-disabled?] - add-account-scanned-address [:add-account-scanned-address]] - [react/keyboard-avoiding-view {:flex 1} - [topbar/topbar {:accessories [{:icon :qr :handler #(request-camera-permissions)}]}] - [react/view {:flex 1 - :justify-content :space-between - :align-items :center :margin-horizontal 16} - [react/view - [react/text {:style {:typography :header :margin-top 16}} - (i18n/label :t/add-a-watch-account)] - [react/text {:style {:color colors/gray :text-align :center :margin-vertical 16}} - (i18n/label :t/enter-watch-account-address)]] - [react/view {:align-items :center :flex 1 :flex-direction :row} - [react/text-input {:auto-focus true - :multiline true - :text-align :center - :default-value add-account-scanned-address - :placeholder (i18n/label :t/enter-address) - :style {:typography :header :flex 1} - :on-change-text #(re-frame/dispatch [:set-in [:add-account :address] %])}]]] +(defn add-account-topbar [type] + (let [title (case type + :generate :t/generate-an-account + :watch :t/add-watch-account + :seed :t/add-seed-account + :key :t/add-private-key-account + "")] + [topbar/topbar + (merge {:title title} + (when (= type :watch) + {:accessories [{:icon :qr + :handler #(request-camera-permissions)}]}))])) + +(defn common-settings [account] + [react/view {:margin-horizontal 16 :margin-top 30} + [text-input/text-input-with-label + {:label (i18n/label :t/account-name) + :auto-focus false + :default-value (:name account) + :placeholder (i18n/label :t/account-name) + :on-change-text #(re-frame/dispatch [:set-in [:add-account :account :name] %])}] + [react/text {:style {:margin-top 30}} (i18n/label :t/account-color)] + [react/touchable-highlight + {:on-press #(re-frame/dispatch + [:show-popover + {:view [account-settings/colors-popover (:color account) + (fn [new-color] + (re-frame/dispatch [:set-in [:add-account :account :color] new-color]) + (re-frame/dispatch [:hide-popover]))] + :style {:max-height "60%"}}])} + [react/view {:height 52 :margin-top 12 :background-color (:color account) :border-radius 8 + :align-items :flex-end :justify-content :center :padding-right 12} + [icons/icon :main-icons/dropdown {:color colors/white}]]]]) + +(defn settings [{:keys [type scanned-address password-error account-error]} + entered-password] + [react/view {:margin-horizontal 16} + (if (= type :watch) + [text-input/text-input-with-label + {:label (i18n/label :t/wallet-key-title) + :auto-focus false + :default-value scanned-address + :placeholder (i18n/label :t/enter-address) + :on-change-text #(re-frame/dispatch [:set-in [:add-account :address] %])}] + [text-input/text-input-with-label + {:label (i18n/label :t/password) + :parent-container {:margin-top 30} + :auto-focus false + :placeholder (i18n/label :t/enter-your-password) + :secure-text-entry true + :text-content-type :none + :error (when password-error (i18n/label :t/add-account-incorrect-password)) + :on-change-text #(do + (re-frame/dispatch [:set-in [:add-account :password-error] nil]) + (reset! entered-password %))}]) + (when (= type :seed) + [text-input/text-input-with-label + {:parent-container {:margin-top 30} + :label (i18n/label :t/recovery-phrase) + :auto-focus false + :placeholder (i18n/label :t/multiaccounts-recover-enter-phrase-title) + :auto-correct false + :keyboard-type "visible-password" + :multiline true + :style (when platform/android? + {:flex 1}) + :height 95 + :error account-error + :on-change-text + #(do + (re-frame/dispatch [:set-in [:add-account :account-error] nil]) + (re-frame/dispatch [:set-in [:add-account :seed] (security/mask-data (string/lower-case %))]))}]) + (when (= type :key) + [text-input/text-input-with-label + {:parent-container {:margin-top 30} + :label (i18n/label :t/private-key) + :auto-focus false + :placeholder (i18n/label :t/enter-a-private-key) + :auto-correct false + :keyboard-type "visible-password" + :error account-error + :secure-text-entry true + :text-content-type :none + :on-change-text + #(do + (re-frame/dispatch [:set-in [:add-account :account-error] nil]) + (re-frame/dispatch [:set-in [:add-account :private-key] (security/mask-data %)]))}])]) + +(defview add-account [] + (letsubs [{:keys [type account] :as add-account} [:add-account] + add-account-disabled? [:add-account-disabled?] + entered-password (reagent/atom "")] + [react/keyboard-avoiding-view {:style {:flex 1}} + [add-account-topbar type] + [react/scroll-view {:keyboard-should-persist-taps :handled + :style {:flex 1}} + [settings add-account entered-password] + [common-settings account]] [toolbar/toolbar {:show-border? true - :right {:type :next - :label (i18n/label :t/next) - :on-press #(re-frame/dispatch [:wallet.accounts/add-watch-account]) - :disabled? add-account-disabled?}}]])) + :right + {:type :next + :label :t/add-account + :on-press #(re-frame/dispatch [:wallet.accounts/add-new-account + (ethereum/sha3 @entered-password)]) + :disabled? (or add-account-disabled? + (and + (not (= type :watch)) + (not (spec/valid? ::multiaccounts.db/password @entered-password))))}}]])) (defview pin [] - (letsubs [pin [:hardwallet/pin] - status [:hardwallet/pin-status] + (letsubs [pin [:hardwallet/pin] + status [:hardwallet/pin-status] error-label [:hardwallet/pin-error-label]] [react/keyboard-avoiding-view {:style {:flex 1}} [topbar/topbar] @@ -116,32 +155,4 @@ :title-label :t/current-pin :description-label :t/current-pin-description :error-label error-label - :step :export-key}]])) - -(defview password [] - (letsubs [{:keys [error]} [:add-account] - entered-password (reagent/atom "")] - [react/keyboard-avoiding-view {:style {:flex 1}} - [topbar/topbar] - [react/view {:flex 1 - :justify-content :space-between - :align-items :center :margin-horizontal 16} - [react/text {:style {:typography :header :margin-top 16}} (i18n/label :t/enter-your-password)] - [react/view {:justify-content :center :flex 1} - [react/text-input {:secure-text-entry true - :auto-focus true - :auto-capitalize :none - :text-align :center - :placeholder "" - :style {:typography :header} - :on-change-text #(reset! entered-password %)}] - (when error - [react/text {:style {:text-align :center :color colors/red :margin-top 76}} error])] - [react/text {:style {:color colors/gray :text-align :center :margin-bottom 16}} - (i18n/label :t/to-encrypt-enter-password)]] - [toolbar/toolbar - {:show-border? true - :right {:type :next - :label :t/generate-account - :on-press #(re-frame/dispatch [:wallet.accounts/generate-new-account @entered-password]) - :disabled? (not (spec/valid? ::multiaccounts.db/password @entered-password))}}]])) + :step :export-key}]])) \ No newline at end of file diff --git a/src/status_im/wallet/accounts/core.cljs b/src/status_im/wallet/accounts/core.cljs index 0258808993..b44bc7988f 100644 --- a/src/status_im/wallet/accounts/core.cljs +++ b/src/status_im/wallet/accounts/core.cljs @@ -13,78 +13,228 @@ [status-im.ui.screens.navigation :as navigation] [status-im.utils.fx :as fx] [status-im.utils.types :as types] - [status-im.wallet.core :as wallet])) + [status-im.wallet.core :as wallet] + [clojure.string :as string] + [status-im.utils.security :as security] + [status-im.multiaccounts.recover.core :as recover] + [status-im.ethereum.mnemonic :as mnemonic])) + +(fx/defn start-adding-new-account + {:events [:wallet.accounts/start-adding-new-account]} + [{:keys [db] :as cofx} {:keys [type] :as add-account}] + (let [{:keys [latest-derived-path]} (:multiaccount db) + path-num (inc latest-derived-path) + account (merge + {:color (rand-nth colors/account-colors)} + (when (= type :generate) + {:name (str "Account " path-num)}))] + (fx/merge cofx + {:db (assoc db :add-account (assoc add-account :account account))} + (navigation/navigate-to-cofx :add-new-account nil)))) + +(fx/defn new-account-error + {:events [::new-account-error]} + [{:keys [db]} error-key error] + {:db (update db :add-account merge {error-key error + :step nil})}) + +(defn account-stored [path type] + (fn [result] + (let [{:keys [error publicKey address]} (types/json->clj result)] + (if error + (re-frame/dispatch [::new-account-error :account-error error]) + (re-frame/dispatch [:wallet.accounts/account-stored + {:address address + :public-key publicKey + :type type + :path path}]))))) + +(def dec-pass-error "could not decrypt key with given password") + +(defn normalize-path [path] + (if (string/starts-with? path "m/") + (str constants/path-wallet-root + "/" (last (string/split path "/"))) + path)) + +(defn derive-and-store-account [path hashed-password type] + (fn [value] + (let [{:keys [id error]} (types/json->clj value)] + (if error + (re-frame/dispatch [::new-account-error :password-error error]) + (status/multiaccount-derive-addresses + id + [path] + (fn [_] + (status/multiaccount-store-derived + id + [path] + hashed-password + (fn [result] + (let [{:keys [error] :as result} (types/json->clj result) + {:keys [publicKey address]} (get result (keyword path))] + (if error + (re-frame/dispatch [::new-account-error :account-error error]) + (re-frame/dispatch + [:wallet.accounts/account-stored + {:address address + :public-key publicKey + :type type + :path (normalize-path path)}]))))))))))) + +(def pass-error "cannot retrieve a valid key for a given account: could not decrypt key with given password") + +(defn store-account [path hashed-password type] + (fn [value] + (let [{:keys [id error]} (types/json->clj value)] + (if error + (re-frame/dispatch [::new-account-error + (if (= error pass-error) :password-error :account-error) + error]) + (status/multiaccount-store-account + id + hashed-password + (account-stored path type)))))) (re-frame/reg-fx - :list.selection/open-share - (fn [obj] - (list-selection/open-share obj))) + ::verify-password + (fn [{:keys [address hashed-password]}] + (status/verify + address hashed-password + #(re-frame/dispatch [:wallet.accounts/add-new-account-password-verifyied % hashed-password])))) (re-frame/reg-fx ::generate-account - (fn [{:keys [derivation-info hashed-password path-num]}] + (fn [{:keys [derivation-info hashed-password]}] (let [{:keys [address path]} derivation-info] (status/multiaccount-load-account address hashed-password - (fn [value] - (let [{:keys [id error]} (types/json->clj value)] - (if error - (re-frame/dispatch [::generate-new-account-error]) - (status/multiaccount-derive-addresses - id - [path] - (fn [result] - (status/multiaccount-store-derived - id - [path] - hashed-password - (fn [result] - (let [{:keys [publicKey address]} - (get (types/json->clj result) (keyword path))] - (re-frame/dispatch [:wallet.accounts/account-generated - {:name (str "Account " path-num) - :address address - :public-key publicKey - :path (str constants/path-wallet-root "/" path-num) - :color (rand-nth colors/account-colors)}]))))))))))))) + (derive-and-store-account path hashed-password :generated))))) -(fx/defn set-symbol-request - {:events [:wallet.accounts/share]} - [_ address] - {:list.selection/open-share {:message (eip55/address->checksum address)}}) +(re-frame/reg-fx + ::import-account-seed + (fn [{:keys [passphrase hashed-password]}] + (status/multiaccount-import-mnemonic + (mnemonic/sanitize-passphrase (security/unmask passphrase)) + "" + (derive-and-store-account constants/path-default-wallet hashed-password :seed)))) + +(re-frame/reg-fx + ::import-account-private-key + (fn [{:keys [private-key hashed-password]}] + (status/multiaccount-import-private-key + (string/trim (security/unmask private-key)) + (store-account constants/path-default-wallet hashed-password :key)))) (fx/defn generate-new-account - {:events [:wallet.accounts/generate-new-account]} - [{:keys [db]} password] + [{:keys [db]} hashed-password] (let [wallet-root-address (get-in db [:multiaccount :wallet-root-address]) path-num (inc (get-in db [:multiaccount :latest-derived-path]))] - (when-not (get-in db [:add-account :step]) - {:db (assoc-in db [:add-account :step] :generating) - ::generate-account {:derivation-info (if wallet-root-address - ;; Use the walllet-root-address for stored on disk keys - ;; This needs to be the RELATIVE path to the key used to derive - {:path (str "m/" path-num) - :address wallet-root-address} - ;; Fallback on the master account for keycards, use the absolute path - {:path (str constants/path-wallet-root "/" path-num) - :address (get-in db [:multiaccount :address])}) - :path-num path-num - :hashed-password (ethereum/sha3 password)}}))) + {:db (assoc-in db [:add-account :step] :generating) + ::generate-account {:derivation-info (if wallet-root-address + ;; Use the walllet-root-address for stored on disk keys + ;; This needs to be the RELATIVE path to the key used to derive + {:path (str "m/" path-num) + :address wallet-root-address} + ;; Fallback on the master account for keycards, use the absolute path + {:path (str constants/path-wallet-root "/" path-num) + :address (get-in db [:multiaccount :address])}) + :hashed-password hashed-password}})) -(fx/defn generate-new-account-error - {:events [::generate-new-account-error]} - [{:keys [db]} password] - {:db (assoc db - :add-account - {:error (i18n/label :t/add-account-incorrect-password)})}) +(fx/defn import-new-account-seed + [{:keys [db]} passphrase hashed-password] + {:db (assoc-in db [:add-account :step] :generating) + ::recover/validate-mnemonic [(security/safe-unmask-data passphrase) + #(re-frame/dispatch [:wallet.accounts/seed-validated + % passphrase hashed-password])]}) + +(fx/defn new-account-seed-validated + {:events [:wallet.accounts/seed-validated]} + [cofx phrase-warnings passphrase hashed-password] + (let [error (:error (types/json->clj phrase-warnings))] + (if-not (string/blank? error) + (new-account-error cofx :account-error error) + {::import-account-seed {:passphrase passphrase + :hashed-password hashed-password}}))) + +(fx/defn import-new-account-private-key + [{:keys [db]} private-key hashed-password] + {:db (assoc-in db [:add-account :step] :generating) + ::import-account-private-key {:private-key private-key + :hashed-password hashed-password}}) + +(fx/defn save-new-account + [{:keys [db] :as cofx}] + (let [{:keys [latest-derived-path]} (:multiaccount db) + {:keys [account type]} (:add-account db) + accounts (:multiaccount/accounts db) + new-accounts (conj accounts account)] + (when account + (fx/merge cofx + {::json-rpc/call [{:method "accounts_saveAccounts" + :params [[account]] + :on-success #()}] + :db (-> db + (assoc :multiaccount/accounts new-accounts) + (dissoc :add-account))} + (when (= type :generate) + (multiaccounts.update/multiaccount-update + :latest-derived-path (inc latest-derived-path) + {})))))) (fx/defn account-generated - {:events [:wallet.accounts/account-generated]} - [{:keys [db] :as cofx} account] - (fx/merge cofx - {:db (update db :add-account assoc :account account :step :generated)} - (navigation/navigate-to-cofx :account-added nil))) + {:events [:wallet.accounts/account-stored]} + [{:keys [db] :as cofx} {:keys [address] :as account}] + (let [accounts (:multiaccount/accounts db)] + (if (some #(when (= (:address %) address) %) accounts) + (new-account-error cofx :account-error (i18n/label :t/account-exists-title)) + (fx/merge cofx + {:db (update-in db [:add-account :account] merge account)} + (save-new-account) + (wallet/update-balances nil) + (wallet/update-prices) + (navigation/navigate-back))))) + +(fx/defn add-watch-account + [{:keys [db] :as cofx}] + (let [address (get-in db [:add-account :address])] + (account-generated cofx {:address (eip55/address->checksum (ethereum/normalized-hex address)) + :type :watch}))) + +(fx/defn add-new-account-password-verifyied + {:events [:wallet.accounts/add-new-account-password-verifyied]} + [{:keys [db] :as cofx} result hashed-password] + (let [{:keys [error]} (types/json->clj result)] + (if (not (string/blank? error)) + (new-account-error cofx :password-error error) + (let [{:keys [type step seed private-key]} (:add-account db)] + (case type + :seed + (import-new-account-seed cofx seed hashed-password) + :key + (import-new-account-private-key cofx private-key hashed-password) + nil))))) + +(fx/defn add-new-account-verify-password + [{:keys [db]} hashed-password] + {:db (assoc-in db [:add-account :step] :generating) + ::verify-password {:address (get-in db [:multiaccount :wallet-root-address]) + :hashed-password hashed-password}}) + +(fx/defn add-new-account + {:events [:wallet.accounts/add-new-account]} + [{:keys [db] :as cofx} hashed-password] + (let [{:keys [type step]} (:add-account db)] + (when-not step + (case type + :watch + (add-watch-account cofx) + :generate + (generate-new-account cofx hashed-password) + (:seed :key) + (add-new-account-verify-password cofx hashed-password) + nil)))) (fx/defn save-account {:events [:wallet.accounts/save-account]} @@ -114,63 +264,6 @@ (assoc-in [:wallet :accounts deleted-address] nil))} (navigation/navigate-to-cofx :wallet nil)))) -(fx/defn save-generated-account - {:events [:wallet.accounts/save-generated-account]} - [{:keys [db] :as cofx}] - (let [{:keys [latest-derived-path]} (:multiaccount db) - {:keys [account path type]} (:add-account db) - accounts (:multiaccount/accounts db) - new-accounts (conj accounts account)] - (when account - (fx/merge cofx - {::json-rpc/call [{:method "accounts_saveAccounts" - :params [[account]] - :on-success #()}] - :db (-> db - (assoc :multiaccount/accounts new-accounts) - (dissoc :add-account))} - (when (= type :generate) - (multiaccounts.update/multiaccount-update - :latest-derived-path (inc latest-derived-path) - {})) - (wallet/update-balances nil) - (navigation/navigate-to-cofx :wallet nil))))) - -(fx/defn start-adding-new-account - {:events [:wallet.accounts/start-adding-new-account]} - [{:keys [db] :as cofx} {:keys [type] :as add-account}] - (let [{:keys [keycard-pairing]} (:multiaccount db) - screen (case type - :generate (if keycard-pairing :add-new-account-pin - :add-new-account-password) - :watch :add-watch-account)] - (fx/merge cofx - {:db (cond-> (assoc db :add-account add-account) - keycard-pairing - (assoc-in [:hardwallet :pin :enter-step] :export-key))} - (navigation/navigate-to-cofx screen nil)))) - -(fx/defn enter-phrase-next-pressed - {:events [:wallet.accounts/enter-phrase-next-pressed]} - [{:keys [db] :as cofx}] - (fx/merge cofx - {:db (-> db - (dissoc :intro-wizard) - (assoc-in [:add-account :seed] (get-in db [:intro-wizard :passphrase])))} - (navigation/navigate-to-cofx :add-new-account-password nil))) - -(fx/defn add-watch-account - {:events [:wallet.accounts/add-watch-account]} - [{:keys [db] :as cofx}] - (let [address (get-in db [:add-account :address])] - (fx/merge cofx - {:db (assoc-in db [:add-account :account] - {:name "" - :address (eip55/address->checksum (ethereum/normalized-hex address)) - :type :watch - :color (rand-nth colors/account-colors)})} - (navigation/navigate-to-cofx :account-added nil)))) - (fx/defn view-only-qr-scanner-result {:events [:wallet.add-new/qr-scanner-result]} [{db :db :as cofx} data _] @@ -184,3 +277,13 @@ {:utils/show-popup {:title (i18n/label :t/error) :content (i18n/label :t/invalid-address-qr-code)}})) (navigation/navigate-back)))) + +(re-frame/reg-fx + :list.selection/open-share + (fn [obj] + (list-selection/open-share obj))) + +(fx/defn wallet-accounts-share + {:events [:wallet.accounts/share]} + [_ address] + {:list.selection/open-share {:message (eip55/address->checksum address)}}) \ No newline at end of file diff --git a/translations/en.json b/translations/en.json index 7a2e60d588..a225efe084 100644 --- a/translations/en.json +++ b/translations/en.json @@ -443,7 +443,7 @@ "gas-price": "Gas price", "gas-used": "Gas used", "generate-a-key": "Generate keys", - "generate-a-new-account": "Generate keys", + "generate-a-new-account": "Generate an account", "generate-a-new-key": "Generate a new key", "generate-account": "Generate keys", "generate-new-key": "Generate keys", @@ -1079,6 +1079,7 @@ "select-account-dapp": "Select the account you wish to use with Dapps", "apply": "Apply", "on-status-tree": "On Status tree", + "off-status-tree": "Off Status tree", "derivation-path": "Derivation path", "storage": "Storage", "keycard-free-pairing-slots": "Keycard has {{n}} free pairing slots", @@ -1089,5 +1090,11 @@ "mail-should-be-configured": "Mail client should be configured", "check-on-etherscan": "Check on etherscan", "transactions-load-more": "Load more", + "private-key": "Private key", + "generate-an-account": "Generate an account", + "add-watch-account": "Add a watch-only account", + "add-seed-account": "Add account with a seed phrase", + "account-exists-title": "Account already exists", + "add-private-key-account": "Add account from private key", "user-not-found": "User not found" }