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
This commit is contained in:
Sean Hagstrom 2024-06-11 13:05:10 +01:00 committed by GitHub
parent f226f0db18
commit 6a7f04a5a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 401 additions and 67 deletions

View File

@ -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 { companion object {
private const val TAG = "AccountManager" private const val TAG = "AccountManager"
private const val gethLogFileName = "geth.log" private const val gethLogFileName = "geth.log"

View File

@ -233,4 +233,12 @@ RCT_EXPORT_METHOD(createAccountFromMnemonicAndDeriveAccountsForPaths:(NSString *
callback(@[result]); 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 @end

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

View File

@ -636,3 +636,14 @@
connection-string connection-string
config-json config-json
callback))) 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)))

View File

@ -16,7 +16,7 @@
:icon 12}}) :icon 12}})
(defn icon-avatar (defn icon-avatar
[{:keys [size icon color opacity border?] [{:keys [size icon color opacity border? blur?]
:or {opacity 20 :or {opacity 20
size :size-32}}] size :size-32}}]
(let [theme (quo.theme/use-theme) (let [theme (quo.theme/use-theme)
@ -29,7 +29,9 @@
:height component-size :height component-size
:border-radius component-size :border-radius component-size
:border-width (when border? 1) :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 :background-color circle-color
:justify-content :center :justify-content :center
:align-items :center}} :align-items :center}}

View File

@ -11,7 +11,7 @@
[schema.core :as schema])) [schema.core :as schema]))
(defn- internal-view (defn- internal-view
[{{:keys [accounts name]} :keypair [{{:keys [accounts name type]} :keypair
:keys [keypair blur? on-options-press]}] :keys [keypair blur? on-options-press]}]
(let [theme (quo.theme/use-theme) (let [theme (quo.theme/use-theme)
on-keypair-options-press (rn/use-callback on-keypair-options-press (rn/use-callback
@ -22,15 +22,15 @@
{:style (style/container {:theme theme {:style (style/container {:theme theme
:blur? blur?}) :blur? blur?})
:accessibility-label :missing-keypair-item} :accessibility-label :missing-keypair-item}
[rn/view
{:style (style/icon-container {:theme theme
:blur? blur?})
:accessibility-label :icon}
[icon-avatar/icon-avatar [icon-avatar/icon-avatar
{:size :size-32 {:size :size-32
:icon :i/seed :icon (case type
:seed :i/seed
:key :i/password
nil)
:color :neutral :color :neutral
:border? false}]] :blur? true
:border? true}]
[rn/view [rn/view
{:style style/name-container {:style style/name-container
:accessibility-label :name} :accessibility-label :name}

View File

@ -19,7 +19,8 @@
(i18n/label :t/keypair-title {:name first-name}))) (i18n/label :t/keypair-title {:name first-name})))
(defn avatar (defn avatar
[{{:keys [full-name]} :details [{:keys [blur?]
{:keys [full-name]} :details
avatar-type :type avatar-type :type
customization-color :customization-color customization-color :customization-color
profile-picture :profile-picture}] profile-picture :profile-picture}]
@ -34,6 +35,7 @@
[icon-avatar/icon-avatar [icon-avatar/icon-avatar
{:size :size-32 {:size :size-32
:icon :i/seed :icon :i/seed
:blur? blur?
:border? true}])) :border? true}]))
(defn title-view (defn title-view

View File

@ -228,7 +228,8 @@
(def regx-string-public-key "0x04[0-9a-f]{128}") (def regx-string-public-key "0x04[0-9a-f]{128}")
(def regx-compressed-key (re-pattern (str "^" regx-string-compressed-key "$"))) (def regx-compressed-key (re-pattern (str "^" regx-string-compressed-key "$")))
(def regx-public-key (re-pattern (str "^" regx-string-public-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 (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])+$") #"^((?:[\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 #"\*[^*]+\*") (def regx-bold #"\*[^*]+\*")

View File

@ -40,8 +40,8 @@
[keypairs key-uids-set] [keypairs key-uids-set]
(map (fn [keypair] (map (fn [keypair]
(if (contains? key-uids-set (:key-uid keypair)) (if (contains? key-uids-set (:key-uid keypair))
(update keypair (-> keypair
:accounts (update :accounts make-keypairs-accounts-fully-operable)
make-keypairs-accounts-fully-operable) (assoc :lowest-operability :fully))
keypair)) keypair))
keypairs)) keypairs))

View File

@ -137,6 +137,36 @@
(rf/reg-event-fx :wallet/make-seed-phrase-keypair-fully-operable make-seed-phrase-keypair-fully-operable) (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 (defn import-keypair-by-seed-phrase
[_ [{:keys [keypair-key-uid seed-phrase password on-success on-error]}]] [_ [{:keys [keypair-key-uid seed-phrase password on-success on-error]}]]
{:fx [[:import-keypair-by-seed-phrase {:fx [[:import-keypair-by-seed-phrase
@ -146,14 +176,10 @@
:on-success (fn [] :on-success (fn []
(rf/dispatch [:wallet/make-keypairs-accounts-fully-operable (rf/dispatch [:wallet/make-keypairs-accounts-fully-operable
#{keypair-key-uid}]) #{keypair-key-uid}])
(cond (rf/call-continuation on-success))
(vector? on-success) (rf/dispatch (conj on-success))
(fn? on-success) (on-success)))
:on-error (fn [error] :on-error (fn [error]
(rf/dispatch [:wallet/import-keypair-by-seed-phrase-failed error]) (rf/dispatch [:wallet/import-keypair-by-seed-phrase-failed error])
(cond (rf/call-continuation on-error error))}]]})
(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) (rf/reg-event-fx :wallet/import-keypair-by-seed-phrase import-keypair-by-seed-phrase)
@ -170,3 +196,28 @@
:text (:error error-data)}]]]}))) :text (:error error-data)}]]]})))
(rf/reg-event-fx :wallet/import-keypair-by-seed-phrase-failed import-keypair-by-seed-phrase-failed) (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)

View File

@ -58,6 +58,7 @@
(deftest make-keypairs-accounts-fully-operable-test (deftest make-keypairs-accounts-fully-operable-test
(let [db (mock-db [{:key-uid mock-key-uid (let [db (mock-db [{:key-uid mock-key-uid
:lowest-operability :no
:accounts [{:key-uid mock-key-uid :operable "no"}]}] :accounts [{:key-uid mock-key-uid :operable "no"}]}]
{"0x1" {:key-uid mock-key-uid :operable "no"}}) {"0x1" {:key-uid mock-key-uid :operable "no"}})
key-uids-to-update [mock-key-uid]] key-uids-to-update [mock-key-uid]]
@ -68,7 +69,8 @@
(get-in result-db [:wallet :keypairs])) (get-in result-db [:wallet :keypairs]))
updated-account (get-in result-db [:wallet :accounts "0x1"])] updated-account (get-in result-db [:wallet :accounts "0x1"])]
(is (= (keyword (-> updated-keypair :accounts first :operable)) :fully)) (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 (deftest connection-string-for-import-keypair-test
(let [cofx {:db (mock-db [] {})} (let [cofx {:db (mock-db [] {})}

View File

@ -28,6 +28,11 @@
[keypair]) [keypair])
on-import-seed-phrase (rn/use-callback on-import-seed-phrase (rn/use-callback
#(rf/dispatch [:open-modal :screen/settings.import-seed-phrase keypair]) #(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])] [keypair])]
[:<> [:<>
[quo/drawer-top drawer-props] [quo/drawer-top drawer-props]
@ -48,7 +53,11 @@
:seed {:icon :i/seed :seed {:icon :i/seed
:accessibility-label :import-seed-phrase :accessibility-label :import-seed-phrase
:label (i18n/label :t/import-by-entering-recovery-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)) nil))
{:icon :i/edit {:icon :i/edit
:accessibility-label :rename-key-pair :accessibility-label :rename-key-pair

View File

@ -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})

View File

@ -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)])]]]))

View File

@ -1,5 +1,6 @@
(ns status-im.contexts.settings.wallet.keypairs-and-accounts.view (ns status-im.contexts.settings.wallet.keypairs-and-accounts.view
(:require [quo.core :as quo] (:require [quo.core :as quo]
[quo.foundations.colors :as colors]
[quo.theme] [quo.theme]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.safe-area :as safe-area] [react-native.safe-area :as safe-area]
@ -19,7 +20,10 @@
{:content (fn [] [actions/view {:content (fn [] [actions/view
{:drawer-props drawer-props {:drawer-props drawer-props
:keypair keypair}]) :keypair keypair}])
:theme (:theme drawer-props)}]))
:blur-background colors/bottom-sheet-background-blur
:theme (:theme drawer-props)
:shell? true}]))
(defn options-drawer-props (defn options-drawer-props
[{{:keys [name]} :keypair [{{:keys [name]} :keypair
@ -80,6 +84,8 @@
[_event keypair-data] [_event keypair-data]
(rf/dispatch [:show-bottom-sheet (rf/dispatch [:show-bottom-sheet
{:theme :dark {:theme :dark
:shell? true
:blur-background colors/bottom-sheet-background-blur
:content (fn [] [actions/view :content (fn [] [actions/view
{:keypair keypair-data {:keypair keypair-data
:drawer-props (options-drawer-props :drawer-props (options-drawer-props

View File

@ -23,7 +23,7 @@
:add-divider? true :add-divider? true
:on-press #(rf/dispatch [:navigate-to :screen/wallet.enter-seed-phrase :on-press #(rf/dispatch [:navigate-to :screen/wallet.enter-seed-phrase
{:recovering-keypair? true}])} {:recovering-keypair? true}])}
(when (ff/enabled? ::wallet.import-private-key) (when (ff/enabled? ::ff/wallet.import-private-key)
{:icon :i/key {:icon :i/key
:accessibility-label :import-private-key :accessibility-label :import-private-key
:label (i18n/label :t/import-private-key) :label (i18n/label :t/import-private-key)

View File

@ -3,4 +3,7 @@
(defn ens-name? [s] (boolean (re-find constants/regx-ens s))) (defn ens-name? [s] (boolean (re-find constants/regx-ens s)))
(defn eth-address? [s] (re-find constants/regx-multichain-address 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)))

View File

@ -3,9 +3,9 @@
[clojure.string :as string] [clojure.string :as string]
[native-module.core :as native-module] [native-module.core :as native-module]
[promesa.core :as promesa] [promesa.core :as promesa]
[re-frame.core :as rf]
[status-im.common.json-rpc.events :as json-rpc] [status-im.common.json-rpc.events :as json-rpc]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[utils.re-frame :as rf]
[utils.security.core :as security] [utils.security.core :as security]
[utils.transforms :as transforms])) [utils.transforms :as transforms]))
@ -46,6 +46,27 @@
(when (and error (fn? on-error)) (when (and error (fn? on-error))
(on-error 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 (defn make-seed-phrase-fully-operable
[mnemonic password] [mnemonic password]
(promesa/create (promesa/create
@ -80,11 +101,24 @@
:import-keypair-by-seed-phrase :import-keypair-by-seed-phrase
(fn [{:keys [keypair-key-uid seed-phrase password on-success on-error]}] (fn [{:keys [keypair-key-uid seed-phrase password on-success on-error]}]
(-> (import-keypair-by-seed-phrase keypair-key-uid seed-phrase password) (-> (import-keypair-by-seed-phrase keypair-key-uid seed-phrase password)
(promesa/then (fn [_result] (promesa/then (partial rf/call-continuation on-success))
(cond (promesa/catch (partial rf/call-continuation on-error)))))
(vector? on-success) (rf/dispatch on-success)
(fn? on-success) (on-success)))) (defn verify-private-key-for-keypair
(promesa/catch (fn [error] [keypair-key-uid private-key]
(cond (-> (create-account-from-private-key private-key)
(vector? on-error) (rf/dispatch (conj on-error error)) (promesa/then
(fn? on-error) (on-error error))))))) (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)))))

View File

@ -59,6 +59,8 @@
[status-im.contexts.profile.settings.view :as settings] [status-im.contexts.profile.settings.view :as settings]
[status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.view :as [status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.view :as
encrypted-key-pair-qr] 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 [status-im.contexts.settings.wallet.keypairs-and-accounts.import-seed-phrase.view :as
import-seed-phrase] import-seed-phrase]
[status-im.contexts.settings.wallet.keypairs-and-accounts.rename.view :as keypair-rename] [status-im.contexts.settings.wallet.keypairs-and-accounts.rename.view :as keypair-rename]
@ -553,6 +555,10 @@
:options options/transparent-screen-options :options options/transparent-screen-options
:component import-seed-phrase/view} :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 {:name :screen/settings.network-settings
:options options/transparent-modal-screen-options :options options/transparent-modal-screen-options
:component network-settings/view} :component network-settings/view}

View File

@ -244,6 +244,7 @@
:address address} :address address}
:networks networks :networks networks
:state :default :state :default
:blur? true
:action :none}))))) :action :none})))))
(defn- format-settings-missing-keypair-accounts (defn- format-settings-missing-keypair-accounts
@ -257,14 +258,13 @@
(rf/reg-sub (rf/reg-sub
:wallet/settings-keypairs-accounts :wallet/settings-keypairs-accounts
:<- [:wallet/keypairs] :<- [:wallet/keypairs]
:<- [:wallet/accounts] (fn [keypairs [_ format-options]]
(fn [[keypairs accounts] [_ format-options]] (let [grouped-keypairs (group-by :lowest-operability keypairs)
(let [grouped-accounts (->> accounts operable-key-pair-ids (->> (concat (:fully grouped-keypairs)
(map #(select-keys % [:operable :key-uid])) (:partially grouped-keypairs))
(group-by :operable)) (map :key-uid)
operable-key-pair-ids (->> (map :key-uid (:fully grouped-accounts))
(into #{})) (into #{}))
missing-key-pair-ids (->> (map :key-uid (:no grouped-accounts)) missing-key-pair-ids (->> (map :key-uid (:no grouped-keypairs))
(into #{}))] (into #{}))]
{:operable (->> keypairs {:operable (->> keypairs
(filter #(contains? operable-key-pair-ids (:key-uid %))) (filter #(contains? operable-key-pair-ids (:key-uid %)))

View File

@ -654,12 +654,14 @@
{:key-uid "abc" {:key-uid "abc"
:name "My Profile" :name "My Profile"
:type "profile" :type "profile"
:lowest-operability :fully
:accounts []}) :accounts []})
(def seed-phrase-keypair-accounts (def seed-phrase-keypair-accounts
{:key-uid "def" {:key-uid "def"
:name "My Key Pair" :name "My Key Pair"
:type "seed" :type "seed"
:lowest-operability :no
:accounts []}) :accounts []})
(h/deftest-sub :wallet/settings-keypairs-accounts (h/deftest-sub :wallet/settings-keypairs-accounts

View File

@ -88,3 +88,45 @@
(def dispatch-sync re-frame/dispatch-sync) (def dispatch-sync re-frame/dispatch-sync)
(def reg-event-fx re-frame/reg-event-fx) (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"}))))

View File

@ -1021,6 +1021,7 @@
"enter-recovery-phrase": "Enter recovery phrase", "enter-recovery-phrase": "Enter recovery phrase",
"import-key-pair": "Import key pair", "import-key-pair": "Import key pair",
"import-by-entering-recovery-phrase": "Import by entering recovery phrase", "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": "Use recovery phrase",
"use-recovery-phrase-subtitle": "If you already have an Ethereum address", "use-recovery-phrase-subtitle": "If you already have an Ethereum address",
"use-keycard": "Use Keycard", "use-keycard": "Use Keycard",
@ -2656,6 +2657,8 @@
"amount-missing-keypairs": "{{amount} missing key pairs", "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.", "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": "Its not a valid private key", "invalid-private-key": "Its 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", "private-key-public-address": "Public address of private key",
"this-account-has-no-activity": "This account has no activity", "this-account-has-no-activity": "This account has no activity",
"this-address-has-activity": "This address has activity", "this-address-has-activity": "This address has activity",