From 6a7f04a5a87619ea89f8868b2d0d67853bd4775a Mon Sep 17 00:00:00 2001 From: Sean Hagstrom Date: Tue, 11 Jun 2024 13:05:10 +0100 Subject: [PATCH] feat(wallet-settings): Implement import missing key pair by private key (#20372) * chore: update private-key validation * chore: add native module bindings * chore: add translation labels * chore: add size-12 for password icon * fix: ensure we use feature-flag namespace when using feature-flags * fix: blur styles for icon-avatar in missing key-pairs * fix: blur styles for accounts in missing key-pairs * fix: blur styles for bottom-sheet * fix: adjust blur background color for bottom-sheets * chore: add support for private-keys for missing key-pair component * chore: add definitions for events and effects * feature: implement private-key import for missing key-pair * tidy: remove debugging * tidy: distinguish to import for missing-keypair * tidy: remove unneeded function * tidy: refactor layout to use floating-button-page * tidy: remove unused effect bindings * tidy: refactor event bindings for importing-missing-keypair-by-private-key * tweak: add error logging * tidy: refactor helper to re-frame utils * tweak: group partially operable and fully operable keypairs together * chore: add documentation for call-continuation * tweak: refactor documentation * tweak: throw exception when not given a compatible continuation * fix: update least-operability when making key-pair fully operable --- .../status/ethereum/module/AccountManager.kt | 5 + .../ios/RCTStatus/AccountManager.m | 8 + resources/images/icons2/12x12/password@2x.png | Bin 0 -> 758 bytes resources/images/icons2/12x12/password@3x.png | Bin 0 -> 1011 bytes src/native_module/core.cljs | 11 ++ src/quo/components/avatars/icon_avatar.cljs | 6 +- .../list_items/missing_keypair/view.cljs | 22 +-- src/quo/components/wallet/keypair/view.cljs | 4 +- src/status_im/constants.cljs | 3 +- .../contexts/settings/wallet/data_store.cljs | 6 +- .../contexts/settings/wallet/events.cljs | 63 +++++++- .../contexts/settings/wallet/events_test.cljs | 8 +- .../keypairs_and_accounts/actions/view.cljs | 11 +- .../import_private_key/style.cljs | 9 ++ .../import_private_key/view.cljs | 138 ++++++++++++++++++ .../wallet/keypairs_and_accounts/view.cljs | 32 ++-- .../create_account/select_keypair/view.cljs | 2 +- .../contexts/wallet/common/validation.cljs | 5 +- src/status_im/contexts/wallet/effects.cljs | 52 +++++-- src/status_im/navigation/screens.cljs | 6 + src/status_im/subs/wallet/wallet.cljs | 14 +- src/status_im/subs/wallet/wallet_test.cljs | 18 ++- src/utils/re_frame.cljs | 42 ++++++ translations/en.json | 3 + 24 files changed, 401 insertions(+), 67 deletions(-) create mode 100644 resources/images/icons2/12x12/password@2x.png create mode 100644 resources/images/icons2/12x12/password@3x.png create mode 100644 src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_private_key/style.cljs create mode 100644 src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_private_key/view.cljs diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/AccountManager.kt b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/AccountManager.kt index b829d2af9d..51b8fea480 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/AccountManager.kt +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/AccountManager.kt @@ -314,6 +314,11 @@ class AccountManager(private val reactContext: ReactApplicationContext) : ReactC ) } + @ReactMethod + fun createAccountFromPrivateKey(json: String, callback: Callback) { + utils.executeRunnableStatusGoMethod({ Statusgo.createAccountFromPrivateKey(json) }, callback) + } + companion object { private const val TAG = "AccountManager" private const val gethLogFileName = "geth.log" diff --git a/modules/react-native-status/ios/RCTStatus/AccountManager.m b/modules/react-native-status/ios/RCTStatus/AccountManager.m index 328800396c..3caafde74c 100644 --- a/modules/react-native-status/ios/RCTStatus/AccountManager.m +++ b/modules/react-native-status/ios/RCTStatus/AccountManager.m @@ -233,4 +233,12 @@ RCT_EXPORT_METHOD(createAccountFromMnemonicAndDeriveAccountsForPaths:(NSString * callback(@[result]); } +RCT_EXPORT_METHOD(createAccountFromPrivateKey:(NSString *)configJSON callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"createAccountFromPrivateKey() method called"); +#endif + NSString *result = StatusgoCreateAccountFromPrivateKey(configJSON); + callback(@[result]); +} + @end diff --git a/resources/images/icons2/12x12/password@2x.png b/resources/images/icons2/12x12/password@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ded220f72fd926d226039e46c2dfb0b8a554321c GIT binary patch literal 758 zcmVWUqJsLlN-018@Hr4ZZ&PWi-Aru zgJZF4Nv;!Or|rNDJL8eOT0QOV+m!(Ru`sx!Y{}M+0roltqwrVar#Z0E9D-=3W&IdH)_8xjX05H<2A`Wn7(0hbXIKBcK5kbZeAb75ItZ#!2 zLMm04Ekoa?Y>fq`Xq8=EZtr_L{7$_-Sa(28JRC!oYw|37`BPtfA-$AJ2BPtHLb zp6HqW`Y{xbK4D6FxU6vB_hk0tb7hK%&^bWnje+o~C(-gTKP{vTi8kG9{cVEYV=Cbt z>8ASXF|RrR!FSzsRICsLA6jrm9FSh7z=yPkQTCmgY{+J(@&J@)b}*4w5GS5c4r!M| z@W$CFOq;Ugn%L?%MtTe3!1IcrNb0gpAm}waWH9$@TDvG2MrQK86~eLL-eK8Oi1p~83JfN4OYkhe_YYi-b6q1bm6oN@9 zw5|X4xv%ZA`d^k!+5S-M1zrZuY#A!A!+IWCf~&w=hlP|Ktvp~6JR_^?OOZh`uFK{= zHCb^F^$U57wtWvi(^rvmtQ9hFoy)DO zqH2Q6B)plhc%IFSw6B)YSpUa9{IwpgJ0O8D#wwwyv^S`yCrjA@+*9)RU`t56J<}J= ozET2JVwsP>Dpc#s_5NSVZw2ERBOW+d9{>OV07*qoM6N<$g0Ia}DgXcg literal 0 HcmV?d00001 diff --git a/resources/images/icons2/12x12/password@3x.png b/resources/images/icons2/12x12/password@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d1a554005bd850e321982ef31921772097b7de53 GIT binary patch literal 1011 zcmVm1Z@=SXp8jmZ{GlM z_R}A@@HYt&ktx6-7MmrfxB{&1$oIUWP805d8sGdN06t;SsW8o95bUqo&tW{exzIb8 z*3dfNIKd<8XySZ27HoF|5I(gSz&QT2k@y<`;b@EZe))qVxG&auE-^geSn?Vx>h)rF!lc?}ei$7An_$U8$| z8T$sV0TqCQTj~NjnXt8xOt@r7l(a{+Rj@vi6U4pf?rph7d5BD`36AxC(x67hm0v>Y z4#piS`M!r_#91OO^uh{+>@W=^XO84pUms&+gJWnH(l*??@s(7+s3~5d5$!Zhh%z4xv?Ou|*m?xvOhA?0GYJCjj z+^*NaIEC!4n~*uitRnNOr;mjOGa50^Pu19L|YpsLq0=_I8CV?lIHf*C9UJ;=tgN)aj|6QNoj zOUZ=M{4$ltLakD1;gQWILx`fUUE!*#q?SaU0m0j}T#*c__DCoZHYQaMqx5{#_^bC) zGma`R&h!LUl?tQ{Qrm4*9bFG-X}!pFYP zDC+=a9ZW_y9?YVvR_dso04A5S$Vfs9Z;Z4tOf}q;>846p$Rt7D8APdKi2o$0 h19;JE;Xj@Ze*m$H!Gm;?drtrW002ovPDHLkV1gVP%6I?( literal 0 HcmV?d00001 diff --git a/src/native_module/core.cljs b/src/native_module/core.cljs index a8bfbdc8f6..9195bf0f94 100644 --- a/src/native_module/core.cljs +++ b/src/native_module/core.cljs @@ -636,3 +636,14 @@ connection-string config-json callback))) + +(defn create-account-from-private-key + "Validate that a mnemonic conforms to BIP39 dictionary/checksum standards" + ([private-key] + (native-utils/promisify-native-module-call create-account-from-private-key private-key)) + ([private-key callback] + (log/debug "[native-module] create-account-from-private-key") + + (.createAccountFromPrivateKey ^js (account-manager) + (types/clj->json {:privateKey private-key}) + callback))) diff --git a/src/quo/components/avatars/icon_avatar.cljs b/src/quo/components/avatars/icon_avatar.cljs index 3ee56af290..646eb074f4 100644 --- a/src/quo/components/avatars/icon_avatar.cljs +++ b/src/quo/components/avatars/icon_avatar.cljs @@ -16,7 +16,7 @@ :icon 12}}) (defn icon-avatar - [{:keys [size icon color opacity border?] + [{:keys [size icon color opacity border? blur?] :or {opacity 20 size :size-32}}] (let [theme (quo.theme/use-theme) @@ -29,7 +29,9 @@ :height component-size :border-radius component-size :border-width (when border? 1) - :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :border-color (if blur? + colors/white-opa-5 + (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)) :background-color circle-color :justify-content :center :align-items :center}} diff --git a/src/quo/components/list_items/missing_keypair/view.cljs b/src/quo/components/list_items/missing_keypair/view.cljs index 8f03f9f4dc..f8af44ddbc 100644 --- a/src/quo/components/list_items/missing_keypair/view.cljs +++ b/src/quo/components/list_items/missing_keypair/view.cljs @@ -11,8 +11,8 @@ [schema.core :as schema])) (defn- internal-view - [{{:keys [accounts name]} :keypair - :keys [keypair blur? on-options-press]}] + [{{:keys [accounts name type]} :keypair + :keys [keypair blur? on-options-press]}] (let [theme (quo.theme/use-theme) on-keypair-options-press (rn/use-callback (fn [event] @@ -22,15 +22,15 @@ {:style (style/container {:theme theme :blur? blur?}) :accessibility-label :missing-keypair-item} - [rn/view - {:style (style/icon-container {:theme theme - :blur? blur?}) - :accessibility-label :icon} - [icon-avatar/icon-avatar - {:size :size-32 - :icon :i/seed - :color :neutral - :border? false}]] + [icon-avatar/icon-avatar + {:size :size-32 + :icon (case type + :seed :i/seed + :key :i/password + nil) + :color :neutral + :blur? true + :border? true}] [rn/view {:style style/name-container :accessibility-label :name} diff --git a/src/quo/components/wallet/keypair/view.cljs b/src/quo/components/wallet/keypair/view.cljs index f6065daab0..a31e0b3178 100644 --- a/src/quo/components/wallet/keypair/view.cljs +++ b/src/quo/components/wallet/keypair/view.cljs @@ -19,7 +19,8 @@ (i18n/label :t/keypair-title {:name first-name}))) (defn avatar - [{{:keys [full-name]} :details + [{:keys [blur?] + {:keys [full-name]} :details avatar-type :type customization-color :customization-color profile-picture :profile-picture}] @@ -34,6 +35,7 @@ [icon-avatar/icon-avatar {:size :size-32 :icon :i/seed + :blur? blur? :border? true}])) (defn title-view diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 974a40964f..ae8854c4c9 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -228,7 +228,8 @@ (def regx-string-public-key "0x04[0-9a-f]{128}") (def regx-compressed-key (re-pattern (str "^" regx-string-compressed-key "$"))) (def regx-public-key (re-pattern (str "^" regx-string-public-key "$"))) -(def regx-private-key #"^0x[0-9a-fA-F]{64}$") +(def regx-private-key-hex #"^0x[0-9a-fA-F]{64}$") +(def regx-private-key #"^[0-9a-fA-F]{64}$") (def regx-emoji #"^((?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC69\uDC6E\uDC70-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD26\uDD30-\uDD39\uDD3D\uDD3E\uDDD1-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])?|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDEEB\uDEEC\uDEF4-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267B\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])\uFE0F|[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF])+$") (def regx-bold #"\*[^*]+\*") diff --git a/src/status_im/contexts/settings/wallet/data_store.cljs b/src/status_im/contexts/settings/wallet/data_store.cljs index f24a3cdc97..a9c9b47aba 100644 --- a/src/status_im/contexts/settings/wallet/data_store.cljs +++ b/src/status_im/contexts/settings/wallet/data_store.cljs @@ -40,8 +40,8 @@ [keypairs key-uids-set] (map (fn [keypair] (if (contains? key-uids-set (:key-uid keypair)) - (update keypair - :accounts - make-keypairs-accounts-fully-operable) + (-> keypair + (update :accounts make-keypairs-accounts-fully-operable) + (assoc :lowest-operability :fully)) keypair)) keypairs)) diff --git a/src/status_im/contexts/settings/wallet/events.cljs b/src/status_im/contexts/settings/wallet/events.cljs index a9347dcdb8..13ffb0dec8 100644 --- a/src/status_im/contexts/settings/wallet/events.cljs +++ b/src/status_im/contexts/settings/wallet/events.cljs @@ -137,6 +137,36 @@ (rf/reg-event-fx :wallet/make-seed-phrase-keypair-fully-operable make-seed-phrase-keypair-fully-operable) +(defn make-private-key-keypair-fully-operable + [_ [private-key password on-success on-error]] + {:fx [[:json-rpc/call + [{:method "accounts_makePrivateKeyKeypairFullyOperable" + :params [(security/safe-unmask-data private-key) + (-> password security/safe-unmask-data native-module/sha3)] + :on-success on-success + :on-error on-error}]]]}) + +(rf/reg-event-fx :wallet/make-private-key-keypair-fully-operable make-private-key-keypair-fully-operable) + +(defn create-account-from-private-key + [_ [private-key on-success on-error]] + {:fx [[:effects.wallet/create-account-from-private-key + {:private-key (security/safe-unmask-data private-key) + :on-success on-success + :on-error on-error}]]}) + +(rf/reg-event-fx :wallet/create-account-from-private-key create-account-from-private-key) + +(defn verify-private-key-for-keypair + [_ [keypair-key-uid private-key on-success on-error]] + {:fx [[:effects.wallet/verify-private-key-for-keypair + {:keypair-key-uid keypair-key-uid + :private-key (security/safe-unmask-data private-key) + :on-success on-success + :on-error on-error}]]}) + +(rf/reg-event-fx :wallet/verify-private-key-for-keypair verify-private-key-for-keypair) + (defn import-keypair-by-seed-phrase [_ [{:keys [keypair-key-uid seed-phrase password on-success on-error]}]] {:fx [[:import-keypair-by-seed-phrase @@ -146,14 +176,10 @@ :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))) + (rf/call-continuation 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/call-continuation on-error error))}]]}) (rf/reg-event-fx :wallet/import-keypair-by-seed-phrase import-keypair-by-seed-phrase) @@ -170,3 +196,28 @@ :text (:error error-data)}]]]}))) (rf/reg-event-fx :wallet/import-keypair-by-seed-phrase-failed import-keypair-by-seed-phrase-failed) + +(defn import-missing-keypair-by-private-key + [_ [{:keys [keypair-key-uid private-key password on-success on-error]}]] + {:fx [[:dispatch + [:wallet/make-private-key-keypair-fully-operable private-key password + (fn [] + (rf/dispatch [:wallet/make-keypairs-accounts-fully-operable #{keypair-key-uid}]) + (rf/call-continuation on-success)) + (fn [error] + (rf/dispatch [:wallet/import-missing-keypair-by-private-key-failed error]) + (log/error "failed to import missing keypair with private key" {:error error}) + (rf/call-continuation on-error error))]]]}) + +(rf/reg-event-fx :wallet/import-missing-keypair-by-private-key import-missing-keypair-by-private-key) + +(defn import-missing-keypair-by-private-key-failed + [_ [error]] + {:fx [[:dispatch + [:toasts/upsert + {:type :negative + :theme :dark + :text error}]]]}) + +(rf/reg-event-fx :wallet/import-missing-keypair-by-private-key-failed + import-missing-keypair-by-private-key-failed) diff --git a/src/status_im/contexts/settings/wallet/events_test.cljs b/src/status_im/contexts/settings/wallet/events_test.cljs index dafced1e67..f1690b25bc 100644 --- a/src/status_im/contexts/settings/wallet/events_test.cljs +++ b/src/status_im/contexts/settings/wallet/events_test.cljs @@ -57,8 +57,9 @@ (sut/remove-keypair cofx [mock-key-uid]))))))) (deftest make-keypairs-accounts-fully-operable-test - (let [db (mock-db [{:key-uid mock-key-uid - :accounts [{:key-uid mock-key-uid :operable "no"}]}] + (let [db (mock-db [{:key-uid mock-key-uid + :lowest-operability :no + :accounts [{:key-uid mock-key-uid :operable "no"}]}] {"0x1" {:key-uid mock-key-uid :operable "no"}}) key-uids-to-update [mock-key-uid]] (testing "make-keypairs-accounts-fully-operable" @@ -68,7 +69,8 @@ (get-in result-db [:wallet :keypairs])) updated-account (get-in result-db [:wallet :accounts "0x1"])] (is (= (keyword (-> updated-keypair :accounts first :operable)) :fully)) - (is (= (keyword (:operable updated-account)) :fully)))))) + (is (= (keyword (:operable updated-account)) :fully)) + (is (= (:lowest-operability updated-keypair) :fully)))))) (deftest connection-string-for-import-keypair-test (let [cofx {:db (mock-db [] {})} 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 1a113ceacb..d0e822a4bd 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 @@ -28,6 +28,11 @@ [keypair]) on-import-seed-phrase (rn/use-callback #(rf/dispatch [:open-modal :screen/settings.import-seed-phrase keypair]) + [keypair]) + on-import-private-key (rn/use-callback + #(rf/dispatch [:open-modal + :screen/settings.missing-keypair-import-private-key + keypair]) [keypair])] [:<> [quo/drawer-top drawer-props] @@ -48,7 +53,11 @@ :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)} + :on-press on-import-seed-phrase} + :key {:icon :i/key + :accessibility-label :import-private-key + :label (i18n/label :t/import-by-entering-private-key) + :on-press on-import-private-key} nil)) {:icon :i/edit :accessibility-label :rename-key-pair diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_private_key/style.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_private_key/style.cljs new file mode 100644 index 0000000000..01dbf913c8 --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_private_key/style.cljs @@ -0,0 +1,9 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.import-private-key.style) + +(def form-container + {:row-gap 8 + :padding-top 8 + :padding-horizontal 20}) + +(def slide-container + {:flex-direction :row}) diff --git a/src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_private_key/view.cljs b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_private_key/view.cljs new file mode 100644 index 0000000000..beeb35b8ff --- /dev/null +++ b/src/status_im/contexts/settings/wallet/keypairs_and_accounts/import_private_key/view.cljs @@ -0,0 +1,138 @@ +(ns status-im.contexts.settings.wallet.keypairs-and-accounts.import-private-key.view + (:require + [clojure.string :as string] + [quo.core :as quo] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.common.standard-authentication.core :as standard-auth] + [status-im.contexts.settings.wallet.keypairs-and-accounts.import-private-key.style :as style] + [status-im.contexts.wallet.common.validation :as validation] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.security.core :as security])) + +(defn navigate-back + [] + (rf/dispatch [:navigate-back])) + +(defn view + [] + (let [blur? true + insets (safe-area/get-insets) + keypair (rf/sub [:get-screen-params]) + customization-color (rf/sub [:profile/customization-color]) + [private-key set-private-key] (rn/use-state "") + [flow-state set-flow-state] (rn/use-state nil) + error? (case flow-state + (:incorrect-private-key + :invalid-private-key) true + false) + clear-errors (rn/use-callback + #(set-flow-state nil)) + show-invalid (rn/use-callback + #(set-flow-state :invalid-private-key)) + show-correct (rn/use-callback + #(set-flow-state :correct-private-key)) + show-incorrect (rn/use-callback + #(set-flow-state :incorrect-private-key)) + verify-private-key (rn/use-callback + (fn [input] + (rf/dispatch [:wallet/verify-private-key-for-keypair + (:key-uid keypair) + (security/mask-data input) + show-correct + show-incorrect])) + [keypair]) + validate-private-key (rn/use-callback + (debounce/debounce + (fn [input] + (if-not (validation/private-key? input) + (show-invalid) + (do (clear-errors) + (verify-private-key input)))) + 500) + [verify-private-key]) + on-change (rn/use-callback + (fn [input] + (set-private-key input) + (validate-private-key input)) + [validate-private-key]) + on-paste (rn/use-callback + #(clipboard/get-string + (fn [clipboard] + (when-not (empty? clipboard) + (on-change clipboard)))) + [on-change]) + on-import-error (rn/use-callback + (fn [_error] + (rf/dispatch [:hide-bottom-sheet]) + (show-invalid))) + 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-missing-keypair-by-private-key + {:keypair-key-uid (:key-uid keypair) + :private-key (security/mask-data private-key) + :password password + :on-success on-import-success + :on-error on-import-error}])) + [keypair private-key on-import-success on-import-error])] + [quo/overlay {:type :shell} + [floating-button-page/view + {:footer-container-padding 0 + :header [quo/page-nav + {:margin-top (:top insets) + :background :blur + :icon-name :i/close + :on-press navigate-back}] + :footer [rn/view {:style style/slide-container} + [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/key + :disabled? (or error? (string/blank? private-key)) + :dependencies [on-auth-success]}]]} + [quo/page-top + {:blur? true + :title (i18n/label :t/import-private-key) + :description :context-tag + :context-tag {:type :icon + :icon :i/password + :size 24 + :context (:name keypair)}}] + [rn/view {:style style/form-container} + [quo/input + {:accessibility-label :import-private-key + :placeholder (i18n/label :t/enter-private-key-placeholder) + :label (i18n/label :t/private-key) + :type :password + :blur? blur? + :error? error? + :return-key-type :done + :auto-focus true + :on-change-text on-change + :button (when (empty? private-key) + {:on-press on-paste + :text (i18n/label :t/paste)}) + :default-value private-key}] + (when flow-state + [quo/info-message + {:type (if (= flow-state :correct-private-key) :success :error) + :size :default + :icon :i/info} + (case flow-state + :correct-private-key (i18n/label :t/correct-private-key) + :invalid-private-key (i18n/label :t/invalid-private-key) + :incorrect-private-key (i18n/label :t/incorrect-private-key {:name (:name keypair)}) + nil)])]]])) 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 8dff0f45d8..cf2e8a33b9 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 @@ -1,5 +1,6 @@ (ns status-im.contexts.settings.wallet.keypairs-and-accounts.view (:require [quo.core :as quo] + [quo.foundations.colors :as colors] [quo.theme] [react-native.core :as rn] [react-native.safe-area :as safe-area] @@ -16,10 +17,13 @@ (defn on-options-press [{:keys [drawer-props keypair]}] (rf/dispatch [:show-bottom-sheet - {:content (fn [] [actions/view - {:drawer-props drawer-props - :keypair keypair}]) - :theme (:theme drawer-props)}])) + {:content (fn [] [actions/view + {:drawer-props drawer-props + :keypair keypair}]) + + :blur-background colors/bottom-sheet-background-blur + :theme (:theme drawer-props) + :shell? true}])) (defn options-drawer-props [{{:keys [name]} :keypair @@ -79,15 +83,17 @@ (defn on-missing-keypair-options-press [_event keypair-data] (rf/dispatch [:show-bottom-sheet - {:theme :dark - :content (fn [] [actions/view - {:keypair keypair-data - :drawer-props (options-drawer-props - {:theme :dark - :type :keypair - :stored :missing - :blur? true - :keypair keypair-data})}])}])) + {:theme :dark + :shell? true + :blur-background colors/bottom-sheet-background-blur + :content (fn [] [actions/view + {:keypair keypair-data + :drawer-props (options-drawer-props + {:theme :dark + :type :keypair + :stored :missing + :blur? true + :keypair keypair-data})}])}])) (defn view [] diff --git a/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs index dcf2331e5e..312c313a3b 100644 --- a/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs +++ b/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs @@ -23,7 +23,7 @@ :add-divider? true :on-press #(rf/dispatch [:navigate-to :screen/wallet.enter-seed-phrase {:recovering-keypair? true}])} - (when (ff/enabled? ::wallet.import-private-key) + (when (ff/enabled? ::ff/wallet.import-private-key) {:icon :i/key :accessibility-label :import-private-key :label (i18n/label :t/import-private-key) diff --git a/src/status_im/contexts/wallet/common/validation.cljs b/src/status_im/contexts/wallet/common/validation.cljs index f11969db24..532c2d61ba 100644 --- a/src/status_im/contexts/wallet/common/validation.cljs +++ b/src/status_im/contexts/wallet/common/validation.cljs @@ -3,4 +3,7 @@ (defn ens-name? [s] (boolean (re-find constants/regx-ens s))) (defn eth-address? [s] (re-find constants/regx-multichain-address s)) -(defn private-key? [s] (re-find constants/regx-private-key s)) +(defn private-key? + [s] + (or (re-find constants/regx-private-key-hex s) + (re-find constants/regx-private-key s))) diff --git a/src/status_im/contexts/wallet/effects.cljs b/src/status_im/contexts/wallet/effects.cljs index 5f0d0d914b..7987e93527 100644 --- a/src/status_im/contexts/wallet/effects.cljs +++ b/src/status_im/contexts/wallet/effects.cljs @@ -3,9 +3,9 @@ [clojure.string :as string] [native-module.core :as native-module] [promesa.core :as promesa] - [re-frame.core :as rf] [status-im.common.json-rpc.events :as json-rpc] [taoensso.timbre :as log] + [utils.re-frame :as rf] [utils.security.core :as security] [utils.transforms :as transforms])) @@ -46,6 +46,27 @@ (when (and error (fn? on-error)) (on-error error))))))) +(defn create-account-from-private-key + [private-key] + (-> private-key + (security/safe-unmask-data) + (native-module/create-account-from-private-key) + (promesa/then (fn [result] + (let [{:keys [address emojiHash keyUid + publicKey privateKey]} (transforms/json->clj result)] + {:address address + :emoji-hash emojiHash + :key-uid keyUid + :public-key publicKey + :private-key privateKey}))))) + +(rf/reg-fx + :effects.wallet/create-account-from-private-key + (fn [[private-key on-success on-error]] + (-> (create-account-from-private-key private-key) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) + (defn make-seed-phrase-fully-operable [mnemonic password] (promesa/create @@ -80,11 +101,24 @@ :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))))))) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) + +(defn verify-private-key-for-keypair + [keypair-key-uid private-key] + (-> (create-account-from-private-key private-key) + (promesa/then + (fn [{:keys [key-uid] :as result}] + (if (= keypair-key-uid key-uid) + result + (promesa/rejected + (ex-info + (error-message :verify-private-key-for-keypair/verification-error) + {:hint :incorrect-private-key-for-keypair}))))))) + +(rf/reg-fx + :effects.wallet/verify-private-key-for-keypair + (fn [{:keys [keypair-key-uid private-key on-success on-error]}] + (-> (verify-private-key-for-keypair keypair-key-uid private-key) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) diff --git a/src/status_im/navigation/screens.cljs b/src/status_im/navigation/screens.cljs index 53335b9526..d2123641ce 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-private-key.view :as + import-private-key] [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] @@ -553,6 +555,10 @@ :options options/transparent-screen-options :component import-seed-phrase/view} + {:name :screen/settings.missing-keypair-import-private-key + :options options/transparent-screen-options + :component import-private-key/view} + {:name :screen/settings.network-settings :options options/transparent-modal-screen-options :component network-settings/view} diff --git a/src/status_im/subs/wallet/wallet.cljs b/src/status_im/subs/wallet/wallet.cljs index de15c26f9c..dc16566c77 100644 --- a/src/status_im/subs/wallet/wallet.cljs +++ b/src/status_im/subs/wallet/wallet.cljs @@ -244,6 +244,7 @@ :address address} :networks networks :state :default + :blur? true :action :none}))))) (defn- format-settings-missing-keypair-accounts @@ -257,14 +258,13 @@ (rf/reg-sub :wallet/settings-keypairs-accounts :<- [:wallet/keypairs] - :<- [:wallet/accounts] - (fn [[keypairs accounts] [_ format-options]] - (let [grouped-accounts (->> accounts - (map #(select-keys % [:operable :key-uid])) - (group-by :operable)) - operable-key-pair-ids (->> (map :key-uid (:fully grouped-accounts)) + (fn [keypairs [_ format-options]] + (let [grouped-keypairs (group-by :lowest-operability keypairs) + operable-key-pair-ids (->> (concat (:fully grouped-keypairs) + (:partially grouped-keypairs)) + (map :key-uid) (into #{})) - missing-key-pair-ids (->> (map :key-uid (:no grouped-accounts)) + missing-key-pair-ids (->> (map :key-uid (:no grouped-keypairs)) (into #{}))] {:operable (->> keypairs (filter #(contains? operable-key-pair-ids (:key-uid %))) diff --git a/src/status_im/subs/wallet/wallet_test.cljs b/src/status_im/subs/wallet/wallet_test.cljs index c6ba5b66bd..dbe2c58b7b 100644 --- a/src/status_im/subs/wallet/wallet_test.cljs +++ b/src/status_im/subs/wallet/wallet_test.cljs @@ -651,16 +651,18 @@ :removed false}) (def default-keypair-accounts - {:key-uid "abc" - :name "My Profile" - :type "profile" - :accounts []}) + {:key-uid "abc" + :name "My Profile" + :type "profile" + :lowest-operability :fully + :accounts []}) (def seed-phrase-keypair-accounts - {:key-uid "def" - :name "My Key Pair" - :type "seed" - :accounts []}) + {:key-uid "def" + :name "My Key Pair" + :type "seed" + :lowest-operability :no + :accounts []}) (h/deftest-sub :wallet/settings-keypairs-accounts [sub-name] diff --git a/src/utils/re_frame.cljs b/src/utils/re_frame.cljs index 26352af53c..ded7d3c593 100644 --- a/src/utils/re_frame.cljs +++ b/src/utils/re_frame.cljs @@ -88,3 +88,45 @@ (def dispatch-sync re-frame/dispatch-sync) (def reg-event-fx re-frame/reg-event-fx) + +(defn call-continuation + "Choose how to call a continuation for a Re-Frame event or effect. + + When defining an event or effect, we can receive `on-success` and `on-error` + parameters for continuing the logic depending on if it succeeded or failed. + When we attempt to continue the logic, we can choose to either dispatch a Re-Frame event, + or we can call a callback function. + + Code example: + + (rf/reg-event-fx :my-event + (fn [_ [arg on-success on-error]] + {:fx [[:my-effect [arg on-success on-error]]]})) + + (rf/reg-event-fx :my-event-success + (fn [db [result]] + {:db (assoc db :my-event-result result)})) + + (rf/reg-event-fx :my-event-error + (fn [db [error]] + {:db (assoc db :my-event-error error)})) + + (rf/reg-fx :my-effect + (fn [[arg on-success on-error]] + (-> (my-effect-impl arg) + (promesa/then (partial call-continuation on-success)) + (promesa/catch (partial call-continuation on-error))))) + + (rf/dispatch [:my-event + :arg + [:my-event-success] + (fn [error] + (log/error error) + (rf/dispatch [:my-event-error error]))])" + [continuation & args] + (cond + (vector? continuation) (dispatch (into continuation args)) + (fn? continuation) (apply continuation args) + :else (throw (ex-info + (str "Unsupported continuation: " (type->str continuation)) + {:hint "Make sure to pass a vector or function"})))) diff --git a/translations/en.json b/translations/en.json index f58f78462e..669c11c93a 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1021,6 +1021,7 @@ "enter-recovery-phrase": "Enter recovery phrase", "import-key-pair": "Import key pair", "import-by-entering-recovery-phrase": "Import by entering recovery phrase", + "import-by-entering-private-key": "Import by entering private key", "use-recovery-phrase": "Use recovery phrase", "use-recovery-phrase-subtitle": "If you already have an Ethereum address", "use-keycard": "Use Keycard", @@ -2656,6 +2657,8 @@ "amount-missing-keypairs": "{{amount} missing key pairs", "import-private-key-info": "New addresses cannot be derived from an account imported from a private key. Import using a seed phrase if you wish to derive addresses.", "invalid-private-key": "It’s not a valid private key", + "correct-private-key": "Correct private key", + "incorrect-private-key": "This is not the private key for {{name}}", "private-key-public-address": "Public address of private key", "this-account-has-no-activity": "This account has no activity", "this-address-has-activity": "This address has activity",