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:
parent
f226f0db18
commit
6a7f04a5a8
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 758 B |
Binary file not shown.
After Width: | Height: | Size: 1011 B |
|
@ -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)))
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 #"\*[^*]+\*")
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 [] {})}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
|
@ -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)])]]]))
|
|
@ -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
|
||||
[]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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)))))
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 %)))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"}))))
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue