From 23fa2f5df335ad71f87a2e15060560a002ab1b75 Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Fri, 4 Aug 2023 14:41:57 +0200 Subject: [PATCH] feat(@desktop/syncing): make a not operable account fully operable, part 1 - handles recovered keypairs Closes the first part of #11779 --- .../main/profile_section/io_interface.nim | 7 +- .../modules/main/profile_section/module.nim | 9 +- src/app/modules/main/profile_section/view.nim | 13 +- .../wallet/accounts/module.nim | 4 + .../profile_section/wallet/accounts/view.nim | 3 + .../profile_section/wallet/controller.nim | 31 +++ .../profile_section/wallet/io_interface.nim | 26 ++- .../main/profile_section/wallet/module.nim | 79 ++++++-- .../main/profile_section/wallet/view.nim | 52 +++++ src/app/modules/shared/keypairs.nim | 118 +++++------ .../derived_address_item.nim | 0 .../derived_address_model.nim | 0 .../shared_models/keypair_account_model.nim | 5 +- .../modules/shared_models/keypair_item.nim | 4 +- .../modules/shared_models/keypair_model.nim | 6 + .../shared_modules/add_account/module.nim | 4 +- .../shared_modules/add_account/view.nim | 25 ++- .../keypair_import/controller.nim | 96 +++++++++ .../internal/import_private_key_state.nim | 12 ++ .../internal/import_seed_phrase_state.nim | 12 ++ .../keypair_import/internal/state.nim | 81 ++++++++ .../keypair_import/internal/state_factory.nim | 21 ++ .../keypair_import/internal/state_wrapper.nim | 62 ++++++ .../keypair_import/io_interface.nim | 51 +++++ .../shared_modules/keypair_import/module.nim | 188 ++++++++++++++++++ .../shared_modules/keypair_import/view.nim | 102 ++++++++++ .../wallet_account/service_account.nim | 48 +++++ .../wallet_account/signals_and_payloads.nim | 1 + src/backend/accounts.nim | 10 + .../controls/WalletKeyPairDelegate.qml | 4 + .../popups/WalletKeypairAccountMenu.qml | 9 +- .../Profile/stores/ProfileSectionStore.qml | 3 +- .../AppLayouts/Profile/stores/WalletStore.qml | 14 +- .../AppLayouts/Profile/views/WalletView.qml | 27 +++ .../Profile/views/wallet/MainView.qml | 48 ++++- ui/imports/shared/panels/EnterSeedPhrase.qml | 2 +- .../popups/addaccount/AddAccountPopup.qml | 3 +- .../addaccount/panels/DerivationPath.qml | 3 +- .../panels/WatchOnlyAddressSection.qml | 3 +- .../addaccount/states/EnterKeypairName.qml | 2 +- .../addaccount/states/EnterSeedPhraseWord.qml | 2 +- .../shared/popups/addaccount/states/Main.qml | 3 +- .../addaccount/stores/AddAccountStore.qml | 23 +-- .../panels => common}/AddressDetails.qml | 5 +- .../AddressWithAddressDetails.qml | 2 + .../shared/popups/common/BasePopupStore.qml | 26 +++ .../states => common}/EnterPrivateKey.qml | 33 +-- .../states => common}/EnterSeedPhrase.qml | 28 ++- .../keypairimport/KeypairImportPopup.qml | 127 ++++++++++++ ui/imports/shared/popups/keypairimport/qmldir | 1 + .../stores/KeypairImportStore.qml | 68 +++++++ ui/imports/utils/Constants.qml | 17 ++ 52 files changed, 1342 insertions(+), 181 deletions(-) create mode 100644 src/app/modules/main/profile_section/wallet/controller.nim create mode 100644 src/app/modules/main/profile_section/wallet/view.nim rename src/app/modules/{shared_modules/add_account => shared_models}/derived_address_item.nim (100%) rename src/app/modules/{shared_modules/add_account => shared_models}/derived_address_model.nim (100%) create mode 100644 src/app/modules/shared_modules/keypair_import/controller.nim create mode 100644 src/app/modules/shared_modules/keypair_import/internal/import_private_key_state.nim create mode 100644 src/app/modules/shared_modules/keypair_import/internal/import_seed_phrase_state.nim create mode 100644 src/app/modules/shared_modules/keypair_import/internal/state.nim create mode 100644 src/app/modules/shared_modules/keypair_import/internal/state_factory.nim create mode 100644 src/app/modules/shared_modules/keypair_import/internal/state_wrapper.nim create mode 100644 src/app/modules/shared_modules/keypair_import/io_interface.nim create mode 100644 src/app/modules/shared_modules/keypair_import/module.nim create mode 100644 src/app/modules/shared_modules/keypair_import/view.nim rename ui/imports/shared/popups/{addaccount/panels => common}/AddressDetails.qml (87%) rename ui/imports/shared/popups/{addaccount/panels => common}/AddressWithAddressDetails.qml (90%) create mode 100644 ui/imports/shared/popups/common/BasePopupStore.qml rename ui/imports/shared/popups/{addaccount/states => common}/EnterPrivateKey.qml (82%) rename ui/imports/shared/popups/{addaccount/states => common}/EnterSeedPhrase.qml (68%) create mode 100644 ui/imports/shared/popups/keypairimport/KeypairImportPopup.qml create mode 100644 ui/imports/shared/popups/keypairimport/qmldir create mode 100644 ui/imports/shared/popups/keypairimport/stores/KeypairImportStore.qml diff --git a/src/app/modules/main/profile_section/io_interface.nim b/src/app/modules/main/profile_section/io_interface.nim index ce53eab087..468a30ad56 100644 --- a/src/app/modules/main/profile_section/io_interface.nim +++ b/src/app/modules/main/profile_section/io_interface.nim @@ -98,8 +98,5 @@ method getKeycardModule*(self: AccessInterface): QVariant {.base.} = method walletModuleDidLoad*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") -method getWalletAccountsModule*(self: AccessInterface): QVariant {.base.} = - raise newException(ValueError, "No implementation available") - -method getWalletNetworksModule*(self: AccessInterface): QVariant {.base.} = - raise newException(ValueError, "No implementation available") +method getWalletModule*(self: AccessInterface): QVariant {.base.} = + raise newException(ValueError, "No implementation available") \ No newline at end of file diff --git a/src/app/modules/main/profile_section/module.nim b/src/app/modules/main/profile_section/module.nim index b4ca4b4ac8..efacb57cd8 100644 --- a/src/app/modules/main/profile_section/module.nim +++ b/src/app/modules/main/profile_section/module.nim @@ -111,7 +111,7 @@ proc newModule*(delegate: delegate_interface.AccessInterface, result.keycardModule = keycard_module.newModule(result, events, keycardService, settingsService, networkService, privacyService, accountsService, walletAccountService, keychainService) - result.walletModule = wallet_module.newModule(result, events, walletAccountService, settingsService, networkService) + result.walletModule = wallet_module.newModule(result, events, accountsService, walletAccountService, settingsService, networkService) singletonInstance.engine.setRootContextProperty("profileSectionModule", result.viewVariant) @@ -276,8 +276,5 @@ method getKeycardModule*(self: Module): QVariant = method walletModuleDidLoad*(self: Module) = self.checkIfModuleDidLoad() -method getWalletAccountsModule*(self: Module): QVariant = - return self.walletModule.getAccountsModuleAsVariant() - -method getWalletNetworksModule*(self: Module): QVariant = - return self.walletModule.getNetworksModuleAsVariant() \ No newline at end of file +method getWalletModule*(self: Module): QVariant = + return self.walletModule.getModuleAsVariant() \ No newline at end of file diff --git a/src/app/modules/main/profile_section/view.nim b/src/app/modules/main/profile_section/view.nim index 8adf6f296e..112eba7a77 100644 --- a/src/app/modules/main/profile_section/view.nim +++ b/src/app/modules/main/profile_section/view.nim @@ -81,12 +81,7 @@ QtObject: QtProperty[QVariant] keycardModule: read = getKeycardModule - proc getWalletAccountsModule(self: View): QVariant {.slot.} = - return self.delegate.getWalletAccountsModule() - QtProperty[QVariant] walletAccountsModule: - read = getWalletAccountsModule - - proc getWalletNetworksModule(self: View): QVariant {.slot.} = - return self.delegate.getWalletNetworksModule() - QtProperty[QVariant] walletNetworksModule: - read = getWalletNetworksModule + proc getWalletModule(self: View): QVariant {.slot.} = + return self.delegate.getWalletModule() + QtProperty[QVariant] walletModule: + read = getWalletModule diff --git a/src/app/modules/main/profile_section/wallet/accounts/module.nim b/src/app/modules/main/profile_section/wallet/accounts/module.nim index 42219a2298..540b216ed2 100644 --- a/src/app/modules/main/profile_section/wallet/accounts/module.nim +++ b/src/app/modules/main/profile_section/wallet/accounts/module.nim @@ -130,6 +130,10 @@ method load*(self: Module) = let areTestNetworksEnabled = self.controller.areTestNetworksEnabled() self.view.onUpdatedAccount(walletAccountToWalletAccountItem(args.account, keycardAccount, areTestNetworksEnabled)) + self.events.on(SIGNAL_KEYPAIR_OPERABILITY_CHANGED) do(e:Args): + let args = KeypairArgs(e) + self.view.onUpdatedKeypairOperability(args.keypair.keyUid, AccountFullyOperable) + self.events.on(SIGNAL_NEW_KEYCARD_SET) do(e: Args): let args = KeycardArgs(e) if not args.success: diff --git a/src/app/modules/main/profile_section/wallet/accounts/view.nim b/src/app/modules/main/profile_section/wallet/accounts/view.nim index 89c112ae59..3c406fd848 100644 --- a/src/app/modules/main/profile_section/wallet/accounts/view.nim +++ b/src/app/modules/main/profile_section/wallet/accounts/view.nim @@ -51,6 +51,9 @@ QtObject: self.accounts.onUpdatedAccount(account) self.keyPairModel.onUpdatedAccount(account.keyUid, account.address, account.name, account.colorId, account.emoji) + proc onUpdatedKeypairOperability*(self: View, keyUid, operability: string) = + self.keyPairModel.onUpdatedKeypairOperability(keyUid, operability) + proc onPreferredSharingChainsUpdated*(self: View, keyUid, address, prodPreferredChainIds, testPreferredChainIds: string) = self.keyPairModel.onPreferredSharingChainsUpdated(keyUid, address, prodPreferredChainIds, testPreferredChainIds) diff --git a/src/app/modules/main/profile_section/wallet/controller.nim b/src/app/modules/main/profile_section/wallet/controller.nim new file mode 100644 index 0000000000..4ff74ca7bf --- /dev/null +++ b/src/app/modules/main/profile_section/wallet/controller.nim @@ -0,0 +1,31 @@ +import io_interface +import app/core/eventemitter +import app_service/service/wallet_account/service as wallet_account_service +import app_service/service/devices/service as devices_service + +type + Controller* = ref object of RootObj + delegate: io_interface.AccessInterface + events: EventEmitter + walletAccountService: wallet_account_service.Service + +proc newController*( + delegate: io_interface.AccessInterface, + events: EventEmitter, + walletAccountService: wallet_account_service.Service, +): Controller = + result = Controller() + result.delegate = delegate + result.events = events + result.walletAccountService = walletAccountService + +proc delete*(self: Controller) = + discard + +proc init*(self: Controller) = + self.events.on(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE) do(e:Args): + let data = LocalPairingStatus(e) + self.delegate.onLocalPairingStatusUpdate(data) + +proc hasPairedDevices*(self: Controller): bool = + return self.walletAccountService.hasPairedDevices() \ No newline at end of file diff --git a/src/app/modules/main/profile_section/wallet/io_interface.nim b/src/app/modules/main/profile_section/wallet/io_interface.nim index 9b15122f71..1a6c0aa7ac 100644 --- a/src/app/modules/main/profile_section/wallet/io_interface.nim +++ b/src/app/modules/main/profile_section/wallet/io_interface.nim @@ -1,4 +1,6 @@ import NimQml +import app/modules/shared_modules/keypair_import/module as keypair_import_module +import app_service/service/devices/service as devices_service type AccessInterface* {.pure inheritable.} = ref object of RootObj @@ -16,18 +18,36 @@ method isLoaded*(self: AccessInterface): bool {.base.} = # View Delegate Interface # Delegate for the view must be declared here due to use of QtObject and multi # inheritance, which is not well supported in Nim. -method viewDidLoad*(self: AccessInterface) {.base.} = +method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} = raise newException(ValueError, "No implementation available") # Methods called by submodules of this module method accountsModuleDidLoad*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") +method getAccountsModule*(self: AccessInterface): QVariant {.base.} = + raise newException(ValueError, "No implementation available") + method networksModuleDidLoad*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") -method getAccountsModuleAsVariant*(self: AccessInterface): QVariant {.base.} = +method getNetworksModule*(self: AccessInterface): QVariant {.base.} = raise newException(ValueError, "No implementation available") -method getNetworksModuleAsVariant*(self: AccessInterface): QVariant {.base.} = +method getKeypairImportModule*(self: AccessInterface): QVariant {.base.} = + raise newException(ValueError, "No implementation available") + +method onKeypairImportModuleLoaded*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method destroyKeypairImportPopup*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method runKeypairImportPopup*(self: AccessInterface, keyUid: string, importOption: ImportOption) {.base.} = + raise newException(ValueError, "No implementation available") + +method hasPairedDevices*(self: AccessInterface): bool {.base.} = + raise newException(ValueError, "No implementation available") + +method onLocalPairingStatusUpdate*(self: AccessInterface, data: LocalPairingStatus) {.base.} = raise newException(ValueError, "No implementation available") \ No newline at end of file diff --git a/src/app/modules/main/profile_section/wallet/module.nim b/src/app/modules/main/profile_section/wallet/module.nim index 58b4927a99..29584fc5c3 100644 --- a/src/app/modules/main/profile_section/wallet/module.nim +++ b/src/app/modules/main/profile_section/wallet/module.nim @@ -1,16 +1,20 @@ import NimQml, chronicles import ./io_interface as io_interface +import ./controller, ./view import ../io_interface as delegate_interface import ./accounts/module as accounts_module import ./networks/module as networks_module -import ../../../../global/global_singleton -import ../../../../core/eventemitter -import ../../../../../app_service/service/wallet_account/service as wallet_account_service -import ../../../../../app_service/service/network/service as network_service -import ../../../../../app_service/service/settings/service as settings_service +import app/global/global_singleton +import app/core/eventemitter +import app/modules/shared_modules/keypair_import/module as keypair_import_module +import app_service/service/accounts/service as accounts_service +import app_service/service/wallet_account/service as wallet_account_service +import app_service/service/network/service as network_service +import app_service/service/settings/service as settings_service +import app_service/service/devices/service as devices_service logScope: topics = "profile-section-wallet-module" @@ -21,43 +25,56 @@ export io_interface type Module* = ref object of io_interface.AccessInterface delegate: delegate_interface.AccessInterface + controller: Controller + view: View + viewVariant: QVariant events: EventEmitter moduleLoaded: bool - + accountsService: accounts_service.Service + walletAccountService: wallet_account_service.Service accountsModule: accounts_module.AccessInterface networksModule: networks_module.AccessInterface + keypairImportModule: keypair_import_module.AccessInterface proc newModule*( delegate: delegate_interface.AccessInterface, events: EventEmitter, + accountsService: accounts_service.Service, walletAccountService: wallet_account_service.Service, settingsService: settings_service.Service, networkService: network_service.Service, ): Module = result = Module() result.delegate = delegate + result.controller = controller.newController(result, events, walletAccountService) + result.view = newView(result) + result.viewVariant = newQVariant(result.view) result.events = events result.moduleLoaded = false - + result.accountsService = accountsService + result.walletAccountService = walletAccountService result.accountsModule = accounts_module.newModule(result, events, walletAccountService, networkService) result.networksModule = networks_module.newModule(result, events, networkService, walletAccountService, settingsService) - + method delete*(self: Module) = + self.controller.delete + self.view.delete + self.viewVariant.delete self.accountsModule.delete self.networksModule.delete + if not self.keypairImportModule.isNil: + self.keypairImportModule.delete method load*(self: Module) = + self.controller.init() self.accountsModule.load() self.networksModule.load() - + method isLoaded*(self: Module): bool = return self.moduleLoaded -method getAccountsModuleAsVariant*(self: Module): QVariant = - return self.accountsModule.getModuleAsVariant() - -method getNetworksModuleAsVariant*(self: Module): QVariant = - return self.networksModule.getModuleAsVariant() +method getModuleAsVariant*(self: Module): QVariant = + return self.viewVariant proc checkIfModuleDidLoad(self: Module) = if(not self.accountsModule.isLoaded()): @@ -75,5 +92,37 @@ method viewDidLoad*(self: Module) = method accountsModuleDidLoad*(self: Module) = self.checkIfModuleDidLoad() +method getAccountsModule*(self: Module): QVariant = + return self.accountsModule.getModuleAsVariant() + method networksModuleDidLoad*(self: Module) = - self.checkIfModuleDidLoad() \ No newline at end of file + self.checkIfModuleDidLoad() + +method getNetworksModule*(self: Module): QVariant = + return self.networksModule.getModuleAsVariant() + +method destroyKeypairImportPopup*(self: Module) = + if self.keypairImportModule.isNil: + return + self.view.emitDestroyKeypairImportPopup() + self.keypairImportModule.delete + self.keypairImportModule = nil + +method runKeypairImportPopup*(self: Module, keyUid: string, importOption: ImportOption) = + self.keypairImportModule = keypair_import_module.newModule(self, self.events, self.accountsService, self.walletAccountService) + self.keypairImportModule.load(keyUid, importOption) + +method getKeypairImportModule*(self: Module): QVariant = + if self.keypairImportModule.isNil: + return newQVariant() + return self.keypairImportModule.getModuleAsVariant() + +method onKeypairImportModuleLoaded*(self: Module) = + self.view.emitDisplayKeypairImportPopup() + +method hasPairedDevices*(self: Module): bool = + return self.controller.hasPairedDevices() + +method onLocalPairingStatusUpdate*(self: Module, data: LocalPairingStatus) = + if data.state == LocalPairingState.Finished: + self.view.emitHasPairedDevicesChangedSignal() \ No newline at end of file diff --git a/src/app/modules/main/profile_section/wallet/view.nim b/src/app/modules/main/profile_section/wallet/view.nim new file mode 100644 index 0000000000..4702a16038 --- /dev/null +++ b/src/app/modules/main/profile_section/wallet/view.nim @@ -0,0 +1,52 @@ +import NimQml + +import ./io_interface +from app/modules/shared_modules/keypair_import/module import ImportOption + +QtObject: + type + View* = ref object of QObject + delegate: io_interface.AccessInterface + + proc delete*(self: View) = + self.QObject.delete + + proc newView*(delegate: io_interface.AccessInterface): View = + new(result, delete) + result.QObject.setup + result.delegate = delegate + + proc getAccountsModule(self: View): QVariant {.slot.} = + return self.delegate.getAccountsModule() + QtProperty[QVariant] accountsModule: + read = getAccountsModule + + proc getNetworksModule(self: View): QVariant {.slot.} = + return self.delegate.getNetworksModule() + QtProperty[QVariant] networksModule: + read = getNetworksModule + + proc runKeypairImportPopup*(self: View, keyUid: string, importOption: int) {.slot.} = + self.delegate.runKeypairImportPopup(keyUid, ImportOption(importOption)) + + proc getKeypairImportModule(self: View): QVariant {.slot.} = + return self.delegate.getKeypairImportModule() + QtProperty[QVariant] keypairImportModule: + read = getKeypairImportModule + + proc displayKeypairImportPopup*(self: View) {.signal.} + proc emitDisplayKeypairImportPopup*(self: View) = + self.displayKeypairImportPopup() + + proc destroyKeypairImportPopup*(self: View) {.signal.} + proc emitDestroyKeypairImportPopup*(self: View) = + self.destroyKeypairImportPopup() + + proc hasPairedDevicesChanged*(self: View) {.signal.} + proc emitHasPairedDevicesChangedSignal*(self: View) = + self.hasPairedDevicesChanged() + proc getHasPairedDevices(self: View): bool {.slot.} = + return self.delegate.hasPairedDevices() + QtProperty[bool] hasPairedDevices: + read = getHasPairedDevices + notify = hasPairedDevicesChanged \ No newline at end of file diff --git a/src/app/modules/shared/keypairs.nim b/src/app/modules/shared/keypairs.nim index b31369b553..fac43495ec 100644 --- a/src/app/modules/shared/keypairs.nim +++ b/src/app/modules/shared/keypairs.nim @@ -10,82 +10,62 @@ export keypair_item logScope: topics = "shared-keypairs" +proc buildKeypairItem*(keypair: KeypairDto, areTestNetworksEnabled: bool): KeyPairItem = + if keypair.accounts.len == 0: + ## we should never be here + error "there must not be any keypair without accounts", keyUid=keypair.keyUid + return + let publicKey = keypair.accounts[0].publicKey # in case of other but the profile keypair we take public key of first account as keypair's public key + var item = newKeyPairItem(keyUid = keypair.keyUid, + pubKey = publicKey, + locked = false, + name = keypair.name, + image = "", + icon = "", + pairType = KeyPairType.Unknown, + derivedFrom = keypair.derivedFrom, + lastUsedDerivationIndex = keypair.lastUsedDerivationIndex, + migratedToKeycard = keypair.keycards.len > 0, + syncedFrom = keypair.syncedFrom) + + if keypair.keypairType == KeypairTypeProfile: + item.setPubKey(singletonInstance.userProfile.getPubKey()) + item.setName(singletonInstance.userProfile.getName()) + item.setImage(singletonInstance.userProfile.getIcon()) + item.setPairType(KeyPairType.Profile.int) + elif keypair.keypairType == KeypairTypeSeed: + item.setIcon(if item.getMigratedToKeycard(): "keycard" else: "key_pair_seed_phrase") + item.setPairType(KeyPairType.SeedImport.int) + elif keypair.keypairType == KeypairTypeKey: + item.setIcon(if item.getMigratedToKeycard(): "keycard" else: "objects") + item.setPairType(KeyPairType.PrivateKeyImport.int) + + for acc in keypair.accounts: + if acc.isChat: + continue + var icon = "" + if acc.emoji.len == 0: + icon = "wallet" + item.addAccount(newKeyPairAccountItem(acc.name, acc.path, acc.address, acc.publicKey, acc.emoji, acc.colorId, + icon, newCurrencyAmount(), balanceFetched = true, operability = acc.operable, acc.isWallet, areTestNetworksEnabled, + acc.prodPreferredChainIds, acc.testPreferredChainIds)) + return item + proc buildKeyPairsList*(keypairs: seq[KeypairDto], excludeAlreadyMigratedPairs: bool, excludePrivateKeyKeypairs: bool, areTestNetworksEnabled: bool = false): seq[KeyPairItem] = var items: seq[KeyPairItem] for kp in keypairs: - if kp.accounts.len == 0: - ## we should never be here - error "there must not be any keypair without accounts", keyUid=kp.keyUid - return - let publicKey = kp.accounts[0].publicKey # in case of other but the profile keypair we take public key of first account as keypair's public key - let kpMigrated = kp.keycards.len > 0 - if excludeAlreadyMigratedPairs and kpMigrated: + let item = buildKeypairItem(kp, areTestNetworksEnabled) + if item.isNil: continue - if kp.keypairType == KeypairTypeProfile: - var item = newKeyPairItem(keyUid = kp.keyUid, - pubKey = singletonInstance.userProfile.getPubKey(), - locked = false, - name = singletonInstance.userProfile.getName(), - image = singletonInstance.userProfile.getIcon(), - icon = "", - pairType = KeyPairType.Profile, - derivedFrom = kp.derivedFrom, - lastUsedDerivationIndex = kp.lastUsedDerivationIndex, - migratedToKeycard = kpMigrated, - syncedFrom = kp.syncedFrom) - for acc in kp.accounts: - if acc.isChat: - continue - var icon = "" - if acc.emoji.len == 0: - icon = "wallet" - item.addAccount(newKeyPairAccountItem(acc.name, acc.path, acc.address, acc.publicKey, acc.emoji, acc.colorId, - icon, newCurrencyAmount(), balanceFetched = true, operability = acc.operable, acc.isWallet, areTestNetworksEnabled, acc.prodPreferredChainIds, acc.testPreferredChainIds)) + if excludeAlreadyMigratedPairs and item.getMigratedToKeycard(): + continue + if item.getPairType() == KeypairType.Profile.int: items.insert(item, 0) # Status Account must be at first place continue - if kp.keypairType == KeypairTypeSeed: - var item = newKeyPairItem(keyUid = kp.keyUid, - pubKey = publicKey, - locked = false, - name = kp.name, - image = "", - icon = if kpMigrated: "keycard" else: "key_pair_seed_phrase", - pairType = KeyPairType.SeedImport, - derivedFrom = kp.derivedFrom, - lastUsedDerivationIndex = kp.lastUsedDerivationIndex, - migratedToKeycard = kpMigrated, - syncedFrom = kp.syncedFrom) - for acc in kp.accounts: - var icon = "" - if acc.emoji.len == 0: - icon = "wallet" - item.addAccount(newKeyPairAccountItem(acc.name, acc.path, acc.address, acc.publicKey, acc.emoji, acc.colorId, - icon, newCurrencyAmount(), balanceFetched = true, operability = acc.operable, acc.isWallet, areTestNetworksEnabled, acc.prodPreferredChainIds, acc.testPreferredChainIds)) - items.add(item) - continue - if kp.keypairType == KeypairTypeKey: - if excludePrivateKeyKeypairs: - continue - var item = newKeyPairItem(keyUid = kp.keyUid, - pubKey = publicKey, - locked = false, - name = kp.name, - image = "", - icon = if kpMigrated: "keycard" else: "objects", - pairType = KeyPairType.PrivateKeyImport, - derivedFrom = kp.derivedFrom, - lastUsedDerivationIndex = kp.lastUsedDerivationIndex, - migratedToKeycard = kpMigrated, - syncedFrom = kp.syncedFrom) - for acc in kp.accounts: - var icon = "" - if acc.emoji.len == 0: - icon = "wallet" - item.addAccount(newKeyPairAccountItem(acc.name, acc.path, acc.address, acc.publicKey, acc.emoji, acc.colorId, - icon, newCurrencyAmount(), balanceFetched = true, operability = acc.operable, acc.isWallet, areTestNetworksEnabled, acc.prodPreferredChainIds, acc.testPreferredChainIds)) - items.add(item) + if item.getPairType() == KeypairType.PrivateKeyImport.int and excludePrivateKeyKeypairs: continue + items.add(item) if items.len == 0: debug "sm_there is no any key pair for the logged in user that is not already migrated to a keycard" - return items + return items \ No newline at end of file diff --git a/src/app/modules/shared_modules/add_account/derived_address_item.nim b/src/app/modules/shared_models/derived_address_item.nim similarity index 100% rename from src/app/modules/shared_modules/add_account/derived_address_item.nim rename to src/app/modules/shared_models/derived_address_item.nim diff --git a/src/app/modules/shared_modules/add_account/derived_address_model.nim b/src/app/modules/shared_models/derived_address_model.nim similarity index 100% rename from src/app/modules/shared_modules/add_account/derived_address_model.nim rename to src/app/modules/shared_models/derived_address_model.nim diff --git a/src/app/modules/shared_models/keypair_account_model.nim b/src/app/modules/shared_models/keypair_account_model.nim index 6421f86504..1e62c5e6bd 100644 --- a/src/app/modules/shared_models/keypair_account_model.nim +++ b/src/app/modules/shared_models/keypair_account_model.nim @@ -125,10 +125,9 @@ QtObject: self.items[i].setEmoji(emoji) return - proc updateOperabilityForAddress*(self: KeyPairAccountModel, address: string, operability: string) = + proc updateOperabilityForAllAddresses*(self: KeyPairAccountModel, operability: string) = for i in 0 ..< self.items.len: - if cmpIgnoreCase(self.items[i].getAddress(), address) == 0: - self.items[i].setOperability(operability) + self.items[i].setOperability(operability) proc setBalanceForAddress*(self: KeyPairAccountModel, address: string, balance: CurrencyAmount) = for i in 0 ..< self.items.len: diff --git a/src/app/modules/shared_models/keypair_item.nim b/src/app/modules/shared_models/keypair_item.nim index c051662130..8d1cee424a 100644 --- a/src/app/modules/shared_models/keypair_item.nim +++ b/src/app/modules/shared_models/keypair_item.nim @@ -270,8 +270,8 @@ QtObject: self.accounts.updatePreferredSharingChainsForAddress(address, prodPreferredChainIds, testPreferredChainIds) proc setBalanceForAddress*(self: KeyPairItem, address: string, balance: CurrencyAmount) = self.accounts.setBalanceForAddress(address, balance) - proc updateOperabilityForAccountWithAddress*(self: KeyPairItem, address: string, operability: string) = - self.accounts.updateOperabilityForAddress(address, operability) + proc updateOperabilityForAllAddresses*(self: KeyPairItem, operability: string) = + self.accounts.updateOperabilityForAllAddresses(operability) self.operabilityChanged() proc setItem*(self: KeyPairItem, item: KeyPairItem) = diff --git a/src/app/modules/shared_models/keypair_model.nim b/src/app/modules/shared_models/keypair_model.nim index b29a61ab7b..285f7ad562 100644 --- a/src/app/modules/shared_models/keypair_model.nim +++ b/src/app/modules/shared_models/keypair_model.nim @@ -82,6 +82,12 @@ QtObject: item.getAccountsModel().updateDetailsForAddressIfTheyAreSet(address, name, colorId, emoji) break + proc onUpdatedKeypairOperability*(self: KeyPairModel, keyUid, operability: string) = + for item in self.items: + if keyUid == item.getKeyUid(): + item.updateOperabilityForAllAddresses(operability) + break + proc onPreferredSharingChainsUpdated*(self: KeyPairModel,keyUid, address, prodPreferredChainIds, testPreferredChainIds: string) = for item in self.items: if keyUid == item.getKeyUid(): diff --git a/src/app/modules/shared_modules/add_account/module.nim b/src/app/modules/shared_modules/add_account/module.nim index 51ae8e9fdc..174eaf6d85 100644 --- a/src/app/modules/shared_modules/add_account/module.nim +++ b/src/app/modules/shared_modules/add_account/module.nim @@ -1,7 +1,7 @@ import NimQml, Tables, strutils, sequtils, sugar, chronicles import io_interface -import view, controller, derived_address_model +import view, controller import internal/[state, state_factory] import ../../../core/eventemitter @@ -9,7 +9,7 @@ import ../../../core/eventemitter import ../../../global/global_singleton import ../../shared/keypairs -import ../../shared_models/[keypair_model] +import ../../shared_models/[keypair_model, derived_address_model] import ../../shared_modules/keycard_popup/module as keycard_shared_module import ../../../../app_service/common/account_constants diff --git a/src/app/modules/shared_modules/add_account/view.nim b/src/app/modules/shared_modules/add_account/view.nim index 0c2a4ee4ef..823d140f81 100644 --- a/src/app/modules/shared_modules/add_account/view.nim +++ b/src/app/modules/shared_modules/add_account/view.nim @@ -1,8 +1,7 @@ import NimQml import io_interface -import derived_address_model import internal/[state, state_wrapper] -import ../../shared_models/[keypair_model, keypair_item] +import ../../shared_models/[keypair_model, keypair_item, derived_address_model] QtObject: type @@ -178,8 +177,8 @@ QtObject: return self.selectedDerivedAddressVariant QtProperty[QVariant] selectedDerivedAddress: read = getSelectedDerivedAddressVariant - notify = selectedDerivedAddressChanged - + notify = selectedDerivedAddressChanged + proc setSelectedDerivedAddress*(self: View, item: DerivedAddressItem) = self.selectedDerivedAddress.setItem(item) self.selectedDerivedAddressChanged() @@ -192,8 +191,8 @@ QtObject: return self.watchOnlyAccAddressVariant QtProperty[QVariant] watchOnlyAccAddress: read = getWatchOnlyAccAddressVariant - notify = watchOnlyAccAddressChanged - + notify = watchOnlyAccAddressChanged + proc setWatchOnlyAccAddress*(self: View, item: DerivedAddressItem) = self.watchOnlyAccAddress.setItem(item) self.watchOnlyAccAddressChanged() @@ -206,8 +205,8 @@ QtObject: return self.privateKeyAccAddressVariant QtProperty[QVariant] privateKeyAccAddress: read = getPrivateKeyAccAddressVariant - notify = privateKeyAccAddressChanged - + notify = privateKeyAccAddressChanged + proc setPrivateKeyAccAddress*(self: View, item: DerivedAddressItem) = self.privateKeyAccAddress.setItem(item) self.privateKeyAccAddressChanged() @@ -258,7 +257,7 @@ QtObject: QtProperty[string] newKeyPairName: read = getNewKeyPairName write = setNewKeyPairName - notify = newKeyPairNameChanged + notify = newKeyPairNameChanged proc selectedEmojiChanged*(self: View) {.signal.} proc setSelectedEmoji*(self: View, value: string) {.slot.} = @@ -312,7 +311,7 @@ QtObject: QtProperty[string] derivationPath: read = getDerivationPath write = setDerivationPath - notify = derivationPathChanged + notify = derivationPathChanged proc suggestedDerivationPathChanged*(self: View) {.signal.} proc getSuggestedDerivationPath*(self: View): string {.slot.} = @@ -320,11 +319,11 @@ QtObject: QtProperty[string] suggestedDerivationPath: read = getSuggestedDerivationPath notify = suggestedDerivationPathChanged - + proc setSuggestedDerivationPath*(self: View, value: string) = self.suggestedDerivationPath = value self.suggestedDerivationPathChanged() - + proc changeSelectedOrigin*(self: View, keyUid: string) {.slot.} = self.delegate.changeSelectedOrigin(keyUid) @@ -354,4 +353,4 @@ QtObject: proc startScanningForActivity*(self: View) {.slot.} = self.delegate.startScanningForActivity() - + diff --git a/src/app/modules/shared_modules/keypair_import/controller.nim b/src/app/modules/shared_modules/keypair_import/controller.nim new file mode 100644 index 0000000000..eb35a30730 --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/controller.nim @@ -0,0 +1,96 @@ +import times, chronicles +import io_interface + +import app/core/eventemitter +import app_service/service/accounts/service as accounts_service +import app_service/service/wallet_account/service as wallet_account_service + +import app/modules/shared_modules/keycard_popup/io_interface as keycard_shared_module + +logScope: + topics = "wallet-keycard-import-controller" + +const UNIQUE_WALLET_SECTION_KEYPAIR_IMPORT_MODULE_IDENTIFIER* = "WalletSection-KeypairImportModule" + +type + Controller* = ref object of RootObj + delegate: io_interface.AccessInterface + events: EventEmitter + accountsService: accounts_service.Service + walletAccountService: wallet_account_service.Service + uniqueFetchingDetailsId: string + tmpPrivateKey: string + tmpSeedPhrase: string + tmpGeneratedAccount: GeneratedAccountDto + +proc newController*(delegate: io_interface.AccessInterface, + events: EventEmitter, + accountsService: accounts_service.Service, + walletAccountService: wallet_account_service.Service): + Controller = + result = Controller() + result.delegate = delegate + result.events = events + result.accountsService = accountsService + result.walletAccountService = walletAccountService + +proc delete*(self: Controller) = + discard + +proc init*(self: Controller) = + self.events.on(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args): + let args = SharedKeycarModuleArgs(e) + if args.uniqueIdentifier != UNIQUE_WALLET_SECTION_KEYPAIR_IMPORT_MODULE_IDENTIFIER: + return + self.delegate.onUserAuthenticated(args.pin, args.password, args.keyUid) + + self.events.on(SIGNAL_WALLET_ACCOUNT_ADDRESS_DETAILS_FETCHED) do(e:Args): + var args = DerivedAddressesArgs(e) + if args.uniqueId != self.uniqueFetchingDetailsId: + return + self.delegate.onAddressDetailsFetched(args.derivedAddresses, args.error) + +proc closeKeypairImportPopup*(self: Controller) = + self.delegate.closeKeypairImportPopup() + +proc setPrivateKey*(self: Controller, value: string) = + self.tmpPrivateKey = value + +proc getPrivateKey*(self: Controller): string = + return self.tmpPrivateKey + +proc setSeedPhrase*(self: Controller, value: string) = + self.tmpSeedPhrase = value + +proc getSeedPhrase*(self: Controller): string = + return self.tmpSeedPhrase + +proc authenticateLoggedInUser*(self: Controller) = + let data = SharedKeycarModuleAuthenticationArgs(uniqueIdentifier: UNIQUE_WALLET_SECTION_KEYPAIR_IMPORT_MODULE_IDENTIFIER) + self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data) + +proc getKeypairByKeyUid*(self: Controller, keyUid: string): KeypairDto = + return self.walletAccountService.getKeypairByKeyUid(keyUid) + +proc createAccountFromPrivateKey*(self: Controller, privateKey: string): GeneratedAccountDto = + self.setPrivateKey(privateKey) + self.tmpGeneratedAccount = self.accountsService.createAccountFromPrivateKey(privateKey) + return self.tmpGeneratedAccount + +proc createAccountFromSeedPhrase*(self: Controller, seedPhrase: string): GeneratedAccountDto = + self.setSeedPhrase(seedPhrase) + self.tmpGeneratedAccount = self.accountsService.createAccountFromMnemonic(seedPhrase) + return self.tmpGeneratedAccount + +proc getGeneratedAccount*(self: Controller): GeneratedAccountDto = + return self.tmpGeneratedAccount + +proc fetchDetailsForAddresses*(self: Controller, addresses: seq[string]) = + self.uniqueFetchingDetailsId = $now().toTime().toUnix() + self.walletAccountService.fetchDetailsForAddresses(self.uniqueFetchingDetailsId, addresses) + +proc makePrivateKeyKeypairFullyOperable*(self: Controller, keyUid, privateKey, password: string, doPasswordHashing: bool): string = + return self.walletAccountService.makePrivateKeyKeypairFullyOperable(keyUid, privateKey, password, doPasswordHashing) + +proc makeSeedPhraseKeypairFullyOperable*(self: Controller, keyUid, mnemonic, password: string, doPasswordHashing: bool): string = + return self.walletAccountService.makeSeedPhraseKeypairFullyOperable(keyUid, mnemonic, password, doPasswordHashing) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/internal/import_private_key_state.nim b/src/app/modules/shared_modules/keypair_import/internal/import_private_key_state.nim new file mode 100644 index 0000000000..3bb5d48f92 --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/internal/import_private_key_state.nim @@ -0,0 +1,12 @@ +type + ImportPrivateKeyState* = ref object of State + +proc newImportPrivateKeyState*(backState: State): ImportPrivateKeyState = + result = ImportPrivateKeyState() + result.setup(StateType.ImportPrivateKey, backState) + +proc delete*(self: ImportPrivateKeyState) = + self.State.delete + +method executePrePrimaryStateCommand*(self: ImportPrivateKeyState, controller: Controller) = + controller.authenticateLoggedInUser() \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/internal/import_seed_phrase_state.nim b/src/app/modules/shared_modules/keypair_import/internal/import_seed_phrase_state.nim new file mode 100644 index 0000000000..3aa5078d7b --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/internal/import_seed_phrase_state.nim @@ -0,0 +1,12 @@ +type + ImportSeedPhraseState* = ref object of State + +proc newImportSeedPhraseState*(backState: State): ImportSeedPhraseState = + result = ImportSeedPhraseState() + result.setup(StateType.ImportSeedPhrase, backState) + +proc delete*(self: ImportSeedPhraseState) = + self.State.delete + +method executePrePrimaryStateCommand*(self: ImportSeedPhraseState, controller: Controller) = + controller.authenticateLoggedInUser() \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/internal/state.nim b/src/app/modules/shared_modules/keypair_import/internal/state.nim new file mode 100644 index 0000000000..fc4199d1d7 --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/internal/state.nim @@ -0,0 +1,81 @@ +import ../controller + +type StateType* {.pure.} = enum + NoState = "NoState" + Main = "Main" + ImportSeedPhrase = "ImportSeedPhrase" + ImportPrivateKey = "ImportPrivateKey" + + +## This is the base class for all states +## We should not instance of this class (in c++ this will be an abstract class). +type + State* {.pure inheritable.} = ref object of RootObj + stateType: StateType + backState: State + +proc setup*(self: State, stateType: StateType, backState: State) = + self.stateType = stateType + self.backState = backState + +## `stateType` - detemines the state this instance describes +## `backState` - the sate (instance) we're moving to if user clicks "back" button, +## in case we should not display "back" button for this state, set it to `nil` +proc newState*(self: State, stateType: StateType, backState: State): State = + result = State() + result.setup(stateType, backState) + +proc delete*(self: State) = + discard + +## Returns state type +method stateType*(self: State): StateType {.inline base.} = + self.stateType + +## Returns back state instance +method getBackState*(self: State): State {.inline base.} = + self.backState + +## Returns true if we should display "back" button, otherwise false +method displayBackButton*(self: State): bool {.inline base.} = + return not self.backState.isNil + +## Returns next state instance if "primary" action is triggered +method getNextPrimaryState*(self: State, controller: Controller): State {.inline base.} = + return nil + +## Returns next state instance if "secondary" action is triggered +method getNextSecondaryState*(self: State, controller: Controller): State {.inline base.} = + return nil + +## Returns next state instance in case the "tertiary" action is triggered +method getNextTertiaryState*(self: State, controller: Controller): State {.inline base.} = + return nil + +## Returns next state instance in case the "quaternary" action is triggered +method getNextQuaternaryState*(self: State, controller: Controller): State {.inline base.} = + return nil + +## This method is executed if "cancel" action is triggered (invalidates current flow) +method executeCancelCommand*(self: State, controller: Controller) {.inline base.} = + controller.closeKeypairImportPopup() + +## This method is executed before back state is set, if "back" action is triggered +method executePreBackStateCommand*(self: State, controller: Controller) {.inline base.} = + discard + +## This method is executed before primary state is set, if "primary" action is triggered +method executePrePrimaryStateCommand*(self: State, controller: Controller) {.inline base.} = + discard + +## This method is executed before secondary state is set, if "secondary" action is triggered +method executePreSecondaryStateCommand*(self: State, controller: Controller) {.inline base.} = + discard + +## This method is executed in case "tertiary" action is triggered +method executePreTertiaryStateCommand*(self: State, controller: Controller) {.inline base.} = + discard + +## This method is executed in case "quaternary" action is triggered +method executePreQuaternaryStateCommand*(self: State, controller: Controller) {.inline base.} = + discard \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/internal/state_factory.nim b/src/app/modules/shared_modules/keypair_import/internal/state_factory.nim new file mode 100644 index 0000000000..9ebf4e8e84 --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/internal/state_factory.nim @@ -0,0 +1,21 @@ +import chronicles +import ../controller + +import state + +logScope: + topics = "keypair-import-module-state-factory" + +# Forward declaration +proc createState*(stateToBeCreated: StateType, backState: State): State + +include import_private_key_state +include import_seed_phrase_state + +proc createState*(stateToBeCreated: StateType, backState: State): State = + if stateToBeCreated == StateType.ImportPrivateKey: + return newImportPrivateKeyState(backState) + if stateToBeCreated == StateType.ImportSeedPhrase: + return newImportSeedPhraseState(backState) + + error "Keypair import - no implementation available for state", state=stateToBeCreated diff --git a/src/app/modules/shared_modules/keypair_import/internal/state_wrapper.nim b/src/app/modules/shared_modules/keypair_import/internal/state_wrapper.nim new file mode 100644 index 0000000000..c526c3096f --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/internal/state_wrapper.nim @@ -0,0 +1,62 @@ +import NimQml +import state + +QtObject: + type StateWrapper* = ref object of QObject + stateObj: State + + proc delete*(self: StateWrapper) = + self.QObject.delete + + proc newStateWrapper*(): StateWrapper = + new(result, delete) + result.QObject.setup() + + proc stateWrapperChanged*(self:StateWrapper) {.signal.} + + proc setStateObj*(self: StateWrapper, stateObj: State) = + self.stateObj = stateObj + self.stateWrapperChanged() + + proc getStateObj*(self: StateWrapper): State = + return self.stateObj + + proc getStateType(self: StateWrapper): string {.slot.} = + if(self.stateObj.isNil): + return $StateType.NoState + return $self.stateObj.stateType() + QtProperty[string] stateType: + read = getStateType + notify = stateWrapperChanged + + proc getDisplayBackButton(self: StateWrapper): bool {.slot.} = + if(self.stateObj.isNil): + return false + return self.stateObj.displayBackButton() + QtProperty[bool] displayBackButton: + read = getDisplayBackButton + notify = stateWrapperChanged + + proc backActionClicked*(self: StateWrapper) {.signal.} + proc doBackAction*(self: StateWrapper) {.slot.} = + self.backActionClicked() + + proc cancelActionClicked*(self: StateWrapper) {.signal.} + proc doCancelAction*(self: StateWrapper) {.slot.} = + self.cancelActionClicked() + + proc primaryActionClicked*(self: StateWrapper) {.signal.} + proc doPrimaryAction*(self: StateWrapper) {.slot.} = + self.primaryActionClicked() + + proc secondaryActionClicked*(self: StateWrapper) {.signal.} + proc doSecondaryAction*(self: StateWrapper) {.slot.} = + self.secondaryActionClicked() + + proc tertiaryActionClicked*(self: StateWrapper) {.signal.} + proc doTertiaryAction*(self: StateWrapper) {.slot.} = + self.tertiaryActionClicked() + + proc quaternaryActionClicked*(self: StateWrapper) {.signal.} + proc doQuaternaryAction*(self: StateWrapper) {.slot.} = + self.quaternaryActionClicked() \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/io_interface.nim b/src/app/modules/shared_modules/keypair_import/io_interface.nim new file mode 100644 index 0000000000..6a4c2faeba --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/io_interface.nim @@ -0,0 +1,51 @@ +import Tables, NimQml + +import app_service/service/wallet_account/dto/derived_address_dto + +type ImportOption* {.pure.}= enum + SeedPhrase = 1, + PrivateKey = 2 + +type + AccessInterface* {.pure inheritable.} = ref object of RootObj + +method delete*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method load*(self: AccessInterface, keyUid: string, importOption: ImportOption) {.base.} = + raise newException(ValueError, "No implementation available") + +method closeKeypairImportPopup*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} = + raise newException(ValueError, "No implementation available") + +method onBackActionClicked*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method onPrimaryActionClicked*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method onCancelActionClicked*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method onUserAuthenticated*(self: AccessInterface, pin: string, password: string, keyUid: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method changePrivateKey*(self: AccessInterface, privateKey: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method changeSeedPhrase*(self: AccessInterface, seedPhrase: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method validSeedPhrase*(self: AccessInterface, seedPhrase: string): bool {.base.} = + raise newException(ValueError, "No implementation available") + +method onAddressDetailsFetched*(self: AccessInterface, derivedAddresses: seq[DerivedAddressDto], error: string) {.base.} = + raise newException(ValueError, "No implementation available") + +type + DelegateInterface* = concept c + c.onKeypairImportModuleLoaded() + c.destroyKeypairImportPopup() \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/module.nim b/src/app/modules/shared_modules/keypair_import/module.nim new file mode 100644 index 0000000000..338f5b8a67 --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/module.nim @@ -0,0 +1,188 @@ +import NimQml, strutils, chronicles + +import io_interface +import view, controller +import internal/[state, state_factory] + +import app/global/global_singleton +import app/core/eventemitter +import app/modules/shared/keypairs +import app/modules/shared_models/[derived_address_model] +import app_service/service/accounts/service as accounts_service +import app_service/service/wallet_account/service as wallet_account_service + +export io_interface + +logScope: + topics = "wallet-keypair-import-module" + +type + Module*[T: io_interface.DelegateInterface] = ref object of io_interface.AccessInterface + delegate: T + events: EventEmitter + view: View + viewVariant: QVariant + controller: Controller + +proc newModule*[T](delegate: T, + events: EventEmitter, + accountsService: accounts_service.Service, + walletAccountService: wallet_account_service.Service): + Module[T] = + result = Module[T]() + result.delegate = delegate + result.view = newView(result) + result.viewVariant = newQVariant(result.view) + result.controller = controller.newController(result, events, accountsService, walletAccountService) + +method delete*[T](self: Module[T]) = + self.view.delete + self.viewVariant.delete + self.controller.delete + +method closeKeypairImportPopup*[T](self: Module[T]) = + self.delegate.destroyKeypairImportPopup() + +method getModuleAsVariant*[T](self: Module[T]): QVariant = + return self.viewVariant + +method load*[T](self: Module[T], keyUid: string, importOption: ImportOption) = + self.controller.init() + let keypair = self.controller.getKeypairByKeyUid(keyUid) + if keypair.isNil: + error "trying to import an unknown keypair" + self.closeKeypairImportPopup() + return + let keypairItem = buildKeypairItem(keypair, areTestNetworksEnabled = false) # testnetworks are irrelevant in this context + if keypairItem.isNil: + error "cannot generate keypair item for provided keypair" + self.closeKeypairImportPopup() + return + self.view.setSelectedKeypair(keypairItem) + if importOption == ImportOption.PrivateKey: + self.view.setCurrentState(newImportPrivateKeyState(nil)) + elif importOption == ImportOption.SeedPhrase: + self.view.setCurrentState(newImportSeedPhraseState(nil)) + self.delegate.onKeypairImportModuleLoaded() + +method onBackActionClicked*[T](self: Module[T]) = + let currStateObj = self.view.currentStateObj() + if currStateObj.isNil: + error "ki_cannot resolve current state" + return + debug "ki_back_action", currState=currStateObj.stateType() + currStateObj.executePreBackStateCommand(self.controller) + let backState = currStateObj.getBackState() + if backState.isNil: + return + self.view.setCurrentState(backState) + debug "ki_back_action - set state", newCurrState=backState.stateType() + +method onCancelActionClicked*[T](self: Module[T]) = + let currStateObj = self.view.currentStateObj() + if currStateObj.isNil: + error "ki_cannot resolve current state" + return + debug "ki_cancel_action", currState=currStateObj.stateType() + currStateObj.executeCancelCommand(self.controller) + +method onPrimaryActionClicked*[T](self: Module[T]) = + let currStateObj = self.view.currentStateObj() + if currStateObj.isNil: + error "ki_cannot resolve current state" + return + debug "ki_primary_action", currState=currStateObj.stateType() + currStateObj.executePrePrimaryStateCommand(self.controller) + let nextState = currStateObj.getNextPrimaryState(self.controller) + if nextState.isNil: + return + self.view.setCurrentState(nextState) + debug "ki_primary_action - set state", setCurrState=nextState.stateType() + +proc authenticateLoggedInUser[T](self: Module[T]) = + self.controller.authenticateLoggedInUser() + +method changePrivateKey*[T](self: Module[T], privateKey: string) = + self.view.setPrivateKeyAccAddress(newDerivedAddressItem()) + if privateKey.len == 0: + return + let genAccDto = self.controller.createAccountFromPrivateKey(privateKey) + if genAccDto.address.len == 0: + error "unable to resolve an address from the provided private key" + return + let kp = self.view.getSelectedKeypair() + if kp.isNil: + # should never be here + return + if kp.getKeyUid() != genAccDto.keyUid: + self.view.setEnteredPrivateKeyMatchTheKeypair(false) + error "entered private key doesn't refer to a keyapir being imported" + return + self.view.setEnteredPrivateKeyMatchTheKeypair(true) + self.view.setPrivateKeyAccAddress(newDerivedAddressItem(order = 0, address = genAccDto.address, publicKey = genAccDto.publicKey)) + self.controller.fetchDetailsForAddresses(@[genAccDto.address]) + +method changeSeedPhrase*[T](self: Module[T], seedPhrase: string) = + if seedPhrase.len == 0: + return + let genAccDto = self.controller.createAccountFromSeedPhrase(seedPhrase) + if seedPhrase.len > 0 and genAccDto.address.len == 0: + error "unable to create an account from the provided seed phrase" + return + +method validSeedPhrase*[T](self: Module[T], seedPhrase: string): bool = + let genAccDto = self.controller.createAccountFromSeedPhrase(seedPhrase) + let kp = self.view.getSelectedKeypair() + if kp.isNil: + # should never be here + return false + return kp.getKeyUid() == genAccDto.keyUid + +method onAddressDetailsFetched*[T](self: Module[T], derivedAddresses: seq[DerivedAddressDto], error: string) = + if error.len > 0: + error "ki_fetching address details error", err=error + return + let currStateObj = self.view.currentStateObj() + if currStateObj.isNil: + error "ki_cannot resolve current state" + return + # we always receive responses one by one + if derivedAddresses.len == 1: + var addressDetailsItem = newDerivedAddressItem(order = 0, + address = derivedAddresses[0].address, + publicKey = derivedAddresses[0].publicKey, + path = derivedAddresses[0].path, + alreadyCreated = derivedAddresses[0].alreadyCreated, + hasActivity = derivedAddresses[0].hasActivity, + loaded = true) + if currStateObj.stateType() == StateType.ImportPrivateKey: + if cmpIgnoreCase(self.view.getPrivateKeyAccAddress().getAddress(), addressDetailsItem.getAddress()) == 0: + self.view.setPrivateKeyAccAddress(addressDetailsItem) + return + error "ki_unknown error, since the length of the response is not expected", length=derivedAddresses.len + +method onUserAuthenticated*[T](self: Module[T], pin: string, password: string, keyUid: string) = + if password.len == 0: + info "ki_unsuccessful authentication" + return + let currStateObj = self.view.currentStateObj() + if currStateObj.isNil: + error "ki_cannot resolve current state" + return + if currStateObj.stateType() == StateType.ImportPrivateKey: + let res = self.controller.makePrivateKeyKeypairFullyOperable(self.controller.getGeneratedAccount().keyUid, + self.controller.getPrivateKey(), + password, + doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser()) + if res.len > 0: + error "ki_unable to make a keypair operable" + return + if currStateObj.stateType() == StateType.ImportSeedPhrase: + let res = self.controller.makeSeedPhraseKeypairFullyOperable(self.controller.getGeneratedAccount().keyUid, + self.controller.getSeedPhrase(), + password, + doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser()) + if res.len > 0: + error "ki_unable to make a keypair operable" + return + self.closeKeypairImportPopup() \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/view.nim b/src/app/modules/shared_modules/keypair_import/view.nim new file mode 100644 index 0000000000..127335f91b --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/view.nim @@ -0,0 +1,102 @@ +import NimQml +import io_interface +import internal/[state, state_wrapper] +import app/modules/shared_models/[keypair_item, derived_address_model] + +QtObject: + type + View* = ref object of QObject + delegate: io_interface.AccessInterface + currentState: StateWrapper + currentStateVariant: QVariant + selectedKeypair: KeyPairItem + selectedKeypairVariant: QVariant + privateKeyAccAddress: DerivedAddressItem + privateKeyAccAddressVariant: QVariant + enteredPrivateKeyMatchTheKeypair: bool + + proc delete*(self: View) = + self.currentStateVariant.delete + self.currentState.delete + self.selectedKeypair.delete + self.selectedKeypairVariant.delete + self.QObject.delete + + proc newView*(delegate: io_interface.AccessInterface): View = + new(result, delete) + result.QObject.setup + result.delegate = delegate + result.currentState = newStateWrapper() + result.currentStateVariant = newQVariant(result.currentState) + result.selectedKeypair = newKeyPairItem() + result.selectedKeypairVariant = newQVariant(result.selectedKeypair) + result.privateKeyAccAddress = newDerivedAddressItem() + result.privateKeyAccAddressVariant = newQVariant(result.privateKeyAccAddress) + result.enteredPrivateKeyMatchTheKeypair = false + + signalConnect(result.currentState, "backActionClicked()", result, "onBackActionClicked()", 2) + signalConnect(result.currentState, "cancelActionClicked()", result, "onCancelActionClicked()", 2) + signalConnect(result.currentState, "primaryActionClicked()", result, "onPrimaryActionClicked()", 2) + + proc currentStateObj*(self: View): State = + return self.currentState.getStateObj() + + proc setCurrentState*(self: View, state: State) = + self.currentState.setStateObj(state) + proc getCurrentState(self: View): QVariant {.slot.} = + return self.currentStateVariant + QtProperty[QVariant] currentState: + read = getCurrentState + + proc onBackActionClicked*(self: View) {.slot.} = + self.delegate.onBackActionClicked() + + proc onCancelActionClicked*(self: View) {.slot.} = + self.delegate.onCancelActionClicked() + + proc onPrimaryActionClicked*(self: View) {.slot.} = + self.delegate.onPrimaryActionClicked() + + proc getSelectedKeypair*(self: View): KeyPairItem = + return self.selectedKeypair + proc getSelectedKeypairAsVariant*(self: View): QVariant {.slot.} = + return self.selectedKeypairVariant + QtProperty[QVariant] selectedKeypair: + read = getSelectedKeypairAsVariant + + proc setSelectedKeypair*(self: View, item: KeyPairItem) = + self.selectedKeypair.setItem(item) + + proc getPrivateKeyAccAddress*(self: View): DerivedAddressItem = + return self.privateKeyAccAddress + + proc privateKeyAccAddressChanged*(self: View) {.signal.} + proc getPrivateKeyAccAddressVariant*(self: View): QVariant {.slot.} = + return self.privateKeyAccAddressVariant + QtProperty[QVariant] privateKeyAccAddress: + read = getPrivateKeyAccAddressVariant + notify = privateKeyAccAddressChanged + + proc setPrivateKeyAccAddress*(self: View, item: DerivedAddressItem) = + self.privateKeyAccAddress.setItem(item) + self.privateKeyAccAddressChanged() + + proc changePrivateKey*(self: View, privateKey: string) {.slot.} = + self.delegate.changePrivateKey(privateKey) + + proc changeSeedPhrase*(self: View, seedPhrase: string) {.slot.} = + self.delegate.changeSeedPhrase(seedPhrase) + + proc validSeedPhrase*(self: View, seedPhrase: string): bool {.slot.} = + return self.delegate.validSeedPhrase(seedPhrase) + + proc enteredPrivateKeyMatchTheKeypairChanged(self: View) {.signal.} + proc getEnteredPrivateKeyMatchTheKeypair*(self: View): bool {.slot.} = + return self.enteredPrivateKeyMatchTheKeypair + QtProperty[bool] enteredPrivateKeyMatchTheKeypair: + read = getEnteredPrivateKeyMatchTheKeypair + notify = enteredPrivateKeyMatchTheKeypairChanged + + proc setEnteredPrivateKeyMatchTheKeypair*(self: View, value: bool) = + self.enteredPrivateKeyMatchTheKeypair = value + self.enteredPrivateKeyMatchTheKeypairChanged() diff --git a/src/app_service/service/wallet_account/service_account.nim b/src/app_service/service/wallet_account/service_account.nim index f63fb04848..5d486c0b5f 100644 --- a/src/app_service/service/wallet_account/service_account.nim +++ b/src/app_service/service/wallet_account/service_account.nim @@ -215,6 +215,15 @@ proc removeAccountFromLocalStoreAndNotify(self: Service, address: string, notify if notify: self.events.emit(SIGNAL_WALLET_ACCOUNT_DELETED, AccountArgs(account: acc)) +proc updateKeypairOperabilityInLocalStoreAndNotify(self: Service, keyUid: string) = + let kp = self.getKeypairByKeyUid(keyUid) + if kp.isNil: + error "there is no known keypair", keyUid=keyUid, procName="updateKeypairOperabilityInLocalStoreAndNotify" + return + for acc in kp.accounts: + acc.operable = AccountFullyOperable + self.events.emit(SIGNAL_KEYPAIR_OPERABILITY_CHANGED, KeypairArgs(keypair: kp)) + proc updateAccountsPositions(self: Service) = let dbAccounts = getAccountsFromDb() for dbAcc in dbAccounts: @@ -305,6 +314,24 @@ proc addNewPrivateKeyKeypair*(self: Service, privateKey, password: string, doPas error "error: ", procName="addNewPrivateKeyKeypair", errName=e.name, errDesription=e.msg return e.msg +proc makePrivateKeyKeypairFullyOperable*(self: Service, keyUid, privateKey, password: string, doPasswordHashing: bool): string = + if password.len == 0: + error "for making a private key keypair fully operable, password must be provided" + return + var finalPassword = password + if doPasswordHashing: + finalPassword = utils.hashPassword(password) + try: + var response = status_go_accounts.makePrivateKeyKeypairFullyOperable(privateKey, finalPassword) + if not response.error.isNil: + error "status-go error", procName="makePrivateKeyKeypairFullyOperable", errCode=response.error.code, errDesription=response.error.message + return response.error.message + self.updateKeypairOperabilityInLocalStoreAndNotify(keyUid) + return "" + except Exception as e: + error "error: ", procName="makePrivateKeyKeypairFullyOperable", errName=e.name, errDesription=e.msg + return e.msg + ## Mandatory fields for all accounts: `address`, `keyUid`, `walletType`, `path`, `publicKey`, `name`, `emoji`, `colorId` proc addNewSeedPhraseKeypair*(self: Service, seedPhrase, password: string, doPasswordHashing: bool, keyUid, keypairName, rootWalletMasterKey: string, accounts: seq[WalletAccountDto]): string = @@ -328,6 +355,24 @@ proc addNewSeedPhraseKeypair*(self: Service, seedPhrase, password: string, doPas error "error: ", procName="addNewSeedPhraseKeypair", errName=e.name, errDesription=e.msg return e.msg +proc makeSeedPhraseKeypairFullyOperable*(self: Service, keyUid, mnemonic, password: string, doPasswordHashing: bool): string = + if password.len == 0: + error "for making a private key keypair fully operable, password must be provided" + return + var finalPassword = password + if doPasswordHashing: + finalPassword = utils.hashPassword(password) + try: + var response = status_go_accounts.makeSeedPhraseKeypairFullyOperable(mnemonic, finalPassword) + if not response.error.isNil: + error "status-go error", procName="makeSeedPhraseKeypairFullyOperable", errCode=response.error.code, errDesription=response.error.message + return response.error.message + self.updateKeypairOperabilityInLocalStoreAndNotify(keyUid) + return "" + except Exception as e: + error "error: ", procName="makeSeedPhraseKeypairFullyOperable", errName=e.name, errDesription=e.msg + return e.msg + proc getRandomMnemonic*(self: Service): string = try: let response = status_go_accounts.getRandomMnemonic() @@ -591,3 +636,6 @@ proc getCurrencyFormat*(self: Service, symbol: string): CurrencyFormatDto = proc areTestNetworksEnabled*(self: Service): bool = return self.settingsService.areTestNetworksEnabled() + +proc hasPairedDevices*(self: Service): bool = + return hasPairedDevices() \ No newline at end of file diff --git a/src/app_service/service/wallet_account/signals_and_payloads.nim b/src/app_service/service/wallet_account/signals_and_payloads.nim index 4c027d10d0..209e008162 100644 --- a/src/app_service/service/wallet_account/signals_and_payloads.nim +++ b/src/app_service/service/wallet_account/signals_and_payloads.nim @@ -19,6 +19,7 @@ const SIGNAL_WALLET_ACCOUNT_PREFERRED_SHARING_CHAINS_UPDATED* = "walletAccount/p const SIGNAL_KEYPAIR_SYNCED* = "keypairSynced" const SIGNAL_KEYPAIR_NAME_CHANGED* = "keypairNameChanged" const SIGNAL_KEYPAIR_DELETED* = "keypairDeleted" +const SIGNAL_KEYPAIR_OPERABILITY_CHANGED* = "keypairOperabilityChanged" const SIGNAL_NEW_KEYCARD_SET* = "newKeycardSet" const SIGNAL_KEYCARD_REBUILD* = "keycardRebuild" diff --git a/src/backend/accounts.nim b/src/backend/accounts.nim index cc8e98cb01..cd49e14d88 100644 --- a/src/backend/accounts.nim +++ b/src/backend/accounts.nim @@ -238,6 +238,11 @@ proc importMnemonic*(mnemonic, password: string): let payload = %* [mnemonic, password] return core.callPrivateRPC("accounts_importMnemonic", payload) +proc makeSeedPhraseKeypairFullyOperable*(mnemonic, password: string): + RpcResponse[JsonNode] {.raises: [Exception].} = + let payload = %* [mnemonic, password] + return core.callPrivateRPC("accounts_makeSeedPhraseKeypairFullyOperable", payload) + proc createAccountFromMnemonicAndDeriveAccountsForPaths*(mnemonic: string, paths: seq[string]): RpcResponse[JsonNode] {.raises: [Exception].} = let payload = %* { "mnemonicPhrase": mnemonic, @@ -258,6 +263,11 @@ proc importPrivateKey*(privateKey, password: string): let payload = %* [privateKey, password] return core.callPrivateRPC("accounts_importPrivateKey", payload) +proc makePrivateKeyKeypairFullyOperable*(privateKey, password: string): + RpcResponse[JsonNode] {.raises: [Exception].} = + let payload = %* [privateKey, password] + return core.callPrivateRPC("accounts_makePrivateKeyKeypairFullyOperable", payload) + proc createAccountFromPrivateKey*(privateKey: string): RpcResponse[JsonNode] {.raises: [Exception].} = let payload = %* {"privateKey": privateKey} try: diff --git a/ui/app/AppLayouts/Profile/controls/WalletKeyPairDelegate.qml b/ui/app/AppLayouts/Profile/controls/WalletKeyPairDelegate.qml index f7f93c0e19..9104615e67 100644 --- a/ui/app/AppLayouts/Profile/controls/WalletKeyPairDelegate.qml +++ b/ui/app/AppLayouts/Profile/controls/WalletKeyPairDelegate.qml @@ -21,6 +21,8 @@ Rectangle { signal goToAccountView(var account) signal toggleIncludeWatchOnlyAccount() + signal runImportViaSeedPhraseFlow() + signal runImportViaPrivateKeyFlow() signal runRenameKeypairFlow() signal runRemoveKeypairFlow() @@ -80,6 +82,8 @@ Rectangle { menuLoader.active = false } keyPair: root.keyPair + onRunImportViaSeedPhraseFlow: root.runImportViaSeedPhraseFlow() + onRunImportViaPrivateKeyFlow: root.runImportViaPrivateKeyFlow() onRunRenameKeypairFlow: root.runRenameKeypairFlow() onRunRemoveKeypairFlow: root.runRemoveKeypairFlow() } diff --git a/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml b/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml index 28e233cf62..f916570285 100644 --- a/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml +++ b/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml @@ -10,6 +10,8 @@ StatusMenu { property var keyPair + signal runImportViaSeedPhraseFlow() + signal runImportViaPrivateKeyFlow() signal runRenameKeypairFlow() signal runRemoveKeypairFlow() @@ -56,15 +58,16 @@ StatusMenu { StatusAction { text: enabled? root.keyPair.pairType === Constants.keypair.type.privateKeyImport? qsTr("Import via entering private key") : qsTr("Import via entering seed phrase") : "" enabled: !!root.keyPair && + root.keyPair.operability === Constants.keypair.operability.nonOperable && (root.keyPair.pairType === Constants.keypair.type.seedImport || root.keyPair.pairType === Constants.keypair.type.privateKeyImport) icon.name: enabled? root.keyPair.pairType === Constants.keypair.type.privateKeyImport? "objects" : "key_pair_seed_phrase" : "" icon.color: Theme.palette.primaryColor1 onTriggered: { if (root.keyPair.pairType === Constants.keypair.type.privateKeyImport) - console.warn("TODO: run import via private key flow") + root.runImportViaPrivateKeyFlow() else - console.warn("TODO: run import via seed phrase flow") + root.runImportViaSeedPhraseFlow() } } @@ -95,4 +98,4 @@ StatusMenu { root.runRemoveKeypairFlow() } } -} \ No newline at end of file +} diff --git a/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml b/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml index acc8d48a3f..ee5da762a7 100644 --- a/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml +++ b/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml @@ -56,8 +56,7 @@ QtObject { } property WalletStore walletStore: WalletStore { - accountsModule: profileSectionModuleInst.walletAccountsModule - networksModule: profileSectionModuleInst.walletNetworksModule + walletModule: profileSectionModuleInst.walletModule } property KeycardStore keycardStore: KeycardStore { diff --git a/ui/app/AppLayouts/Profile/stores/WalletStore.qml b/ui/app/AppLayouts/Profile/stores/WalletStore.qml index 24c9312d3a..41ebb37336 100644 --- a/ui/app/AppLayouts/Profile/stores/WalletStore.qml +++ b/ui/app/AppLayouts/Profile/stores/WalletStore.qml @@ -5,8 +5,9 @@ import utils 1.0 QtObject { id: root - property var accountsModule - property var networksModule + property var walletModule + property var accountsModule: root.walletModule.accountsModule + property var networksModule: root.walletModule.networksModule property var accountSensitiveSettings: Global.appIsReady? localAccountSensitiveSettings : null @@ -58,9 +59,18 @@ QtObject { } function runAddAccountPopup() { + // TODO: + // - `runAddAccountPopup` should be part of `root.walletModule` + // - `AddAccountPopup {}` should be moved from `MainView` to `WalletView` + // - `Edit account` popup opened from the wallet settings should be the same as one opened from the wallet section + // - `walletSection` should not be used in the context of wallet settings walletSection.runAddAccountPopup(false) } + function runKeypairImportPopup(keyUid, importOption) { + root.walletModule.runKeypairImportPopup(keyUid, importOption) + } + function evaluateRpcEndPoint(url) { return networksModule.fetchChainIdForUrl(url) } diff --git a/ui/app/AppLayouts/Profile/views/WalletView.qml b/ui/app/AppLayouts/Profile/views/WalletView.qml index e8599ae580..37940fc122 100644 --- a/ui/app/AppLayouts/Profile/views/WalletView.qml +++ b/ui/app/AppLayouts/Profile/views/WalletView.qml @@ -11,6 +11,7 @@ import utils 1.0 import shared 1.0 import shared.panels 1.0 import shared.popups 1.0 +import shared.popups.keypairimport 1.0 import shared.status 1.0 import "../controls" @@ -252,5 +253,31 @@ SettingsContentBase { } onLoaded: removeKeypairPopup.item.open() } + + Connections { + target: root.walletStore.walletModule + + function onDisplayKeypairImportPopup() { + keypairImport.active = true + } + function onDestroyKeypairImportPopup() { + keypairImport.active = false + } + } + + + Loader { + id: keypairImport + active: false + asynchronous: true + + sourceComponent: KeypairImportPopup { + store.keypairImportModule: root.walletStore.walletModule.keypairImportModule + } + + onLoaded: { + keypairImport.item.open() + } + } } } diff --git a/ui/app/AppLayouts/Profile/views/wallet/MainView.qml b/ui/app/AppLayouts/Profile/views/wallet/MainView.qml index b220354eb1..80a51b4c33 100644 --- a/ui/app/AppLayouts/Profile/views/wallet/MainView.qml +++ b/ui/app/AppLayouts/Profile/views/wallet/MainView.qml @@ -6,6 +6,7 @@ import shared.status 1.0 import shared.panels 1.0 import StatusQ.Core.Theme 0.1 import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 import StatusQ.Components 0.1 import shared.popups 1.0 @@ -41,6 +42,10 @@ Column { } } + component Spacer: Item { + height: 8 + } + Loader { id: addAccount @@ -91,9 +96,42 @@ Column { Separator {} - Item { + Spacer { + visible: root.walletStore.walletModule.hasPairedDevices + width: parent.width + } + + Rectangle { + visible: root.walletStore.walletModule.hasPairedDevices + height: 102 + width: parent.width + color: Theme.palette.transparent + radius: 8 + border.width: 1 + border.color: Theme.palette.baseColor5 + + Column { + anchors.fill: parent + padding: 16 + spacing: 8 + + StatusBaseText { + text: qsTr("Import keypairs from this device to your other synced devices") + font.pixelSize: 15 + } + + StatusButton { + text: qsTr("Show encrypted QR of keypairs on device") + icon.name: "qr" + onClicked: { + console.warn("TODO: run generate qr code flow...") + } + } + } + } + + Spacer { width: parent.width - height: 8 } Column { @@ -112,6 +150,12 @@ Column { onToggleIncludeWatchOnlyAccount: walletStore.toggleIncludeWatchOnlyAccount() onRunRenameKeypairFlow: root.runRenameKeypairFlow(model) onRunRemoveKeypairFlow: root.runRemoveKeypairFlow(model) + onRunImportViaSeedPhraseFlow: { + root.walletStore.runKeypairImportPopup(model.keyPair.keyUid, Constants.keypairImportPopup.importOption.seedPhrase) + } + onRunImportViaPrivateKeyFlow: { + root.walletStore.runKeypairImportPopup(model.keyPair.keyUid, Constants.keypairImportPopup.importOption.privateKey) + } } } } diff --git a/ui/imports/shared/panels/EnterSeedPhrase.qml b/ui/imports/shared/panels/EnterSeedPhrase.qml index 5962eed4aa..41a872ca91 100644 --- a/ui/imports/shared/panels/EnterSeedPhrase.qml +++ b/ui/imports/shared/panels/EnterSeedPhrase.qml @@ -71,7 +71,7 @@ ColumnLayout { function pasteWords () { const clipboardText = globalUtils.getFromClipboard() // Split words separated by commas and or blank spaces (spaces, enters, tabs) - const words = clipboardText.split(/[, \s]+/) + const words = clipboardText.trim().split(/[, \s]+/) let index = d.tabs.indexOf(words.length) if (index === -1) { diff --git a/ui/imports/shared/popups/addaccount/AddAccountPopup.qml b/ui/imports/shared/popups/addaccount/AddAccountPopup.qml index 4e188fcfed..7f89196647 100644 --- a/ui/imports/shared/popups/addaccount/AddAccountPopup.qml +++ b/ui/imports/shared/popups/addaccount/AddAccountPopup.qml @@ -9,6 +9,7 @@ import utils 1.0 import "./stores" import "./states" +import "../common" StatusModal { id: root @@ -255,7 +256,7 @@ StatusModal { } onClicked: { - root.store.submitAddAccount(null) + root.store.submitPopup(null) } } ] diff --git a/ui/imports/shared/popups/addaccount/panels/DerivationPath.qml b/ui/imports/shared/popups/addaccount/panels/DerivationPath.qml index b283603266..01b73ec5c0 100644 --- a/ui/imports/shared/popups/addaccount/panels/DerivationPath.qml +++ b/ui/imports/shared/popups/addaccount/panels/DerivationPath.qml @@ -10,6 +10,7 @@ import StatusQ.Components 0.1 import utils 1.0 import "../stores" +import "../../common" GridLayout { id: root @@ -77,7 +78,7 @@ GridLayout { } onEditingFinished: { - root.store.submitAddAccount(null) + root.store.submitPopup(null) } input.rightComponent: StatusIcon { diff --git a/ui/imports/shared/popups/addaccount/panels/WatchOnlyAddressSection.qml b/ui/imports/shared/popups/addaccount/panels/WatchOnlyAddressSection.qml index 97fa5891a0..bbb9a898e5 100644 --- a/ui/imports/shared/popups/addaccount/panels/WatchOnlyAddressSection.qml +++ b/ui/imports/shared/popups/addaccount/panels/WatchOnlyAddressSection.qml @@ -9,6 +9,7 @@ import StatusQ.Controls.Validators 0.1 import utils 1.0 import "../stores" +import "../../common" Column { id: root @@ -53,7 +54,7 @@ Column { } onKeyPressed: { - root.store.submitAddAccount(event) + root.store.submitPopup(event) } } diff --git a/ui/imports/shared/popups/addaccount/states/EnterKeypairName.qml b/ui/imports/shared/popups/addaccount/states/EnterKeypairName.qml index 2a77ab29d0..40d96cf661 100644 --- a/ui/imports/shared/popups/addaccount/states/EnterKeypairName.qml +++ b/ui/imports/shared/popups/addaccount/states/EnterKeypairName.qml @@ -41,7 +41,7 @@ Item { } onKeyPressed: { - root.store.submitAddAccount(event) + root.store.submitPopup(event) } } diff --git a/ui/imports/shared/popups/addaccount/states/EnterSeedPhraseWord.qml b/ui/imports/shared/popups/addaccount/states/EnterSeedPhraseWord.qml index 797f7f9df1..18aa6e098e 100644 --- a/ui/imports/shared/popups/addaccount/states/EnterSeedPhraseWord.qml +++ b/ui/imports/shared/popups/addaccount/states/EnterSeedPhraseWord.qml @@ -151,7 +151,7 @@ Item { } onKeyPressed: { - root.store.submitAddAccount(event) + root.store.submitPopup(event) } } } diff --git a/ui/imports/shared/popups/addaccount/states/Main.qml b/ui/imports/shared/popups/addaccount/states/Main.qml index 6e3c69997f..3330274b49 100644 --- a/ui/imports/shared/popups/addaccount/states/Main.qml +++ b/ui/imports/shared/popups/addaccount/states/Main.qml @@ -12,6 +12,7 @@ import utils 1.0 import "../stores" import "../panels" +import "../../common" Item { id: root @@ -140,7 +141,7 @@ Item { } onKeyPressed: { - root.store.submitAddAccount(event) + root.store.submitPopup(event) } onValidChanged: { diff --git a/ui/imports/shared/popups/addaccount/stores/AddAccountStore.qml b/ui/imports/shared/popups/addaccount/stores/AddAccountStore.qml index 166b4e92bd..6f574a54b0 100644 --- a/ui/imports/shared/popups/addaccount/stores/AddAccountStore.qml +++ b/ui/imports/shared/popups/addaccount/stores/AddAccountStore.qml @@ -1,9 +1,12 @@ import QtQuick 2.13 import utils 1.0 -QtObject { +import "../../common" + +BasePopupStore { id: root + isAddAccountPopup: true required property var addAccountModule required property var emojiPopup @@ -19,13 +22,11 @@ QtObject { property var derivedAddressModel: root.addAccountModule.derivedAddressModel property var selectedDerivedAddress: root.addAccountModule.selectedDerivedAddress property var watchOnlyAccAddress: root.addAccountModule.watchOnlyAccAddress - property var privateKeyAccAddress: root.addAccountModule.privateKeyAccAddress + privateKeyAccAddress: root.addAccountModule.privateKeyAccAddress property bool editMode: root.addAccountModule.editMode property bool disablePopup: root.addAccountModule.disablePopup property bool accountNameIsValid: false - property bool enteredSeedPhraseIsValid: false - property bool enteredPrivateKeyIsValid: false property bool addingNewMasterKeyConfirmed: false property bool seedPhraseRevealed: false property bool seedPhraseWord1Valid: false @@ -82,7 +83,7 @@ QtObject { return root.addAccountModule.getStoredSelectedColorId() } - function submitAddAccount(event) { + submitPopup: function(event) { if (!root.primaryPopupButtonEnabled) { return } @@ -116,11 +117,11 @@ QtObject { root.addAccountModule.changeWatchOnlyAccountAddress("") } - readonly property var changePrivateKeyPostponed: Backpressure.debounce(root, 400, function (privateKey) { + changePrivateKeyPostponed: Backpressure.debounce(root, 400, function (privateKey) { root.addAccountModule.changePrivateKey(privateKey) }) - function cleanPrivateKey() { + cleanPrivateKey: function() { root.enteredPrivateKeyIsValid = false root.addAccountModule.newKeyPairName = "" root.addAccountModule.changePrivateKey("") @@ -152,11 +153,11 @@ QtObject { root.addAccountModule.startScanningForActivity() } - function validSeedPhrase(seedPhrase) { + validSeedPhrase: function(seedPhrase) { return root.addAccountModule.validSeedPhrase(seedPhrase) } - function changeSeedPhrase(seedPhrase) { + changeSeedPhrase: function(seedPhrase) { root.addAccountModule.changeSeedPhrase(seedPhrase) } @@ -186,10 +187,6 @@ QtObject { } } - function getFromClipboard() { - return globalUtils.getFromClipboard() - } - readonly property bool primaryPopupButtonEnabled: { if (!root.addAccountModule || !root.currentState || root.disablePopup) { return false diff --git a/ui/imports/shared/popups/addaccount/panels/AddressDetails.qml b/ui/imports/shared/popups/common/AddressDetails.qml similarity index 87% rename from ui/imports/shared/popups/addaccount/panels/AddressDetails.qml rename to ui/imports/shared/popups/common/AddressDetails.qml index fd27670f8e..29188651bb 100644 --- a/ui/imports/shared/popups/addaccount/panels/AddressDetails.qml +++ b/ui/imports/shared/popups/common/AddressDetails.qml @@ -12,6 +12,7 @@ Row { property var addressDetailsItem property bool defaultMessageCondition: true property string defaultMessage: "" + property bool alreadyCreatedAccountIsAnError: true StatusIcon { id: icon @@ -36,7 +37,7 @@ Row { if (!root.addressDetailsItem || !root.addressDetailsItem.loaded) { return qsTr("Scanning for activity...") } - if (root.addressDetailsItem.alreadyCreated) { + if (root.alreadyCreatedAccountIsAnError && root.addressDetailsItem.alreadyCreated) { return qsTr("Already added") } if (root.addressDetailsItem.hasActivity) { @@ -48,7 +49,7 @@ Row { if (root.defaultMessageCondition || !root.addressDetailsItem || !root.addressDetailsItem.loaded) { return Theme.palette.baseColor1 } - if (root.addressDetailsItem.alreadyCreated) { + if (root.alreadyCreatedAccountIsAnError && root.addressDetailsItem.alreadyCreated) { return Theme.palette.dangerColor1 } if (root.addressDetailsItem.hasActivity) { diff --git a/ui/imports/shared/popups/addaccount/panels/AddressWithAddressDetails.qml b/ui/imports/shared/popups/common/AddressWithAddressDetails.qml similarity index 90% rename from ui/imports/shared/popups/addaccount/panels/AddressWithAddressDetails.qml rename to ui/imports/shared/popups/common/AddressWithAddressDetails.qml index 4b81a0bf58..4a779093c7 100644 --- a/ui/imports/shared/popups/addaccount/panels/AddressWithAddressDetails.qml +++ b/ui/imports/shared/popups/common/AddressWithAddressDetails.qml @@ -17,6 +17,7 @@ Column { property bool addressResolved: true property bool displayDetails: true property bool displayCopyButton: true + property bool alreadyCreatedAccountIsAnError: true spacing: Style.current.halfPadding @@ -44,5 +45,6 @@ Column { addressDetailsItem: root.addressDetailsItem defaultMessage: "" defaultMessageCondition: !root.addressResolved + alreadyCreatedAccountIsAnError: root.alreadyCreatedAccountIsAnError } } diff --git a/ui/imports/shared/popups/common/BasePopupStore.qml b/ui/imports/shared/popups/common/BasePopupStore.qml new file mode 100644 index 0000000000..6ecd86172f --- /dev/null +++ b/ui/imports/shared/popups/common/BasePopupStore.qml @@ -0,0 +1,26 @@ +import QtQuick 2.13 +import utils 1.0 + +QtObject { + id: root + + // store properties + required property bool isAddAccountPopup + property bool enteredPrivateKeyIsValid: false + property bool enteredPrivateKeyMatchTheKeypair: true + property bool enteredSeedPhraseIsValid: false + + // backend properties + required property var privateKeyAccAddress + + // functions + property var changePrivateKeyPostponed: function(){} + property var cleanPrivateKey: function(){} + property var submitPopup: function(){} + property var changeSeedPhrase: function(){} + property var validSeedPhrase: function(){} + + function getFromClipboard() { + return globalUtils.getFromClipboard() + } +} diff --git a/ui/imports/shared/popups/addaccount/states/EnterPrivateKey.qml b/ui/imports/shared/popups/common/EnterPrivateKey.qml similarity index 82% rename from ui/imports/shared/popups/addaccount/states/EnterPrivateKey.qml rename to ui/imports/shared/popups/common/EnterPrivateKey.qml index d1f77ed8dc..ce2881cbaa 100644 --- a/ui/imports/shared/popups/addaccount/states/EnterPrivateKey.qml +++ b/ui/imports/shared/popups/common/EnterPrivateKey.qml @@ -11,13 +11,10 @@ import StatusQ.Popups 0.1 import utils 1.0 -import "../stores" -import "../panels" - Item { id: root - property AddAccountStore store + property BasePopupStore store QtObject { id: d @@ -38,8 +35,10 @@ Item { spacing: Style.current.halfPadding StatusBaseText { - text: qsTr("Private key") + width: parent.width + text: root.store.isAddAccountPopup? qsTr("Private key") : qsTr("Private key for %1 keypair").arg(root.store.selectedKeypair.name) font.pixelSize: Constants.addAccountPopup.labelFontSize1 + elide: Text.ElideRight } GridLayout { @@ -68,7 +67,7 @@ Item { } onPressed: { - root.store.submitAddAccount(event) + root.store.submitPopup(event) } StatusButton { @@ -97,11 +96,17 @@ Item { StatusBaseText { Layout.alignment: Qt.AlignRight - visible: privKeyInput.text !== "" && !root.store.enteredPrivateKeyIsValid + visible: privKeyInput.text !== "" && !(root.store.enteredPrivateKeyIsValid && root.store.enteredPrivateKeyMatchTheKeypair) wrapMode: Text.WordWrap font.pixelSize: 12 color: Theme.palette.dangerColor1 - text: qsTr("Private key invalid") + text: { + if (!root.store.enteredPrivateKeyIsValid) + return qsTr("Private key invalid") + if (!root.store.enteredPrivateKeyMatchTheKeypair) + return qsTr("This is not the correct private key") + return "" + } } } } @@ -136,17 +141,18 @@ Item { addressDetailsItem: root.store.privateKeyAccAddress addressResolved: d.addressResolved displayCopyButton: false + alreadyCreatedAccountIsAnError: root.store.isAddAccountPopup } StatusModalDivider { width: parent.width - visible: d.addressResolved + visible: root.store.isAddAccountPopup && d.addressResolved } Column { width: parent.width spacing: Style.current.halfPadding - visible: d.addressResolved + visible: root.store.isAddAccountPopup && d.addressResolved StatusInput { objectName: "AddAccountPopup-PrivateKeyName" @@ -154,9 +160,12 @@ Item { label: qsTr("Key name") charLimit: Constants.addAccountPopup.keyPairNameMaxLength placeholderText: qsTr("Enter a name") - text: root.store.addAccountModule.newKeyPairName + text: root.store.isAddAccountPopup? root.store.addAccountModule.newKeyPairName : "" onTextChanged: { + if (!root.store.isAddAccountPopup) { + return + } if (text.trim() == "") { root.store.addAccountModule.newKeyPairName = "" return @@ -165,7 +174,7 @@ Item { } onKeyPressed: { - root.store.submitAddAccount(event) + root.store.submitPopup(event) } } diff --git a/ui/imports/shared/popups/addaccount/states/EnterSeedPhrase.qml b/ui/imports/shared/popups/common/EnterSeedPhrase.qml similarity index 68% rename from ui/imports/shared/popups/addaccount/states/EnterSeedPhrase.qml rename to ui/imports/shared/popups/common/EnterSeedPhrase.qml index cfa4c5faaf..44b6035b3b 100644 --- a/ui/imports/shared/popups/addaccount/states/EnterSeedPhrase.qml +++ b/ui/imports/shared/popups/common/EnterSeedPhrase.qml @@ -12,13 +12,10 @@ import StatusQ.Popups 0.1 import utils 1.0 import shared.panels 1.0 as SharedPanels -import "../stores" -import "../panels" - Item { id: root - property AddAccountStore store + property BasePopupStore store Column { anchors.top: parent.top @@ -29,8 +26,10 @@ Item { StatusBaseText { - text: qsTr("Enter seed phrase") + width: parent.width + text: root.store.isAddAccountPopup? qsTr("Enter seed phrase") : qsTr("Enter seed phrase for %1 keypair").arg(root.store.selectedKeypair.name) font.pixelSize: Constants.addAccountPopup.labelFontSize1 + elide: Text.ElideRight } SharedPanels.EnterSeedPhrase { @@ -47,24 +46,28 @@ Item { } root.store.enteredSeedPhraseIsValid = valid if (!enterSeedPhrase.isSeedPhraseValid(seedPhrase)) { - enterSeedPhrase.setWrongSeedPhraseMessage(qsTr("The entered seed phrase is already added")) + let err = qsTr("The entered seed phrase is already added") + if (!root.store.isAddAccountPopup) { + err = qsTr("This is not the correct seed phrase for %1 key").arg(root.store.selectedKeypair.name) + } + enterSeedPhrase.setWrongSeedPhraseMessage(err) } } onSubmitSeedPhrase: { - root.store.submitAddAccount() + root.store.submitPopup() } } StatusModalDivider { width: parent.width - visible: root.store.enteredSeedPhraseIsValid + visible: root.store.isAddAccountPopup && root.store.enteredSeedPhraseIsValid } Column { width: parent.width spacing: Style.current.halfPadding - visible: root.store.enteredSeedPhraseIsValid + visible: root.store.isAddAccountPopup && root.store.enteredSeedPhraseIsValid StatusInput { objectName: "AddAccountPopup-ImportedSeedPhraseKeyName" @@ -72,9 +75,12 @@ Item { label: qsTr("Key name") charLimit: Constants.addAccountPopup.keyPairNameMaxLength placeholderText: qsTr("Enter a name") - text: root.store.addAccountModule.newKeyPairName + text: root.store.isAddAccountPopup? root.store.addAccountModule.newKeyPairName : "" onTextChanged: { + if (!root.store.isAddAccountPopup) { + return + } if (text.trim() == "") { root.store.addAccountModule.newKeyPairName = "" return @@ -83,7 +89,7 @@ Item { } onKeyPressed: { - root.store.submitAddAccount(event) + root.store.submitPopup(event) } } diff --git a/ui/imports/shared/popups/keypairimport/KeypairImportPopup.qml b/ui/imports/shared/popups/keypairimport/KeypairImportPopup.qml new file mode 100644 index 0000000000..8058a7753e --- /dev/null +++ b/ui/imports/shared/popups/keypairimport/KeypairImportPopup.qml @@ -0,0 +1,127 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 + +import StatusQ.Core 0.1 +import StatusQ.Popups 0.1 +import StatusQ.Controls 0.1 + +import utils 1.0 + +import "./stores" +import "../common" + +StatusModal { + id: root + + property KeypairImportStore store: KeypairImportStore { } + + width: Constants.keypairImportPopup.popupWidth + + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + headerSettings.title: { + return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name) + } + + onClosed: { + root.store.currentState.doCancelAction() + } + + StatusScrollView { + id: scrollView + + anchors.fill: parent + padding: 0 + contentWidth: availableWidth + + Item { + id: content + + implicitWidth: loader.implicitWidth + implicitHeight: loader.implicitHeight + width: scrollView.availableWidth + + Loader { + id: loader + width: parent.width + sourceComponent: { + switch (root.store.currentState.stateType) { + case Constants.keypairImportPopup.state.importPrivateKey: + return keypairImportPrivateKeyComponent + case Constants.keypairImportPopup.state.importSeedPhrase: + return keypairImportSeedPhraseComponent + } + + return undefined + } + + onLoaded: { + content.height = Qt.binding(function(){return item.height}) + } + } + + Component { + id: keypairImportPrivateKeyComponent + EnterPrivateKey { + height: Constants.keypairImportPopup.contentHeight + store: root.store + } + } + + Component { + id: keypairImportSeedPhraseComponent + EnterSeedPhrase { + height: Constants.keypairImportPopup.contentHeight + store: root.store + } + } + } + } + + leftButtons: [ + StatusBackButton { + id: backButton + visible: root.store.currentState.displayBackButton + height: Constants.keypairImportPopup.footerButtonsHeight + width: height + onClicked: { + root.store.currentState.doBackAction() + } + } + ] + + rightButtons: [ + StatusButton { + id: primaryButton + height: Constants.keypairImportPopup.footerButtonsHeight + text: { + switch (root.store.currentState.stateType) { + + case Constants.keypairImportPopup.state.importPrivateKey: + case Constants.keypairImportPopup.state.importSeedPhrase: + return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name) + } + + return "" + } + visible: text !== "" + enabled: root.store.primaryPopupButtonEnabled + + icon.name: { + if (root.store.userProfileUsingBiometricLogin) { + return "touch-id" + } + + if (root.store.userProfileIsKeycardUser) { + return "keycard" + } + + return "password" + } + + onClicked: { + root.store.submitPopup(null) + } + } + ] +} diff --git a/ui/imports/shared/popups/keypairimport/qmldir b/ui/imports/shared/popups/keypairimport/qmldir new file mode 100644 index 0000000000..87f7139900 --- /dev/null +++ b/ui/imports/shared/popups/keypairimport/qmldir @@ -0,0 +1 @@ +KeypairImportPopup 1.0 KeypairImportPopup.qml diff --git a/ui/imports/shared/popups/keypairimport/stores/KeypairImportStore.qml b/ui/imports/shared/popups/keypairimport/stores/KeypairImportStore.qml new file mode 100644 index 0000000000..7cdd9ed42b --- /dev/null +++ b/ui/imports/shared/popups/keypairimport/stores/KeypairImportStore.qml @@ -0,0 +1,68 @@ +import QtQuick 2.13 +import utils 1.0 + +import "../../common" + +BasePopupStore { + id: root + + isAddAccountPopup: false + required property var keypairImportModule + + property bool userProfileIsKeycardUser: userProfile.isKeycardUser + property bool userProfileUsingBiometricLogin: userProfile.usingBiometricLogin + + // Module Properties + property var currentState: root.keypairImportModule.currentState + property var selectedKeypair: root.keypairImportModule.selectedKeypair + enteredPrivateKeyMatchTheKeypair: root.keypairImportModule.enteredPrivateKeyMatchTheKeypair + privateKeyAccAddress: root.keypairImportModule.privateKeyAccAddress + + submitPopup: function(event) { + if (!root.primaryPopupButtonEnabled) { + return + } + + if(!event) { + root.currentState.doPrimaryAction() + } + else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + event.accepted = true + root.currentState.doPrimaryAction() + } + } + + changePrivateKeyPostponed: Backpressure.debounce(root, 400, function (privateKey) { + root.keypairImportModule.changePrivateKey(privateKey) + }) + + cleanPrivateKey: function() { + root.enteredPrivateKeyIsValid = false + root.keypairImportModule.changePrivateKey("") + } + + function validSeedPhrase(seedPhrase) { + return root.keypairImportModule.validSeedPhrase(seedPhrase) + } + + function changeSeedPhrase(seedPhrase) { + root.keypairImportModule.changeSeedPhrase(seedPhrase) + } + + readonly property bool primaryPopupButtonEnabled: { + if (root.currentState.stateType === Constants.keypairImportPopup.state.importPrivateKey) { + return root.enteredPrivateKeyIsValid && + root.enteredPrivateKeyMatchTheKeypair && + !!root.privateKeyAccAddress && + root.privateKeyAccAddress.loaded && + root.privateKeyAccAddress.alreadyCreated && + root.privateKeyAccAddress.address !== "" + } + + if (root.currentState.stateType === Constants.keypairImportPopup.state.importSeedPhrase) { + return root.enteredSeedPhraseIsValid + } + + return true + } +} diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 8bfc29dba6..8c7e0ba0dc 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -739,6 +739,23 @@ QtObject { } } + readonly property QtObject keypairImportPopup: QtObject { + readonly property int popupWidth: 480 + readonly property int contentHeight: 626 + readonly property int footerButtonsHeight: 44 + + readonly property QtObject importOption: QtObject { + readonly property int seedPhrase: 1 + readonly property int privateKey: 2 + } + + readonly property QtObject state: QtObject { + readonly property string noState: "NoState" + readonly property string importSeedPhrase: "ImportSeedPhrase" + readonly property string importPrivateKey: "ImportPrivateKey" + } + } + readonly property QtObject localPairingAction: QtObject { readonly property int actionUnknown: 0 readonly property int actionConnect: 1