From aec5dc62c9f232f257ea318e8437ab11d30a00d0 Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Wed, 23 Aug 2023 13:46:04 +0200 Subject: [PATCH] fix(@desktop/wallet): importing keypairs at the account level (navigating into an account page in Wallet) (point 9) Point 9 of #11968 --- src/app/modules/main/module.nim | 4 +- .../main/wallet_section/controller.nim | 6 + .../main/wallet_section/io_interface.nim | 15 +++ .../modules/main/wallet_section/module.nim | 106 ++++++++++++++---- src/app/modules/main/wallet_section/view.nim | 36 ++++++ .../wallet_account/service_account.nim | 6 + .../Profile/views/wallet/AccountView.qml | 40 +------ ui/app/AppLayouts/Wallet/WalletLayout.qml | 23 ++++ .../AppLayouts/Wallet/views/RightTabView.qml | 13 +++ ui/app/AppLayouts/stores/RootStore.qml | 1 + .../shared/controls/ImportKeypairInfo.qml | 55 +++++++++ ui/imports/shared/controls/qmldir | 1 + 12 files changed, 249 insertions(+), 57 deletions(-) create mode 100644 ui/imports/shared/controls/ImportKeypairInfo.qml diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index 917693443d..fbebba9a51 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -197,7 +197,7 @@ proc newModule*[T]( result, events, tokenService, currencyService, transactionService, walletAccountService, settingsService, savedAddressService, networkService, accountsService, - keycardService, nodeService, networkConnectionService + keycardService, nodeService, networkConnectionService, devicesService ) result.browserSectionModule = browser_section_module.newModule( result, events, bookmarkService, settingsService, networkService, @@ -1355,6 +1355,6 @@ method communityMembersRevealedAccountsLoaded*[T](self: Module[T], communityId: if revealedAccount.isAirdropAddress: communityMembersAirdropAddress[pubkey] = revealedAccount.address discard - + self.view.model.setMembersAirdropAddress(communityId, communityMembersAirdropAddress) diff --git a/src/app/modules/main/wallet_section/controller.nim b/src/app/modules/main/wallet_section/controller.nim index 11374a7df2..686270dc9c 100644 --- a/src/app/modules/main/wallet_section/controller.nim +++ b/src/app/modules/main/wallet_section/controller.nim @@ -68,3 +68,9 @@ proc toggleIncludeWatchOnlyAccount*(self: Controller) = proc isIncludeWatchOnlyAccount*(self: Controller): bool = return self.walletAccountService.isIncludeWatchOnlyAccount() + +proc getKeypairByAccountAddress*(self: Controller, address: string): KeypairDto = + return self.walletAccountService.getKeypairByAccountAddress(address) + +proc hasPairedDevices*(self: Controller): bool = + return self.walletAccountService.hasPairedDevices() \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/io_interface.nim b/src/app/modules/main/wallet_section/io_interface.nim index f606fe07fc..8031fb874f 100644 --- a/src/app/modules/main/wallet_section/io_interface.nim +++ b/src/app/modules/main/wallet_section/io_interface.nim @@ -99,3 +99,18 @@ method getLatestBlockNumber*(self: AccessInterface, chainId: int): string {.base method fetchDecodedTxData*(self: AccessInterface, txHash: string, data: string) {.base.} = raise newException(ValueError, "No implementation available") + +method runKeypairImportPopup*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +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 hasPairedDevices*(self: AccessInterface): bool {.base.} = + raise newException(ValueError, "No implementation available") \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/module.nim b/src/app/modules/main/wallet_section/module.nim index fc1ad606c1..c056d64359 100644 --- a/src/app/modules/main/wallet_section/module.nim +++ b/src/app/modules/main/wallet_section/module.nim @@ -1,4 +1,4 @@ -import NimQml, chronicles, sequtils, sugar +import NimQml, chronicles, sequtils, strutils, sugar import ./controller, ./view, ./filter import ./io_interface as io_interface @@ -12,25 +12,27 @@ import ./buy_sell_crypto/module as buy_sell_crypto_module import ./networks/module as networks_module import ./overview/module as overview_module import ./send/module as send_module -import ../../shared_modules/add_account/module as add_account_module import ./activity/controller as activityc import app/modules/shared_modules/collectibles/controller as collectiblesc import app/modules/shared_modules/collectible_details/controller as collectible_detailsc -import ../../../global/global_singleton -import ../../../core/eventemitter -import ../../../../app_service/service/keycard/service as keycard_service -import ../../../../app_service/service/token/service as token_service -import ../../../../app_service/service/currency/service as currency_service -import ../../../../app_service/service/transaction/service as transaction_service -import ../../../../app_service/service/wallet_account/service as wallet_account_service -import ../../../../app_service/service/settings/service as settings_service -import ../../../../app_service/service/saved_address/service as saved_address_service -import ../../../../app_service/service/network/service as network_service -import ../../../../app_service/service/accounts/service as accounts_service -import ../../../../app_service/service/node/service as node_service -import ../../../../app_service/service/network_connection/service as network_connection_service +import app/global/global_singleton +import app/core/eventemitter +import app/modules/shared_modules/add_account/module as add_account_module +import app/modules/shared_modules/keypair_import/module as keypair_import_module +import app_service/service/keycard/service as keycard_service +import app_service/service/token/service as token_service +import app_service/service/currency/service as currency_service +import app_service/service/transaction/service as transaction_service +import app_service/service/wallet_account/service as wallet_account_service +import app_service/service/settings/service as settings_service +import app_service/service/saved_address/service as saved_address_service +import app_service/service/network/service as network_service +import app_service/service/accounts/service as accounts_service +import app_service/service/node/service as node_service +import app_service/service/network_connection/service as network_connection_service +import app_service/service/devices/service as devices_service import backend/collectibles as backend_collectibles @@ -52,13 +54,16 @@ type view: View filter: Filter + # shared modules + addAccountModule: add_account_module.AccessInterface + keypairImportModule: keypair_import_module.AccessInterface + # modules accountsModule: accounts_module.AccessInterface allTokensModule: all_tokens_module.AccessInterface assetsModule: assets_module.AccessInterface sendModule: send_module.AccessInterface savedAddressesModule: saved_addresses_module.AccessInterface buySellCryptoModule: buy_sell_crypto_module.AccessInterface - addAccountModule: add_account_module.AccessInterface overviewModule: overview_module.AccessInterface networksModule: networks_module.AccessInterface networksService: network_service.Service @@ -66,6 +71,7 @@ type keycardService: keycard_service.Service accountsService: accounts_service.Service walletAccountService: wallet_account_service.Service + devicesService: devices_service.Service activityController: activityc.Controller collectiblesController: collectiblesc.Controller @@ -73,6 +79,10 @@ type # instance to be used in temporary, short-lived, workflows (e.g. send popup) tmpActivityController: activityc.Controller +## Forward declaration +proc onUpdatedKeypairsOperability*(self: Module, updatedKeypairs: seq[KeypairDto]) +proc onLocalPairingStatusUpdate*(self: Module, data: LocalPairingStatus) + proc newModule*( delegate: delegate_interface.AccessInterface, events: EventEmitter, @@ -86,7 +96,8 @@ proc newModule*( accountsService: accounts_service.Service, keycardService: keycard_service.Service, nodeService: node_service.Service, - networkConnectionService: network_connection_service.Service + networkConnectionService: network_connection_service.Service, + devicesService: devices_service.Service ): Module = result = Module() result.delegate = delegate @@ -94,6 +105,7 @@ proc newModule*( result.keycardService = keycardService result.accountsService = accountsService result.walletAccountService = walletAccountService + result.devicesService = devicesService result.moduleLoaded = false result.controller = newController(result, settingsService, walletAccountService, currencyService, networkService) @@ -132,6 +144,8 @@ method delete*(self: Module) = if not self.addAccountModule.isNil: self.addAccountModule.delete + if not self.keypairImportModule.isNil: + self.keypairImportModule.delete method updateCurrency*(self: Module, currency: string) = self.controller.updateCurrency(currency) @@ -167,10 +181,16 @@ method toggleWatchOnlyAccounts*(self: Module) = self.filter.toggleWatchOnlyAccounts() method setFilterAddress*(self: Module, address: string) = + let keypair = self.controller.getKeypairByAccountAddress(address) + if keypair.isNil: + self.view.setKeypairOperabilityForObservedAccount("") + else: + self.view.setKeypairOperabilityForObservedAccount(keypair.getOperability()) self.filter.setAddress(address) self.notifyFilterChanged() method setFillterAllAddresses*(self: Module) = + self.view.setKeypairOperabilityForObservedAccount("") self.filter.setFillterAllAddresses() self.notifyFilterChanged() @@ -189,8 +209,7 @@ method load*(self: Module) = self.events.on(SIGNAL_WALLET_ACCOUNT_SAVED) do(e:Args): let args = AccountArgs(e) self.setTotalCurrencyBalance() - self.filter.setAddress(args.account.address) - self.notifyFilterChanged() + self.setFilterAddress(args.account.address) self.events.on(SIGNAL_WALLET_ACCOUNT_DELETED) do(e:Args): let args = AccountArgs(e) self.setTotalCurrencyBalance() @@ -223,6 +242,14 @@ method load*(self: Module) = self.events.on(SIGNAL_TRANSACTION_DECODED) do(e: Args): let args = TransactionDecodedArgs(e) self.view.txDecoded(args.txHash, args.dataDecoded) + self.events.on(SIGNAL_IMPORTED_KEYPAIRS) do(e:Args): + let args = KeypairsArgs(e) + if args.error.len != 0: + return + self.onUpdatedKeypairsOperability(args.keypairs) + self.events.on(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE) do(e:Args): + let data = LocalPairingStatus(e) + self.onLocalPairingStatusUpdate(data) self.controller.init() self.view.load() @@ -344,3 +371,44 @@ method getLatestBlockNumber*(self: Module, chainId: int): string = method fetchDecodedTxData*(self: Module, txHash: string, data: string) = self.transactionService.fetchDecodedTxData(txHash, data) + +proc onUpdatedKeypairsOperability*(self: Module, updatedKeypairs: seq[KeypairDto]) = + if self.filter.addresses.len != 1: + return + for kp in updatedKeypairs: + for acc in kp.accounts: + if cmpIgnoreCase(acc.address, self.filter.addresses[0]) == 0: + self.view.setKeypairOperabilityForObservedAccount(kp.getOperability()) + return + +method destroyKeypairImportPopup*(self: Module) = + if self.keypairImportModule.isNil: + return + self.view.emitDestroyKeypairImportPopup() + self.keypairImportModule.delete + self.keypairImportModule = nil + +method runKeypairImportPopup*(self: Module) = + if self.filter.addresses.len != 1: + return + let keypair = self.controller.getKeypairByAccountAddress(self.filter.addresses[0]) + if keypair.isNil: + return + self.keypairImportModule = keypair_import_module.newModule(self, self.events, self.accountsService, + self.walletAccountService, self.devicesService) + self.keypairImportModule.load(keypair.keyUid, ImportKeypairModuleMode.SelectImportMethod) + +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() + +proc 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/wallet_section/view.nim b/src/app/modules/main/wallet_section/view.nim index aab1b47ce4..d50376b642 100644 --- a/src/app/modules/main/wallet_section/view.nim +++ b/src/app/modules/main/wallet_section/view.nim @@ -20,6 +20,7 @@ QtObject: collectiblesController: collectiblesc.Controller collectibleDetailsController: collectible_detailsc.Controller isNonArchivalNode: bool + keypairOperabilityForObservedAccount: string proc setup(self: View) = self.QObject.setup @@ -167,3 +168,38 @@ QtObject: notify = isNonArchivalNodeChanged proc txDecoded*(self: View, txHash: string, dataDecoded: string) {.signal.} + + 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 + + proc keypairOperabilityForObservedAccountChanged(self: View) {.signal.} + proc getKeypairOperabilityForObservedAccount(self: View): string {.slot.} = + return self.keypairOperabilityForObservedAccount + QtProperty[string] keypairOperabilityForObservedAccount: + read = getKeypairOperabilityForObservedAccount + notify = keypairOperabilityForObservedAccountChanged + proc setKeypairOperabilityForObservedAccount*(self: View, value: string) = + self.keypairOperabilityForObservedAccount = value + self.keypairOperabilityForObservedAccountChanged() + + proc runKeypairImportPopup*(self: View) {.slot.} = + self.delegate.runKeypairImportPopup() + + 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() \ No newline at end of file diff --git a/src/app_service/service/wallet_account/service_account.nim b/src/app_service/service/wallet_account/service_account.nim index 845d5db6e7..a8b5160e2e 100644 --- a/src/app_service/service/wallet_account/service_account.nim +++ b/src/app_service/service/wallet_account/service_account.nim @@ -30,6 +30,12 @@ proc getKeypairByKeyUid*(self: Service, keyUid: string): KeypairDto = return return self.keypairs[keyUid] +proc getKeypairByAccountAddress*(self: Service, address: string): KeypairDto = + for _, kp in self.keypairs: + for acc in kp.accounts: + if cmpIgnoreCase(acc.address, address) == 0: + return kp + proc getWatchOnlyAccounts*(self: Service): seq[WalletAccountDto] = return toSeq(self.watchOnlyAccounts.values) diff --git a/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml b/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml index 2b0bf2df2a..c330f9669d 100644 --- a/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml +++ b/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml @@ -11,6 +11,7 @@ import AppLayouts.Wallet 1.0 import AppLayouts.Wallet.controls 1.0 import AppLayouts.Profile.popups 1.0 +import shared.controls 1.0 import shared.popups 1.0 import shared.panels 1.0 import utils 1.0 @@ -79,50 +80,17 @@ ColumnLayout { } } - Rectangle { + ImportKeypairInfo { Layout.fillWidth: true Layout.topMargin: Style.current.bigPadding Layout.preferredHeight: childrenRect.height visible: !!root.keyPair && root.keyPair.operability === Constants.keypair.operability.nonOperable - radius: Style.current.radius - border.width: 1 - border.color: Theme.palette.directColor8 - color: Theme.palette.transparent - ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Style.current.padding - anchors.rightMargin: Style.current.padding - spacing: Style.current.halfPadding - - StatusBaseText { - Layout.fillWidth: true - Layout.topMargin: Style.current.padding - text: qsTr("Import keypair to use this account") - color: Theme.palette.warningColor1 - } - - StatusBaseText { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: qsTr("This account was added to one of your synced devices. To use this account you will first need import the associated keypair to this device.") - } - - StatusButton { - Layout.alignment: Qt.AlignLeft - Layout.bottomMargin: Style.current.padding - text: qsTr("Import keypair") - type: StatusBaseButton.Type.Warning - icon.name: "download" - onClicked: { - root.runImportMissingKeypairFlow() - } - } + onRunImport: { + root.runImportMissingKeypairFlow() } } - StatusBaseText { Layout.topMargin: Style.current.bigPadding text: qsTr("Account details") diff --git a/ui/app/AppLayouts/Wallet/WalletLayout.qml b/ui/app/AppLayouts/Wallet/WalletLayout.qml index 69f00e8842..04f4175677 100644 --- a/ui/app/AppLayouts/Wallet/WalletLayout.qml +++ b/ui/app/AppLayouts/Wallet/WalletLayout.qml @@ -6,6 +6,7 @@ import StatusQ.Layout 0.1 import utils 1.0 import shared.controls 1.0 +import shared.popups.keypairimport 1.0 import "popups" import "panels" @@ -32,6 +33,14 @@ Item { function onFilterChanged(address, includeWatchOnly, allAddresses) { root.showAllAccounts = allAddresses } + + function onDisplayKeypairImportPopup() { + keypairImport.active = true + } + + function onDestroyKeypairImportPopup() { + keypairImport.active = false + } } function showSigningPhrasePopup(){ @@ -150,4 +159,18 @@ Item { anchors.centerIn: parent } } + + Loader { + id: keypairImport + active: false + asynchronous: true + + sourceComponent: KeypairImportPopup { + store.keypairImportModule: root.store.walletSectionInst.keypairImportModule + } + + onLoaded: { + keypairImport.item.open() + } + } } diff --git a/ui/app/AppLayouts/Wallet/views/RightTabView.qml b/ui/app/AppLayouts/Wallet/views/RightTabView.qml index ee98c3f0df..354d78c08a 100644 --- a/ui/app/AppLayouts/Wallet/views/RightTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/RightTabView.qml @@ -5,6 +5,7 @@ import StatusQ.Core 0.1 import StatusQ.Controls 0.1 import utils 1.0 +import shared.controls 1.0 import shared.views 1.0 import shared.stores 1.0 @@ -76,6 +77,18 @@ Item { onLaunchShareAddressModal: root.launchShareAddressModal() onSwitchHideWatchOnlyAccounts: RootStore.toggleWatchOnlyAccounts() } + + ImportKeypairInfo { + Layout.fillWidth: true + Layout.topMargin: Style.current.bigPadding + Layout.preferredHeight: childrenRect.height + visible: root.store.walletSectionInst.hasPairedDevices && root.store.walletSectionInst.keypairOperabilityForObservedAccount === Constants.keypair.operability.nonOperable + + onRunImport: { + root.store.walletSectionInst.runKeypairImportPopup() + } + } + StatusTabBar { id: walletTabBar objectName: "rightSideWalletTabBar" diff --git a/ui/app/AppLayouts/stores/RootStore.qml b/ui/app/AppLayouts/stores/RootStore.qml index be5163bb7c..c8b50b1fe4 100644 --- a/ui/app/AppLayouts/stores/RootStore.qml +++ b/ui/app/AppLayouts/stores/RootStore.qml @@ -11,6 +11,7 @@ QtObject { id: root property var mainModuleInst: mainModule + property var walletSectionInst: walletSection property var aboutModuleInst: aboutModule property var communitiesModuleInst: communitiesModule property bool newVersionAvailable: false diff --git a/ui/imports/shared/controls/ImportKeypairInfo.qml b/ui/imports/shared/controls/ImportKeypairInfo.qml new file mode 100644 index 0000000000..ba33a8458a --- /dev/null +++ b/ui/imports/shared/controls/ImportKeypairInfo.qml @@ -0,0 +1,55 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +import utils 1.0 + +Rectangle { + id: root + + property string title: qsTr("Import keypair to use this account") + property string info: qsTr("This account was added to one of your synced devices. To use this account you will first need import the associated keypair to this device.") + property string buttonName: qsTr("Import keypair") + + signal runImport() + + radius: Style.current.radius + border.width: 1 + border.color: Theme.palette.directColor8 + color: Theme.palette.transparent + + ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Style.current.padding + anchors.rightMargin: Style.current.padding + spacing: Style.current.halfPadding + + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: Style.current.padding + text: root.title + color: Theme.palette.warningColor1 + } + + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: root.info + } + + StatusButton { + Layout.alignment: Qt.AlignLeft + Layout.bottomMargin: Style.current.padding + text: root.buttonName + type: StatusBaseButton.Type.Warning + icon.name: "download" + onClicked: { + root.runImport() + } + } + } +} diff --git a/ui/imports/shared/controls/qmldir b/ui/imports/shared/controls/qmldir index ec685c6537..80274259c5 100644 --- a/ui/imports/shared/controls/qmldir +++ b/ui/imports/shared/controls/qmldir @@ -45,3 +45,4 @@ ErrorDetails 1.0 ErrorDetails.qml CopyButton 1.0 CopyButton.qml DisabledTooltipButton 1.0 DisabledTooltipButton.qml WalletAccountListItem 1.0 WalletAccountListItem.qml +ImportKeypairInfo 1.0 ImportKeypairInfo.qml