From 77ca8761a674344cd4372091731675ced1af95e9 Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Mon, 5 Aug 2024 10:18:17 +0200 Subject: [PATCH] feat: some limits for wallet section Added limitations: - allowed adding of max 20 accounts - allowed adding of max 3 watch only accounts - allowed adding of max 5 key pairs (including the profile key pair) - allowed adding of max 20 saved addresses per mode Closes #15934 --- .../saved_addresses/controller.nim | 3 + .../saved_addresses/io_interface.nim | 3 + .../wallet_section/saved_addresses/module.nim | 5 +- .../wallet_section/saved_addresses/view.nim | 5 +- .../shared_modules/add_account/controller.nim | 11 ++- .../add_account/io_interface.nim | 9 +++ .../shared_modules/add_account/module.nim | 9 +++ .../shared_modules/add_account/view.nim | 11 ++- .../service/saved_address/service.nim | 11 ++- .../wallet_account/service_account.nim | 27 ++++++++ src/backend/accounts.nim | 12 ++++ src/backend/backend.nim | 6 ++ .../popups/AddEditSavedAddressPopup.qml | 34 +++++++++ ui/app/AppLayouts/Wallet/stores/RootStore.qml | 4 ++ .../popups/addaccount/AddAccountPopup.qml | 69 +++++++++++++++++++ .../shared/popups/addaccount/states/Main.qml | 13 ++++ .../addaccount/stores/AddAccountStore.qml | 24 +++++++ ui/imports/utils/Constants.qml | 14 ++++ 18 files changed, 265 insertions(+), 5 deletions(-) diff --git a/src/app/modules/main/wallet_section/saved_addresses/controller.nim b/src/app/modules/main/wallet_section/saved_addresses/controller.nim index 06502c47ea..383045c87b 100644 --- a/src/app/modules/main/wallet_section/saved_addresses/controller.nim +++ b/src/app/modules/main/wallet_section/saved_addresses/controller.nim @@ -52,3 +52,6 @@ proc createOrUpdateSavedAddress*(self: Controller, name: string, address: string proc deleteSavedAddress*(self: Controller, address: string) = self.savedAddressService.deleteSavedAddress(address) + +proc remainingCapacityForSavedAddresses*(self: Controller): int = + return self.savedAddressService.remainingCapacityForSavedAddresses() diff --git a/src/app/modules/main/wallet_section/saved_addresses/io_interface.nim b/src/app/modules/main/wallet_section/saved_addresses/io_interface.nim index c6650c8499..23ffff93c6 100644 --- a/src/app/modules/main/wallet_section/saved_addresses/io_interface.nim +++ b/src/app/modules/main/wallet_section/saved_addresses/io_interface.nim @@ -39,6 +39,9 @@ method savedAddressNameExists*(self: AccessInterface, name: string): bool {.base method getSavedAddressAsJson*(self: AccessInterface, address: string): string {.base.} = raise newException(ValueError, "No implementation available") +method remainingCapacityForSavedAddresses*(self: AccessInterface): int {.base.} = + raise newException(ValueError, "No implementation available") + type ## Abstract class (concept) which must be implemented by object/s used in this ## module. diff --git a/src/app/modules/main/wallet_section/saved_addresses/module.nim b/src/app/modules/main/wallet_section/saved_addresses/module.nim index 2202e8af94..bf3631876a 100644 --- a/src/app/modules/main/wallet_section/saved_addresses/module.nim +++ b/src/app/modules/main/wallet_section/saved_addresses/module.nim @@ -103,4 +103,7 @@ method getSavedAddressAsJson*(self: Module, address: string): string = "chainShortNames": saDto.chainShortNames, "isTest": saDto.isTest, } - return $jsonObj \ No newline at end of file + return $jsonObj + +method remainingCapacityForSavedAddresses*(self: Module): int = + return self.controller.remainingCapacityForSavedAddresses() \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/saved_addresses/view.nim b/src/app/modules/main/wallet_section/saved_addresses/view.nim index 8baaf3ef31..553f41a0b3 100644 --- a/src/app/modules/main/wallet_section/saved_addresses/view.nim +++ b/src/app/modules/main/wallet_section/saved_addresses/view.nim @@ -58,4 +58,7 @@ QtObject: return self.delegate.savedAddressNameExists(name) proc getSavedAddressAsJson*(self: View, address: string): string {.slot.} = - return self.delegate.getSavedAddressAsJson(address) \ No newline at end of file + return self.delegate.getSavedAddressAsJson(address) + + proc remainingCapacityForSavedAddresses*(self: View): int {.slot.} = + return self.delegate.remainingCapacityForSavedAddresses() \ No newline at end of file diff --git a/src/app/modules/shared_modules/add_account/controller.nim b/src/app/modules/shared_modules/add_account/controller.nim index 14747214fc..9d0ac9c259 100644 --- a/src/app/modules/shared_modules/add_account/controller.nim +++ b/src/app/modules/shared_modules/add_account/controller.nim @@ -253,4 +253,13 @@ proc resolveSuggestedPathForKeypair*(self: Controller, keyUid: string): string = return self.walletAccountService.resolveSuggestedPathForKeypair(keyUid) proc isChecksumValidForAddress*(self: Controller, address: string): bool = - return self.walletAccountService.isChecksumValidForAddress(address) \ No newline at end of file + return self.walletAccountService.isChecksumValidForAddress(address) + +proc remainingAccountCapacity*(self: Controller): int = + return self.walletAccountService.remainingAccountCapacity() + +proc remainingKeypairCapacity*(self: Controller): int = + return self.walletAccountService.remainingKeypairCapacity() + +proc remainingWatchOnlyAccountCapacity*(self: Controller): int = + return self.walletAccountService.remainingWatchOnlyAccountCapacity() \ No newline at end of file diff --git a/src/app/modules/shared_modules/add_account/io_interface.nim b/src/app/modules/shared_modules/add_account/io_interface.nim index 15c6352af0..723594d891 100644 --- a/src/app/modules/shared_modules/add_account/io_interface.nim +++ b/src/app/modules/shared_modules/add_account/io_interface.nim @@ -110,6 +110,15 @@ method savedAddressDeleted*(self: AccessInterface, address: string, errorMsg: st method isChecksumValidForAddress*(self: AccessInterface, address: string): bool {.base.} = raise newException(ValueError, "No implementation available") +method remainingAccountCapacity*(self: AccessInterface): int {.base.} = + raise newException(ValueError, "No implementation available") + +method remainingKeypairCapacity*(self: AccessInterface): int {.base.} = + raise newException(ValueError, "No implementation available") + +method remainingWatchOnlyAccountCapacity*(self: AccessInterface): int {.base.} = + raise newException(ValueError, "No implementation available") + type DelegateInterface* = concept c c.onAddAccountModuleLoaded() diff --git a/src/app/modules/shared_modules/add_account/module.nim b/src/app/modules/shared_modules/add_account/module.nim index d9b9f9c066..b9ed98e01f 100644 --- a/src/app/modules/shared_modules/add_account/module.nim +++ b/src/app/modules/shared_modules/add_account/module.nim @@ -739,4 +739,13 @@ method buildNewSeedPhraseKeypairAndAddItToOrigin*[T](self: Module[T]) = method isChecksumValidForAddress*[T](self: Module[T], address: string): bool = return self.controller.isChecksumValidForAddress(address) +method remainingAccountCapacity*[T](self: Module[T]): int = + return self.controller.remainingAccountCapacity() + +method remainingKeypairCapacity*[T](self: Module[T]): int = + return self.controller.remainingKeypairCapacity() + +method remainingWatchOnlyAccountCapacity*[T](self: Module[T]): int = + return self.controller.remainingWatchOnlyAccountCapacity() + {.pop.} diff --git a/src/app/modules/shared_modules/add_account/view.nim b/src/app/modules/shared_modules/add_account/view.nim index 3ccdb39f72..829d13367c 100644 --- a/src/app/modules/shared_modules/add_account/view.nim +++ b/src/app/modules/shared_modules/add_account/view.nim @@ -365,4 +365,13 @@ QtObject: self.setDisablePopup(false) proc isChecksumValidForAddress*(self: View, address: string): bool {.slot.} = - return self.delegate.isChecksumValidForAddress(address) \ No newline at end of file + return self.delegate.isChecksumValidForAddress(address) + + proc remainingAccountCapacity*(self: View): int {.slot.} = + return self.delegate.remainingAccountCapacity() + + proc remainingKeypairCapacity*(self: View): int {.slot.} = + return self.delegate.remainingKeypairCapacity() + + proc remainingWatchOnlyAccountCapacity*(self: View): int {.slot.} = + return self.delegate.remainingWatchOnlyAccountCapacity() \ No newline at end of file diff --git a/src/app_service/service/saved_address/service.nim b/src/app_service/service/saved_address/service.nim index 1be278b55a..621043a145 100644 --- a/src/app_service/service/saved_address/service.nim +++ b/src/app_service/service/saved_address/service.nim @@ -171,4 +171,13 @@ QtObject: except Exception as e: error "onDeleteSavedAddress", msg = e.msg arg.errorMsg = e.msg - self.updateAddresses(SIGNAL_SAVED_ADDRESS_DELETED, arg) \ No newline at end of file + self.updateAddresses(SIGNAL_SAVED_ADDRESS_DELETED, arg) + + proc remainingCapacityForSavedAddresses*(self: Service): int = + try: + let response = backend.remainingCapacityForSavedAddresses(self.areTestNetworksEnabled()) + if not response.error.isNil: + raise newException(CatchableError, response.error.message) + return response.result.getInt + except Exception as e: + error "error: ", procName="remainingCapacityForSavedAddresses", errName=e.name, errDesription=e.msg \ 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 76027df759..79c3dd4c5a 100644 --- a/src/app_service/service/wallet_account/service_account.nim +++ b/src/app_service/service/wallet_account/service_account.nim @@ -811,3 +811,30 @@ proc resolveSuggestedPathForKeypair*(self: Service, keyUid: string): string = return response.result.getStr except Exception as e: error "error: ", procName="resolveSuggestedPathForKeypair", errName=e.name, errDesription=e.msg + +proc remainingAccountCapacity*(self: Service): int = + try: + let response = status_go_accounts.remainingAccountCapacity() + if not response.error.isNil: + raise newException(CatchableError, response.error.message) + return response.result.getInt + except Exception as e: + error "error: ", procName="remainingAccountCapacity", errName=e.name, errDesription=e.msg + +proc remainingKeypairCapacity*(self: Service): int = + try: + let response = status_go_accounts.remainingKeypairCapacity() + if not response.error.isNil: + raise newException(CatchableError, response.error.message) + return response.result.getInt + except Exception as e: + error "error: ", procName="remainingKeypairCapacity", errName=e.name, errDesription=e.msg + +proc remainingWatchOnlyAccountCapacity*(self: Service): int = + try: + let response = status_go_accounts.remainingWatchOnlyAccountCapacity() + if not response.error.isNil: + raise newException(CatchableError, response.error.message) + return response.result.getInt + except Exception as e: + error "error: ", procName="remainingWatchOnlyAccountCapacity", errName=e.name, errDesription=e.msg \ No newline at end of file diff --git a/src/backend/accounts.nim b/src/backend/accounts.nim index a69b63d901..e693af6b99 100644 --- a/src/backend/accounts.nim +++ b/src/backend/accounts.nim @@ -420,3 +420,15 @@ proc getNumOfAddressesToGenerateForKeypair*(keyUID: string): RpcResponse[JsonNod proc resolveSuggestedPathForKeypair*(keyUID: string): RpcResponse[JsonNode] = let payload = %* [keyUID] result = core.callPrivateRPC("accounts_resolveSuggestedPathForKeypair", payload) + +proc remainingAccountCapacity*(): RpcResponse[JsonNode] = + let payload = %* [] + return core.callPrivateRPC("accounts_remainingAccountCapacity", payload) + +proc remainingKeypairCapacity*(): RpcResponse[JsonNode] = + let payload = %* [] + return core.callPrivateRPC("accounts_remainingKeypairCapacity", payload) + +proc remainingWatchOnlyAccountCapacity*(): RpcResponse[JsonNode] = + let payload = %* [] + return core.callPrivateRPC("accounts_remainingWatchOnlyAccountCapacity", payload) \ No newline at end of file diff --git a/src/backend/backend.nim b/src/backend/backend.nim index 13f5909564..fa85c5f49b 100644 --- a/src/backend/backend.nim +++ b/src/backend/backend.nim @@ -76,6 +76,12 @@ rpc(deleteSavedAddress, "wakuext"): rpc(getSavedAddresses, "wakuext"): discard +rpc(getSavedAddressesPerMode, "wakuext"): + isTest: bool + +rpc(remainingCapacityForSavedAddresses, "wakuext"): + isTest: bool + rpc(checkConnected, "wallet"): discard diff --git a/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml b/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml index 726abbb6ca..f015c12366 100644 --- a/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml +++ b/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml @@ -18,6 +18,7 @@ import StatusQ.Core.Backpressure 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 as StatusQUtils import StatusQ.Popups 0.1 +import StatusQ.Popups.Dialog 0.1 import SortFilterProxyModel 0.2 @@ -283,6 +284,11 @@ StatusModal { || event !== undefined && event.key !== Qt.Key_Return && event.key !== Qt.Key_Enter) return + if (!d.editMode && root.store.remainingCapacityForSavedAddresses() === 0) { + limitPopup.active = true + return + } + root.store.createOrUpdateSavedAddress(d.name, d.address, d.ens, d.colorId, d.chainShortNames) root.close() } @@ -365,6 +371,34 @@ StatusModal { spacing: Style.current.xlPadding + Loader { + id: limitPopup + active: false + asynchronous: true + + sourceComponent: StatusDialog { + width: root.width - 2*Style.current.padding + + title: Constants.walletConstants.maxNumberOfSavedAddressesTitle + + StatusBaseText { + anchors.fill: parent + text: Constants.walletConstants.maxNumberOfSavedAddressesContent + wrapMode: Text.WordWrap + } + + standardButtons: Dialog.Ok + + onClosed: { + limitPopup.active = false + } + } + + onLoaded: { + limitPopup.item.open() + } + } + StatusInput { id: nameInput implicitWidth: d.componentWidth diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index c6c615557f..23d7e92c84 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -369,6 +369,10 @@ QtObject { return walletSectionSavedAddresses.savedAddressNameExists(name) } + function remainingCapacityForSavedAddresses() { + return walletSectionSavedAddresses.remainingCapacityForSavedAddresses() + } + function toggleNetwork(chainId) { networksModule.toggleNetwork(chainId) } diff --git a/ui/imports/shared/popups/addaccount/AddAccountPopup.qml b/ui/imports/shared/popups/addaccount/AddAccountPopup.qml index 825934ef1a..1c33285c51 100644 --- a/ui/imports/shared/popups/addaccount/AddAccountPopup.qml +++ b/ui/imports/shared/popups/addaccount/AddAccountPopup.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts 1.15 import StatusQ.Core 0.1 import StatusQ.Popups 0.1 import StatusQ.Controls 0.1 +import StatusQ.Popups.Dialog 0.1 import utils 1.0 import shared.popups 1.0 @@ -18,6 +19,12 @@ StatusModal { property AddAccountStore store: AddAccountStore { } + enum LimitWarning { + Accounts, + Keypairs, + WatchOnlyAccounts + } + width: Constants.addAccountPopup.popupWidth closePolicy: root.store.disablePopup? Popup.NoAutoClose : Popup.CloseOnEscape | Popup.CloseOnPressOutside @@ -27,6 +34,8 @@ StatusModal { onOpened: { root.store.resetStoreValues() + + root.store.showLimitPopup.connect(limitPopup.showPopup) } onClosed: { @@ -55,6 +64,59 @@ StatusModal { implicitHeight: loader.implicitHeight width: scrollView.availableWidth + Loader { + id: limitPopup + active: false + asynchronous: true + + property string title + property string content + + function showPopup(warningType) { + if (warningType === AddAccountPopup.LimitWarning.Accounts) { + limitPopup.title = Constants.walletConstants.maxNumberOfAccountsTitle + limitPopup.content = Constants.walletConstants.maxNumberOfAccountsContent + } else if (warningType === AddAccountPopup.LimitWarning.Keypairs) { + limitPopup.title = Constants.walletConstants.maxNumberOfKeypairsTitle + limitPopup.content = Constants.walletConstants.maxNumberOfKeypairsContent + } else if (warningType === AddAccountPopup.LimitWarning.WatchOnlyAccounts) { + limitPopup.title = Constants.walletConstants.maxNumberOfWatchOnlyAccountsTitle + limitPopup.content = Constants.walletConstants.maxNumberOfSavedAddressesContent + } else { + console.error("unsupported warning type") + return + } + + limitPopup.active = true + } + + sourceComponent: StatusDialog { + width: root.width - 2*Style.current.padding + + property string contentText + + title: Constants.walletConstants.maxNumberOfAccountsTitle + + StatusBaseText { + anchors.fill: parent + text: contentText + wrapMode: Text.WordWrap + } + + standardButtons: Dialog.Ok + + onClosed: { + limitPopup.active = false + } + } + + onLoaded: { + limitPopup.item.title = limitPopup.title + limitPopup.item.contentText = limitPopup.content + limitPopup.item.open() + } + } + Loader { id: loader width: parent.width @@ -93,6 +155,13 @@ StatusModal { id: mainComponent Main { store: root.store + + onWatchOnlyAccountsLimitReached: { + limitPopup.showPopup(AddAccountPopup.LimitWarning.WatchOnlyAccounts) + } + onKeypairLimitReached: { + limitPopup.showPopup(AddAccountPopup.LimitWarning.Keypairs) + } } } diff --git a/ui/imports/shared/popups/addaccount/states/Main.qml b/ui/imports/shared/popups/addaccount/states/Main.qml index e58c1d4c86..bee68c89ac 100644 --- a/ui/imports/shared/popups/addaccount/states/Main.qml +++ b/ui/imports/shared/popups/addaccount/states/Main.qml @@ -21,6 +21,9 @@ Item { property AddAccountStore store + signal watchOnlyAccountsLimitReached() + signal keypairLimitReached() + implicitHeight: layout.implicitHeight Component.onCompleted: { @@ -194,7 +197,17 @@ Item { enabled: !root.store.editMode onOriginSelected: { + if (keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc) { + if (root.store.remainingWatchOnlyAccountCapacity() === 0) { + root.watchOnlyAccountsLimitReached() + return + } + } if (keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey) { + if (root.store.remainingKeypairCapacity() === 0) { + root.keypairLimitReached() + return + } root.store.currentState.doSecondaryAction() return } diff --git a/ui/imports/shared/popups/addaccount/stores/AddAccountStore.qml b/ui/imports/shared/popups/addaccount/stores/AddAccountStore.qml index aef4f09dfd..0b77b829fb 100644 --- a/ui/imports/shared/popups/addaccount/stores/AddAccountStore.qml +++ b/ui/imports/shared/popups/addaccount/stores/AddAccountStore.qml @@ -54,6 +54,8 @@ BasePopupStore { Constants.addAccountPopup.predefinedPaths.ethereumLedgerLive ] + signal showLimitPopup(int warningType) + function resetStoreValues() { root.enteredSeedPhraseIsValid = false root.enteredPrivateKeyIsValid = false @@ -91,10 +93,20 @@ BasePopupStore { } if(!event) { + if (!root.editMode && root.remainingAccountCapacity() === 0) { + root.showLimitPopup(0) + return + } + root.currentState.doPrimaryAction() } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { event.accepted = true + if (!root.editMode && root.remainingAccountCapacity() === 0) { + root.showLimitPopup(0) + return + } + root.currentState.doPrimaryAction() } } @@ -159,6 +171,18 @@ BasePopupStore { return root.addAccountModule.isChecksumValidForAddress(address) } + function remainingAccountCapacity() { + return root.addAccountModule.remainingAccountCapacity() + } + + function remainingKeypairCapacity() { + return root.addAccountModule.remainingKeypairCapacity() + } + + function remainingWatchOnlyAccountCapacity() { + return root.addAccountModule.remainingWatchOnlyAccountCapacity() + } + validSeedPhrase: function(seedPhrase) { return root.addAccountModule.validSeedPhrase(seedPhrase) } diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index f2a1070762..c28590db65 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -873,6 +873,20 @@ QtObject { readonly property string market: "market" } + readonly property QtObject walletConstants: QtObject { + readonly property string maxNumberOfAccountsTitle: qsTr("Limit of 20 accounts reached") + readonly property string maxNumberOfAccountsContent: qsTr("Remove any account to add a new one.") + + readonly property string maxNumberOfKeypairsTitle: qsTr("Limit of 5 key pairs reached") + readonly property string maxNumberOfKeypairsContent: qsTr("Remove key pair to add a new one.") + + readonly property string maxNumberOfWatchOnlyAccountsTitle: qsTr("Limit of 3 watched addresses reached") + readonly property string maxNumberOfWatchOnlyAccountsContent: qsTr("Remove a watched address to add a new one.") + + readonly property string maxNumberOfSavedAddressesTitle: qsTr("Limit of 20 saved addresses reached") + readonly property string maxNumberOfSavedAddressesContent: qsTr("Remove a saved address to add a new one.") + } + enum ConnectionStatus { Success = 0, Failure = 1,