[#20035] feat: import missing key pair by scanning QR code (#20144)

This commit is contained in:
Mohsen 2024-05-30 22:07:33 +03:00 committed by GitHub
parent dafab10582
commit e04c7f449f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 371 additions and 71 deletions

View File

@ -88,4 +88,16 @@ class NetworkManager(private val reactContext: ReactApplicationContext) : ReactC
callback)
}
@ReactMethod
fun inputConnectionStringForImportingKeypairsKeystores(connectionString: String, configJSON: String, callback: Callback) {
val jsonConfig = JSONObject(configJSON)
val receiverConfig = jsonConfig.getJSONObject("receiverConfig")
val keyStorePath = utils.pathCombine(utils.getNoBackupDirectory(), "/keystore")
receiverConfig.put("keystorePath", keyStorePath)
utils.executeRunnableStatusGoMethod(
{ Statusgo.inputConnectionStringForImportingKeypairsKeystores(connectionString, jsonConfig.toString()) },
callback
)
}
}

View File

@ -124,4 +124,23 @@ RCT_EXPORT_METHOD(getConnectionStringForExportingKeypairsKeystores:(NSString *)c
callback(@[result]);
}
RCT_EXPORT_METHOD(inputConnectionStringForImportingKeypairsKeystores:(NSString *)cs
configJSON:(NSString *)configJSON
callback:(RCTResponseSenderBlock)callback) {
NSData *configData = [configJSON dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSMutableDictionary *configDict = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:&error];
NSMutableDictionary *receiverConfig = configDict[@"receiverConfig"];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *rootUrl =[[fileManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *multiaccountKeystoreDir = [rootUrl URLByAppendingPathComponent:@"keystore"];
NSString *keystoreDir = multiaccountKeystoreDir.path;
[receiverConfig setValue:keystoreDir forKey:@"keystorePath"];
NSString *modifiedConfigJSON = [Utils jsonStringWithPrettyPrint:NO fromDictionary:configDict];
NSString *result = StatusgoInputConnectionStringForImportingKeypairsKeystores(cs, modifiedConfigJSON);
callback(@[result]);
}
@end

View File

@ -604,8 +604,27 @@
(defn get-connection-string-for-exporting-keypairs-keystores
"Generates connection string form status-go for the purpose of exporting keypairs and keystores on sender side"
[config-json callback]
(log/info "[native-module] Fetching Export Keypairs Connection String"
{:fn :get-connection-string-for-exporting-keypairs-keystores
:config-json config-json})
(.getConnectionStringForExportingKeypairsKeystores ^js (network) config-json callback))
([config-json]
(native-utils/promisify-native-module-call get-connection-string-for-exporting-keypairs-keystores
config-json))
([config-json callback]
(log/info "[native-module] Fetching Export Keypairs Connection String"
{:fn :get-connection-string-for-exporting-keypairs-keystores
:config-json config-json})
(.getConnectionStringForExportingKeypairsKeystores ^js (network) config-json callback)))
(defn input-connection-string-for-importing-keypairs-keystores
"Provides connection string to status-go for the purpose of importing keypairs and keystores on the receiver side"
([connection-string config-json]
(native-utils/promisify-native-module-call input-connection-string-for-importing-keypairs-keystores
connection-string
config-json))
([connection-string config-json callback]
(log/info "[native-module] Sending Import Keypairs Connection String"
{:fn :input-connection-string-for-importing-keypairs-keystores
:config-json config-json
:connection-string connection-string})
(.inputConnectionStringForImportingKeypairsKeystores ^js (network)
connection-string
config-json
callback)))

View File

@ -13,7 +13,13 @@
{:top 1})
(def title-info-container
{:padding-left 8})
{:padding-left 8
:flex 1})
(def title-row
{:display :flex
:flex-direction :row
:justify-content :space-between})
(def title-container
{:align-items :flex-start

View File

@ -1,5 +1,6 @@
(ns quo.components.wallet.missing-keypairs.view
(:require
[quo.components.buttons.button.view :as button]
[quo.components.icon :as icon]
[quo.components.list-items.missing-keypair.view :as missing-keypair]
[quo.components.markdown.text :as text]
@ -10,7 +11,7 @@
[utils.i18n :as i18n]))
(defn title-view
[{:keys [keypairs blur?]}]
[{:keys [keypairs blur? on-import-press]}]
(let [theme (quo.theme/use-theme)]
[rn/view
{:accessibility-label :title
@ -22,11 +23,18 @@
:color colors/warning-60}]]
[rn/view
{:style style/title-info-container}
[text/text
{:weight :medium
:style {:color colors/warning-60}}
(i18n/label :t/amount-missing-keypairs
{:amount (str (count keypairs))})]
[rn/view {:style style/title-row}
[text/text
{:weight :medium
:style {:color colors/warning-60}}
(i18n/label :t/amount-missing-keypairs
{:amount (str (count keypairs))})]
[button/button
{:type :outline
:background :blur
:size 24
:on-press on-import-press}
(i18n/label :t/import)]]
[text/text
{:size :paragraph-2
:style (style/subtitle blur? theme)}

View File

@ -0,0 +1,47 @@
(ns status-im.contexts.settings.wallet.data-store)
(defn extract-keypair-name
[db key-uids-set]
(when (= (count key-uids-set) 1)
(let [key-uid (first key-uids-set)
keypairs (get-in db [:wallet :keypairs])]
(->> (filter #(= (:key-uid %) key-uid) keypairs)
first
:name))))
(defn update-keypair
[keypairs key-uid update-fn]
(mapcat (fn [keypair]
(if (= (keypair :key-uid) key-uid)
(if-let [updated (update-fn keypair)]
[updated]
[])
[keypair]))
keypairs))
(defn make-accounts-fully-operable
[accounts key-uids-set]
(reduce-kv
(fn [acc k account]
(if (and (contains? key-uids-set (:key-uid account))
(= (keyword (:operable account)) :no))
(assoc acc k (assoc account :operable :fully))
(assoc acc k account)))
{}
accounts))
(defn- make-keypairs-accounts-fully-operable
[accounts]
(map (fn [account]
(assoc account :operable :fully))
accounts))
(defn make-keypairs-fully-operable
[keypairs key-uids-set]
(map (fn [keypair]
(if (contains? key-uids-set (:key-uid keypair))
(update keypair
:accounts
make-keypairs-accounts-fully-operable)
keypair))
keypairs))

View File

@ -0,0 +1,38 @@
(ns status-im.contexts.settings.wallet.effects
(:require [native-module.core :as native-module]
[promesa.core :as promesa]
[status-im.contexts.syncing.utils :as sync-utils]
[utils.re-frame :as rf]
[utils.security.core :as security]
[utils.transforms :as transforms]))
(rf/reg-fx :effects.connection-string/export-keypair
(fn [{:keys [key-uid sha3-pwd keypair-key-uid on-success on-fail]}]
(let [config-map (transforms/clj->json {:senderConfig {:loggedInKeyUid key-uid
:keystorePath ""
:keypairsToExport [keypair-key-uid]
:password (security/safe-unmask-data
sha3-pwd)}
:serverConfig {:timeout 0}})]
(-> (native-module/get-connection-string-for-exporting-keypairs-keystores
config-map)
(promesa/then (fn [response]
(if (sync-utils/valid-connection-string? response)
(on-success response)
(on-fail (js/Error.
"generic-error: failed to get connection string")))))
(promesa/catch on-fail)))))
(rf/reg-fx :effects.connection-string/import-keypair
(fn [{:keys [key-uid sha3-pwd keypairs-key-uids connection-string on-success on-fail]}]
(let [config-map (transforms/clj->json {:receiverConfig
{:loggedInKeyUid key-uid
:keystorePath ""
:password (security/safe-unmask-data
sha3-pwd)
:keypairsToImport keypairs-key-uids}})]
(-> (native-module/input-connection-string-for-importing-keypairs-keystores
connection-string
config-map)
(promesa/then #(on-success keypairs-key-uids))
(promesa/catch on-fail)))))

View File

@ -1,29 +1,16 @@
(ns status-im.contexts.settings.wallet.events
(:require
[native-module.core :as native-module]
[status-im.contexts.syncing.utils :as sync-utils]
[status-im.contexts.settings.wallet.data-store :as data-store]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[utils.security.core :as security]
[utils.transforms :as transforms]))
(defn- update-keypair
[keypairs key-uid update-fn]
(mapcat (fn [keypair]
(if (= (keypair :key-uid) key-uid)
(if-let [updated (update-fn keypair)]
[updated]
[])
[keypair]))
keypairs))
[utils.re-frame :as rf]))
(rf/reg-event-fx
:wallet/rename-keypair-success
(fn [{:keys [db]} [key-uid name]]
{:db (update-in db
[:wallet :keypairs]
#(update-keypair % key-uid (fn [keypair] (assoc keypair :name name))))
#(data-store/update-keypair % key-uid (fn [keypair] (assoc keypair :name name))))
:fx [[:dispatch [:navigate-back]]
[:dispatch
[:toasts/upsert
@ -41,31 +28,28 @@
(rf/reg-event-fx :wallet/rename-keypair rename-keypair)
(defn get-key-pair-export-connection
(defn get-keypair-export-connection
[{:keys [db]} [{:keys [sha3-pwd keypair-key-uid callback]}]]
(let [key-uid (get-in db [:profile/profile :key-uid])
config-map (transforms/clj->json {:senderConfig {:loggedInKeyUid key-uid
:keystorePath ""
:keypairsToExport [keypair-key-uid]
:password (security/safe-unmask-data
sha3-pwd)}
:serverConfig {:timeout 0}})
handle-connection (fn [response]
(when (sync-utils/valid-connection-string? response)
(callback response)
(rf/dispatch [:hide-bottom-sheet])))]
(native-module/get-connection-string-for-exporting-keypairs-keystores
config-map
handle-connection)))
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:effects.connection-string/export-keypair
{:key-uid key-uid
:sha3-pwd sha3-pwd
:keypair-key-uid keypair-key-uid
:on-success (fn [connect-string]
(callback connect-string)
(rf/dispatch [:hide-bottom-sheet]))
:on-fail (fn [error]
(rf/dispatch [:toasts/upsert
{:type :negative
:text (.-message error)}]))}]]}))
(rf/reg-event-fx :wallet/get-key-pair-export-connection get-key-pair-export-connection)
(rf/reg-event-fx :wallet/get-keypair-export-connection get-keypair-export-connection)
(rf/reg-event-fx
:wallet/remove-keypair-success
(rf/reg-event-fx :wallet/remove-keypair-success
(fn [{:keys [db]} [key-uid]]
{:db (update-in db
[:wallet :keypairs]
#(update-keypair % key-uid (fn [_] nil)))
#(data-store/update-keypair % key-uid (fn [_] nil)))
:fx [[:dispatch [:hide-bottom-sheet]]
[:dispatch
[:toasts/upsert
@ -79,6 +63,57 @@
[{:method "accounts_deleteKeypair"
:params [key-uid]
:on-success [:wallet/remove-keypair-success key-uid]
:on-error #(log/info "failed to remove keypair " %)}]]]})
:on-error #(log/error "failed to remove keypair " {:error %})}]]]})
(rf/reg-event-fx :wallet/remove-keypair remove-keypair)
(defn make-keypairs-accounts-fully-operable
[{:keys [db]} [key-uids-to-update]]
(let [key-uids-set (set key-uids-to-update)
keypair-name (data-store/extract-keypair-name db key-uids-set)]
{:db (-> db
(update-in [:wallet :accounts] #(data-store/make-accounts-fully-operable % key-uids-set))
(update-in [:wallet :keypairs] #(data-store/make-keypairs-fully-operable % key-uids-set)))
:fx [[:dispatch
[:toasts/upsert
{:type :positive
:theme :dark
:text (if (= (count key-uids-to-update) 1)
(i18n/label :t/key-pair-imported-successfully {:name keypair-name})
(i18n/label :t/key-pairs-successfully-imported
{:count (count key-uids-to-update)}))}]]]}))
(rf/reg-event-fx :wallet/make-keypairs-accounts-fully-operable make-keypairs-accounts-fully-operable)
(defn connection-string-for-import-keypair
[{:keys [db]} [{:keys [sha3-pwd keypairs-key-uids connection-string]}]]
(let [key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:effects.connection-string/import-keypair
{:key-uid key-uid
:sha3-pwd sha3-pwd
:keypairs-key-uids keypairs-key-uids
:connection-string connection-string
:on-success #(rf/dispatch [:wallet/make-keypairs-accounts-fully-operable %])
:on-fail #(rf/dispatch [:toasts/upsert
{:type :negative
:theme :dark
:text %}])}]]}))
(rf/reg-event-fx :wallet/connection-string-for-import-keypair connection-string-for-import-keypair)
(defn success-keypair-qr-scan
[_ [connection-string keypairs-key-uids]]
{:fx [[:dispatch
[:standard-auth/authorize-with-password
{:blur? true
:theme :dark
:auth-button-label (i18n/label :t/confirm)
:on-auth-success (fn [password]
(rf/dispatch [:hide-bottom-sheet])
(rf/dispatch
[:wallet/connection-string-for-import-keypair
{:connection-string connection-string
:keypairs-key-uids keypairs-key-uids
:sha3-pwd password}]))}]]]})
(rf/reg-event-fx :wallet/success-keypair-qr-scan success-keypair-qr-scan)

View File

@ -1,20 +1,97 @@
(ns status-im.contexts.settings.wallet.events-test
(:require
[cljs.test :refer-macros [deftest is]]
[cljs.test :refer-macros [deftest is testing]]
matcher-combinators.test
[status-im.contexts.settings.wallet.events :as sut]))
(def key-uid "0xfef454bb492ee4677594f8e05921c84f336fa811deb99b8d922477cc87a38b98")
(def mock-key-uid "key-1")
(defn mock-db
[keypairs accounts]
{:wallet {:keypairs keypairs
:accounts accounts}
:profile/profile {:key-uid "test-key-uid"}})
(deftest rename-keypair-test
(let [new-keypair-name "key pair new"
cofx {:db {}}
expected {:fx [[:json-rpc/call
[{:method "accounts_updateKeypairName"
:params [key-uid new-keypair-name]
:on-success [:wallet/rename-keypair-success key-uid new-keypair-name]
:on-error fn?}]]]}]
(is (match? expected
(sut/rename-keypair cofx
[{:key-uid key-uid
:keypair-name new-keypair-name}])))))
cofx {:db {}}]
(testing "rename-keypair"
(let [expected {:fx [[:json-rpc/call
[{:method "accounts_updateKeypairName"
:params [mock-key-uid new-keypair-name]
:on-success [:wallet/rename-keypair-success mock-key-uid
new-keypair-name]
:on-error fn?}]]]}]
(is (match? expected
(sut/rename-keypair cofx
[{:key-uid mock-key-uid
:keypair-name new-keypair-name}])))))))
(deftest get-keypair-export-connection-test
(let [cofx {:db (mock-db [] {})}
sha3-pwd "test-password"
user-key-uid "test-key-uid"
callback (fn [connect-string] (println "callback" connect-string))]
(testing "get-keypair-export-connection"
(let [expected {:fx [[:effects.connection-string/export-keypair
{:key-uid user-key-uid
:sha3-pwd sha3-pwd
:keypair-key-uid mock-key-uid
:on-success fn?
:on-fail fn?}]]}]
(is (match? expected
(sut/get-keypair-export-connection
cofx
[{:sha3-pwd sha3-pwd :keypair-key-uid mock-key-uid :callback callback}])))))))
(deftest remove-keypair-test
(let [cofx {:db {}}]
(testing "remove-keypair"
(let [expected {:fx [[:json-rpc/call
[{:method "accounts_deleteKeypair"
:params [mock-key-uid]
:on-success [:wallet/remove-keypair-success mock-key-uid]
:on-error fn?}]]]}]
(is (match? expected
(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"}]}]
{"0x1" {:key-uid mock-key-uid :operable "no"}})
key-uids-to-update [mock-key-uid]]
(testing "make-keypairs-accounts-fully-operable"
(let [effects (sut/make-keypairs-accounts-fully-operable {:db db} [key-uids-to-update])
result-db (:db effects)
updated-keypair (some #(when (= (:key-uid %) mock-key-uid) %)
(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))))))
(deftest connection-string-for-import-keypair-test
(let [cofx {:db (mock-db [] {})}
sha3-pwd "test-password"
user-key-uid "test-key-uid"
connection-string "test-connection-string"]
(testing "connection-string-for-import-keypair"
(let [expected {:fx [[:effects.connection-string/import-keypair
{:key-uid user-key-uid
:sha3-pwd sha3-pwd
:keypairs-key-uids [mock-key-uid]
:connection-string connection-string
:on-success fn?
:on-fail fn?}]]}]
(is (match? expected
(sut/connection-string-for-import-keypair cofx
[{:sha3-pwd sha3-pwd
:keypairs-key-uids [mock-key-uid]
:connection-string
connection-string}])))))))
(deftest success-keypair-qr-scan-test
(let [connection-string "valid-connection-string"
keypairs-key-uids ["keypair-uid"]]
(testing "success-keypair-qr-scan"
(let [effects (sut/success-keypair-qr-scan nil [connection-string keypairs-key-uids])
fx (:fx effects)]
(is (some? fx))))))

View File

@ -28,7 +28,7 @@
cleanup-clock (rn/use-callback #(set-code nil))
on-auth-success (rn/use-callback (fn [entered-password]
(rf/dispatch
[:wallet/get-key-pair-export-connection
[:wallet/get-keypair-export-connection
{:sha3-pwd entered-password
:keypair-key-uid key-uid
:callback validate-and-set-code}]))

View File

@ -0,0 +1,23 @@
(ns status-im.contexts.settings.wallet.keypairs-and-accounts.scan-qr.view
(:require
[react-native.core :as rn]
[status-im.common.scan-qr-code.view :as scan-qr-code]
[status-im.contexts.communities.events]
[status-im.contexts.syncing.utils :as sync-utils]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn view
[]
(let [keypairs-key-uids (rf/sub [:get-screen-params])
on-success-scan (rn/use-callback (fn [scanned-text]
(rf/dispatch [:wallet/success-keypair-qr-scan scanned-text
keypairs-key-uids])
[keypairs-key-uids]))]
[scan-qr-code/view
{:title (i18n/label :t/scan-key-pairs-qr-code)
:subtitle (i18n/label :t/find-it-in-setting)
:share-button? false
:validate-fn sync-utils/valid-connection-string?
:error-message (i18n/label :t/invalid-qr)
:on-success-scan on-success-scan}]))

View File

@ -93,7 +93,11 @@
profile-picture (rf/sub [:profile/image])
customization-color (rf/sub [:profile/customization-color])
{missing-keypairs :missing
operable-keypairs :operable} (rf/sub [:wallet/settings-keypairs-accounts])]
operable-keypairs :operable} (rf/sub [:wallet/settings-keypairs-accounts])
on-import-press (rn/use-callback #(rf/dispatch [:open-modal
:screen/settings.scan-keypair-qr
(map :key-uid missing-keypairs)])
[missing-keypairs])]
[quo/overlay
{:type :shell
:container-style (style/page-wrapper (:top insets))}
@ -112,6 +116,7 @@
[quo/missing-keypairs
{:blur? true
:keypairs missing-keypairs
:on-import-press on-import-press
:container-style style/missing-keypairs-container-style
:on-options-press on-missing-keypair-options-press}])
[rn/flat-list

View File

@ -41,19 +41,12 @@
:VerifyTransactionChainID config/verify-transaction-chain-id}}
log-config)))
(defn- extract-error
[json-str]
(-> json-str
transforms/json->clj
(get :error "")
not-empty))
(defn- input-connection-string-callback
[res]
(log/info "[local-pairing] input-connection-string-for-bootstrapping callback"
{:response res
:event :syncing/input-connection-string-for-bootstrapping})
(let [error (when (extract-error res)
(let [error (when (sync-utils/extract-error res)
(str "generic-error: " res))]
(when (some? error)
(rf/dispatch [:toasts/upsert

View File

@ -1,7 +1,8 @@
(ns status-im.contexts.syncing.utils
(:require
[clojure.string :as string]
[status-im.constants :as constants]))
[status-im.constants :as constants]
[utils.transforms :as transforms]))
(defn valid-connection-string?
[connection-string]
@ -9,3 +10,10 @@
(string/starts-with?
connection-string
constants/local-pairing-connection-string-identifier)))
(defn extract-error
[json-str]
(-> json-str
transforms/json->clj
(get :error "")
not-empty))

View File

@ -4,6 +4,7 @@
[clojure.string :as string]
[react-native.platform :as platform]
[status-im.constants :as constants]
[status-im.contexts.settings.wallet.effects]
[status-im.contexts.settings.wallet.events]
[status-im.contexts.wallet.common.utils.networks :as network-utils]
[status-im.contexts.wallet.data-store :as data-store]

View File

@ -60,6 +60,7 @@
[status-im.contexts.settings.wallet.keypairs-and-accounts.encrypted-qr.view :as
encrypted-key-pair-qr]
[status-im.contexts.settings.wallet.keypairs-and-accounts.rename.view :as keypair-rename]
[status-im.contexts.settings.wallet.keypairs-and-accounts.scan-qr.view :as scan-keypair-qr]
[status-im.contexts.settings.wallet.keypairs-and-accounts.view :as keypairs-and-accounts]
[status-im.contexts.settings.wallet.network-settings.view :as network-settings]
[status-im.contexts.settings.wallet.saved-addresses.view :as saved-addresses-settings]
@ -524,6 +525,10 @@
:options options/transparent-modal-screen-options
:component keypairs-and-accounts/view}
{:name :screen/settings.scan-keypair-qr
:options options/transparent-modal-screen-options
:component scan-keypair-qr/view}
{:name :screen/settings.network-settings
:options options/transparent-modal-screen-options
:component network-settings/view}

View File

@ -1292,6 +1292,7 @@
"scan-qr": "Scan QR",
"scan-qr-code": "Scan QR code",
"scan-with-status-app": "Scan with the Status app on another device",
"scan-key-pairs-qr-code": "Scan key pairs QR code",
"invalid-qr": "Oops! This QR doesnt work with Status",
"search": "Search",
"search-discover-communities": "Search communities or categories",
@ -2213,6 +2214,7 @@
"create-new-profile": "Create new profile",
"add-existing-status-profile": "Add existing Status profile",
"find-sync-code": "Find sync code",
"find-it-in-setting": "Find it in Settings on your other synced device",
"sign-in-by-syncing": "Sign in by syncing",
"synchronise-your-data-across-your-devices": "Synchronise your data across your devices",
"scan-sync-qr-code": "Scan QR code",
@ -2452,6 +2454,8 @@
"no-other-accounts": "No other accounts",
"here-is-a-cat-in-a-box-instead": "Heres a cat in a box instead",
"accounts-count": "{{count}} accounts",
"key-pairs-successfully-imported": "{{count}} key pairs successfully imported",
"key-pair-imported-successfully": "{{name}} key pair imported successfully",
"n-m-people": "{{n}}/{{m}} people",
"enter-eth": "Enter any ETH address or ENS name.",
"eth-or-ens": "ETH address or ENS name.",