From feaa91d062e751acee2546356b7236c1e924aa88 Mon Sep 17 00:00:00 2001 From: Khushboo Mehta Date: Thu, 31 Mar 2022 13:46:25 +0200 Subject: [PATCH] feat(@desktop/wallet): Add derivation path to wallet account generation fixes #5074 --- .../wallet_section/accounts/controller.nim | 16 +- .../accounts/derived_address_item.nim | 33 ++ .../accounts/derived_address_model.nim | 94 +++++ .../accounts/generated_wallet_item.nim | 40 ++ .../accounts/generated_wallet_model.nim | 76 ++++ .../wallet_section/accounts/io_interface.nim | 12 +- .../main/wallet_section/accounts/item.nim | 11 +- .../main/wallet_section/accounts/model.nim | 10 +- .../main/wallet_section/accounts/module.nim | 20 +- .../main/wallet_section/accounts/view.nim | 99 ++++- src/app_service/service/accounts/service.nim | 15 +- .../wallet_account/derived_address.nim | 14 + .../service/wallet_account/dto.nim | 8 +- .../service/wallet_account/service.nim | 42 +- src/backend/accounts.nim | 8 + src/backend/backend.nim | 9 +- .../Onboarding/views/SeedPhraseInputView.qml | 1 + .../Wallet/panels/DerivationPathsPanel.qml | 58 +++ .../Wallet/panels/DerivedAddressesPanel.qml | 153 +++++++ .../Wallet/panels/ImportPrivateKeyPanel.qml | 69 ++++ .../Wallet/panels/ImportSeedPhrasePanel.qml | 210 ++++++++++ .../Wallet/panels/SelectGeneratedAccount.qml | 136 ++++++ .../Wallet/popups/AddAccountModal.qml | 75 ++-- .../Wallet/stores/DerivationPathModel.qml | 37 ++ ui/app/AppLayouts/Wallet/stores/RootStore.qml | 39 +- ui/app/AppLayouts/Wallet/stores/qmldir | 1 + .../Wallet/views/AdvancedAddAccountView.qml | 386 ++++-------------- .../shared}/stores/BIP39_en.qml | 0 .../shared}/stores/english.txt | 0 ui/imports/shared/stores/qmldir | 1 + vendor/status-go | 2 +- 31 files changed, 1306 insertions(+), 369 deletions(-) create mode 100644 src/app/modules/main/wallet_section/accounts/derived_address_item.nim create mode 100644 src/app/modules/main/wallet_section/accounts/derived_address_model.nim create mode 100644 src/app/modules/main/wallet_section/accounts/generated_wallet_item.nim create mode 100644 src/app/modules/main/wallet_section/accounts/generated_wallet_model.nim create mode 100644 src/app_service/service/wallet_account/derived_address.nim create mode 100644 ui/app/AppLayouts/Wallet/panels/DerivationPathsPanel.qml create mode 100644 ui/app/AppLayouts/Wallet/panels/DerivedAddressesPanel.qml create mode 100644 ui/app/AppLayouts/Wallet/panels/ImportPrivateKeyPanel.qml create mode 100644 ui/app/AppLayouts/Wallet/panels/ImportSeedPhrasePanel.qml create mode 100644 ui/app/AppLayouts/Wallet/panels/SelectGeneratedAccount.qml create mode 100644 ui/app/AppLayouts/Wallet/stores/DerivationPathModel.qml rename ui/{app/AppLayouts/Onboarding => imports/shared}/stores/BIP39_en.qml (100%) rename ui/{app/AppLayouts/Onboarding => imports/shared}/stores/english.txt (100%) diff --git a/src/app/modules/main/wallet_section/accounts/controller.nim b/src/app/modules/main/wallet_section/accounts/controller.nim index 5649232109..b4438e9c93 100644 --- a/src/app/modules/main/wallet_section/accounts/controller.nim +++ b/src/app/modules/main/wallet_section/accounts/controller.nim @@ -23,17 +23,25 @@ proc init*(self: Controller) = proc getWalletAccounts*(self: Controller): seq[wallet_account_service.WalletAccountDto] = return self.walletAccountService.getWalletAccounts() -proc generateNewAccount*(self: Controller, password: string, accountName: string, color: string, emoji: string): string = - return self.walletAccountService.generateNewAccount(password, accountName, color, emoji) +proc generateNewAccount*(self: Controller, password: string, accountName: string, color: string, emoji: string, path: string, derivedFrom: string): string = + return self.walletAccountService.generateNewAccount(password, accountName, color, emoji, path, derivedFrom) proc addAccountsFromPrivateKey*(self: Controller, privateKey: string, password: string, accountName: string, color: string, emoji: string): string = return self.walletAccountService.addAccountsFromPrivateKey(privateKey, password, accountName, color, emoji) -proc addAccountsFromSeed*(self: Controller, seedPhrase: string, password: string, accountName: string, color: string, emoji: string): string = - return self.walletAccountService.addAccountsFromSeed(seedPhrase, password, accountName, color, emoji) +proc addAccountsFromSeed*(self: Controller, seedPhrase: string, password: string, accountName: string, color: string, emoji: string, path: string): string = + return self.walletAccountService.addAccountsFromSeed(seedPhrase, password, accountName, color, emoji, path) proc addWatchOnlyAccount*(self: Controller, address: string, accountName: string, color: string, emoji: string): string = return self.walletAccountService.addWatchOnlyAccount(address, accountName, color, emoji) proc deleteAccount*(self: Controller, address: string) = self.walletAccountService.deleteAccount(address) + +method getDerivedAddressList*(self: Controller, password: string, derivedFrom: string, path: string, pageSize: int, pageNumber: int): (seq[wallet_account_service.DerivedAddressDto], string) = + return self.walletAccountService.getDerivedAddressList(password, derivedFrom, path, pageSize, pageNumber) + +method getDerivedAddressListForMnemonic*(self: Controller, mnemonic: string, path: string, pageSize: int, pageNumber: int): (seq[wallet_account_service.DerivedAddressDto], string) = + return self.walletAccountService.getDerivedAddressListForMnemonic(mnemonic, path, pageSize, pageNumber) + + diff --git a/src/app/modules/main/wallet_section/accounts/derived_address_item.nim b/src/app/modules/main/wallet_section/accounts/derived_address_item.nim new file mode 100644 index 0000000000..7cf19c5460 --- /dev/null +++ b/src/app/modules/main/wallet_section/accounts/derived_address_item.nim @@ -0,0 +1,33 @@ +import strformat + +type + DerivedAddressItem* = object + address: string + path: string + hasActivity: bool + +proc initDerivedAddressItem*( + address: string, + path: string, + hasActivity: bool +): DerivedAddressItem = + result.address = address + result.path = path + result.hasActivity = hasActivity + +proc `$`*(self: DerivedAddressItem): string = + result = fmt"""DerivedAddressItem( + address: {self.address}, + path: {self.path}, + hasActivity: {self.hasActivity} + ]""" + +proc getAddress*(self: DerivedAddressItem): string = + return self.address + +proc getPath*(self: DerivedAddressItem): string = + return self.path + +proc getHasActivity*(self: DerivedAddressItem): bool = + return self.hasActivity + diff --git a/src/app/modules/main/wallet_section/accounts/derived_address_model.nim b/src/app/modules/main/wallet_section/accounts/derived_address_model.nim new file mode 100644 index 0000000000..ecd6cd90ca --- /dev/null +++ b/src/app/modules/main/wallet_section/accounts/derived_address_model.nim @@ -0,0 +1,94 @@ +import NimQml, Tables, strutils, strformat + +import ./derived_address_item + +type + ModelRole {.pure.} = enum + Address = UserRole + 1, + Path, + HasActivity, + +QtObject: + type + DerivedAddressModel* = ref object of QAbstractListModel + derivedWalletAddresses: seq[DerivedAddressItem] + + proc delete(self: DerivedAddressModel) = + self.derivedWalletAddresses = @[] + self.QAbstractListModel.delete + + proc setup(self: DerivedAddressModel) = + self.QAbstractListModel.setup + + proc newDerivedAddressModel*(): DerivedAddressModel = + new(result, delete) + result.setup + + proc `$`*(self: DerivedAddressModel): string = + for i in 0 ..< self.derivedWalletAddresses.len: + result &= fmt"""[{i}]:({$self.derivedWalletAddresses[i]})""" + + proc countChanged(self: DerivedAddressModel) {.signal.} + + proc getCount(self: DerivedAddressModel): int {.slot.} = + self.derivedWalletAddresses.len + + QtProperty[int] count: + read = getCount + notify = countChanged + + method rowCount(self: DerivedAddressModel, index: QModelIndex = nil): int = + return self.derivedWalletAddresses.len + + method roleNames(self: DerivedAddressModel): Table[int, string] = + { + ModelRole.Address.int: "address", + ModelRole.Path.int: "path", + ModelRole.HasActivity.int: "hasActivity", + }.toTable + + method data(self: DerivedAddressModel, index: QModelIndex, role: int): QVariant = + if (not index.isValid): + return + + if (index.row < 0 or index.row >= self.derivedWalletAddresses.len): + return + + let item = self.derivedWalletAddresses[index.row] + let enumRole = role.ModelRole + + case enumRole: + of ModelRole.Address: + result = newQVariant(item.getAddress()) + of ModelRole.Path: + result = newQVariant(item.getPath()) + of ModelRole.HasActivity: + result = newQVariant(item.getHasActivity()) + + proc setItems*(self: DerivedAddressModel, items: seq[DerivedAddressItem]) = + self.beginResetModel() + self.derivedWalletAddresses = items + self.endResetModel() + self.countChanged() + + proc getDerivedAddressAtIndex*(self: DerivedAddressModel, index: int): string = + if (index < 0 or index > self.getCount()): + return + let item = self.derivedWalletAddresses[index] + result = item.getAddress() + + + proc getDerivedAddressPathAtIndex*(self: DerivedAddressModel, index: int): string = + if (index < 0 or index > self.getCount()): + return + let item = self.derivedWalletAddresses[index] + result = item.getPath() + + + proc getDerivedAddressHasActivityAtIndex*(self: DerivedAddressModel, index: int): bool = + if (index < 0 or index > self.getCount()): + return + let item = self.derivedWalletAddresses[index] + result = item.getHasActivity() + + diff --git a/src/app/modules/main/wallet_section/accounts/generated_wallet_item.nim b/src/app/modules/main/wallet_section/accounts/generated_wallet_item.nim new file mode 100644 index 0000000000..e9ef499a04 --- /dev/null +++ b/src/app/modules/main/wallet_section/accounts/generated_wallet_item.nim @@ -0,0 +1,40 @@ +import strformat +import ./model + +type + GeneratedWalletItem* = object + name: string + iconName: string + generatedModel: Model + derivedfrom: string + +proc initGeneratedWalletItem*( + name: string, + iconName: string, + generatedModel: Model, + derivedfrom: string +): GeneratedWalletItem = + result.name = name + result.iconName = iconName + result.generatedModel = generatedModel + result.derivedfrom = derivedfrom + +proc `$`*(self: GeneratedWalletItem): string = + result = fmt"""GeneratedWalletItem( + name: {self.name}, + iconName: {self.iconName}, + generatedModel: {self.generatedModel}, + derivedfrom: {self.derivedfrom} + ]""" + +proc getName*(self: GeneratedWalletItem): string = + return self.name + +proc getIconName*(self: GeneratedWalletItem): string = + return self.iconName + +proc getGeneratedModel*(self: GeneratedWalletItem): Model = + return self.generatedModel + +proc getDerivedfrom*(self: GeneratedWalletItem): string = + return self.derivedfrom diff --git a/src/app/modules/main/wallet_section/accounts/generated_wallet_model.nim b/src/app/modules/main/wallet_section/accounts/generated_wallet_model.nim new file mode 100644 index 0000000000..fa81e0129e --- /dev/null +++ b/src/app/modules/main/wallet_section/accounts/generated_wallet_model.nim @@ -0,0 +1,76 @@ +import NimQml, Tables, strutils, strformat + +import ./generated_wallet_item + +type + ModelRole {.pure.} = enum + Name = UserRole + 1, + IconName, + GeneratedModel, + DerivedFrom + +QtObject: + type + GeneratedWalletModel* = ref object of QAbstractListModel + generatedWalletItems: seq[GeneratedWalletItem] + + proc delete(self: GeneratedWalletModel) = + self.generatedWalletItems = @[] + self.QAbstractListModel.delete + + proc setup(self: GeneratedWalletModel) = + self.QAbstractListModel.setup + + proc newGeneratedWalletModel*(): GeneratedWalletModel = + new(result, delete) + result.setup + + proc `$`*(self: GeneratedWalletModel): string = + for i in 0 ..< self.generatedWalletItems.len: + result &= fmt"""[{i}]:({$self.generatedWalletItems[i]})""" + + proc countChanged(self: GeneratedWalletModel) {.signal.} + + proc getCount(self: GeneratedWalletModel): int {.slot.} = + self.generatedWalletItems.len + + QtProperty[int] count: + read = getCount + notify = countChanged + + method rowCount(self: GeneratedWalletModel, index: QModelIndex = nil): int = + return self.generatedWalletItems.len + + method roleNames(self: GeneratedWalletModel): Table[int, string] = + { + ModelRole.Name.int: "name", + ModelRole.IconName.int: "iconName", + ModelRole.GeneratedModel.int: "generatedModel", + ModelRole.DerivedFrom.int: "derivedfrom", + }.toTable + + method data(self: GeneratedWalletModel, index: QModelIndex, role: int): QVariant = + if (not index.isValid): + return + + if (index.row < 0 or index.row >= self.generatedWalletItems.len): + return + + let item = self.generatedWalletItems[index.row] + let enumRole = role.ModelRole + + case enumRole: + of ModelRole.Name: + result = newQVariant(item.getName()) + of ModelRole.IconName: + result = newQVariant(item.getIconName()) + of ModelRole.GeneratedModel: + result = newQVariant(item.getGeneratedModel()) + of ModelRole.DerivedFrom: + result = newQVariant(item.getDerivedFrom()) + + proc setItems*(self: GeneratedWalletModel, items: seq[GeneratedWalletItem]) = + self.beginResetModel() + self.generatedWalletItems = items + self.endResetModel() + self.countChanged() diff --git a/src/app/modules/main/wallet_section/accounts/io_interface.nim b/src/app/modules/main/wallet_section/accounts/io_interface.nim index 57217dc98b..ab1df3a4cf 100644 --- a/src/app/modules/main/wallet_section/accounts/io_interface.nim +++ b/src/app/modules/main/wallet_section/accounts/io_interface.nim @@ -1,3 +1,5 @@ +import ../../../../../app_service/service/wallet_account/service as wallet_account_service + type AccessInterface* {.pure inheritable.} = ref object of RootObj ## Abstract class for any input/interaction with this module. @@ -11,13 +13,13 @@ method load*(self: AccessInterface) {.base.} = method isLoaded*(self: AccessInterface): bool {.base.} = raise newException(ValueError, "No implementation available") -method generateNewAccount*(self: AccessInterface, password: string, accountName: string, color: string, emoji: string): string {.base.} = +method generateNewAccount*(self: AccessInterface, password: string, accountName: string, color: string, emoji: string, path: string, derivedFrom: string): string {.base.} = raise newException(ValueError, "No implementation available") method addAccountsFromPrivateKey*(self: AccessInterface, privateKey: string, password: string, accountName: string, color: string, emoji: string): string {.base.} = raise newException(ValueError, "No implementation available") -method addAccountsFromSeed*(self: AccessInterface, seedPhrase: string, password: string, accountName: string, color: string, emoji: string): string {.base.} = +method addAccountsFromSeed*(self: AccessInterface, seedPhrase: string, password: string, accountName: string, color: string, emoji: string, path: string): string {.base.} = raise newException(ValueError, "No implementation available") method addWatchOnlyAccount*(self: AccessInterface, address: string, accountName: string, color: string, emoji: string): string {.base.} = @@ -29,6 +31,12 @@ method deleteAccount*(self: AccessInterface, address: string) {.base.} = method refreshWalletAccounts*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") +method getDerivedAddressList*(self: AccessInterface, password: string, derivedFrom: string, path: string, pageSize: int, pageNumber: int): (seq[wallet_account_service.DerivedAddressDto], string) {.base.} = + raise newException(ValueError, "No implementation available") + +method getDerivedAddressListForMnemonic*(self: AccessInterface, mnemonic: string, path: string, pageSize: int, pageNumber: int): (seq[wallet_account_service.DerivedAddressDto], string) {.base.} = + raise newException(ValueError, "No implementation available") + # 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. diff --git a/src/app/modules/main/wallet_section/accounts/item.nim b/src/app/modules/main/wallet_section/accounts/item.nim index 8604d986fb..58291c5a23 100644 --- a/src/app/modules/main/wallet_section/accounts/item.nim +++ b/src/app/modules/main/wallet_section/accounts/item.nim @@ -14,6 +14,7 @@ type currencyBalance: float64 assets: token_model.Model emoji: string + derivedfrom: string proc initItem*( name: string, @@ -26,7 +27,8 @@ proc initItem*( isChat: bool, currencyBalance: float64, assets: token_model.Model, - emoji: string + emoji: string, + derivedfrom: string ): Item = result.name = name result.address = address @@ -39,6 +41,7 @@ proc initItem*( result.currencyBalance = currencyBalance result.assets = assets result.emoji = emoji + result.derivedfrom = derivedfrom proc `$`*(self: Item): string = result = fmt"""WalletAccountItem( @@ -52,7 +55,8 @@ proc `$`*(self: Item): string = isChat: {self.isChat}, currencyBalance: {self.currencyBalance}, assets.len: {self.assets.getCount()}, - emoji: {self.emoji} + emoji: {self.emoji}, + derivedfrom: {self.derivedfrom} ]""" proc getName*(self: Item): string = @@ -87,3 +91,6 @@ proc getCurrencyBalance*(self: Item): float64 = proc getAssets*(self: Item): token_model.Model = return self.assets + +proc getDerivedFrom*(self: Item): string = + return self.derivedfrom diff --git a/src/app/modules/main/wallet_section/accounts/model.nim b/src/app/modules/main/wallet_section/accounts/model.nim index 34ec56a87b..3ba806400c 100644 --- a/src/app/modules/main/wallet_section/accounts/model.nim +++ b/src/app/modules/main/wallet_section/accounts/model.nim @@ -14,7 +14,8 @@ type IsChat, CurrencyBalance, Assets, - Emoji + Emoji, + DerivedFrom QtObject: type @@ -38,7 +39,7 @@ QtObject: proc countChanged(self: Model) {.signal.} - proc getCount(self: Model): int {.slot.} = + proc getCount*(self: Model): int {.slot.} = self.items.len QtProperty[int] count: @@ -60,7 +61,8 @@ QtObject: ModelRole.IsChat.int:"isChat", ModelRole.Assets.int:"assets", ModelRole.CurrencyBalance.int:"currencyBalance", - ModelRole.Emoji.int: "emoji" + ModelRole.Emoji.int: "emoji", + ModelRole.DerivedFrom.int: "derivedfrom" }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -96,6 +98,8 @@ QtObject: result = newQVariant(item.getAssets()) of ModelRole.Emoji: result = newQVariant(item.getEmoji()) + of ModelRole.DerivedFrom: + result = newQVariant(item.getDerivedFrom()) proc setItems*(self: Model, items: seq[Item]) = self.beginResetModel() diff --git a/src/app/modules/main/wallet_section/accounts/module.nim b/src/app/modules/main/wallet_section/accounts/module.nim index cfd9aecc46..b9cc33352d 100644 --- a/src/app/modules/main/wallet_section/accounts/module.nim +++ b/src/app/modules/main/wallet_section/accounts/module.nim @@ -63,7 +63,8 @@ method refreshWalletAccounts*(self: Module) = w.isChat, w.getCurrencyBalance(), assets, - w.emoji + w.emoji, + w.derivedfrom )) self.view.setItems(items) @@ -101,17 +102,26 @@ method viewDidLoad*(self: Module) = self.moduleLoaded = true self.delegate.accountsModuleDidLoad() -method generateNewAccount*(self: Module, password: string, accountName: string, color: string, emoji: string): string = - return self.controller.generateNewAccount(password, accountName, color, emoji) +method generateNewAccount*(self: Module, password: string, accountName: string, color: string, emoji: string, path: string, derivedFrom: string): string = + return self.controller.generateNewAccount(password, accountName, color, emoji, path, derivedFrom) method addAccountsFromPrivateKey*(self: Module, privateKey: string, password: string, accountName: string, color: string, emoji: string): string = return self.controller.addAccountsFromPrivateKey(privateKey, password, accountName, color, emoji) -method addAccountsFromSeed*(self: Module, seedPhrase: string, password: string, accountName: string, color: string, emoji: string): string = - return self.controller.addAccountsFromSeed(seedPhrase, password, accountName, color, emoji) +method addAccountsFromSeed*(self: Module, seedPhrase: string, password: string, accountName: string, color: string, emoji: string, path: string): string = + return self.controller.addAccountsFromSeed(seedPhrase, password, accountName, color, emoji, path) method addWatchOnlyAccount*(self: Module, address: string, accountName: string, color: string, emoji: string): string = return self.controller.addWatchOnlyAccount(address, accountName, color, emoji) method deleteAccount*(self: Module, address: string) = self.controller.deleteAccount(address) + +method getDerivedAddressList*(self: Module, password: string, derivedFrom: string, path: string, pageSize: int, pageNumber: int): (seq[DerivedAddressDto], string) = + return self.controller.getDerivedAddressList(password, derivedFrom, path, pageSize, pageNumber) + +method getDerivedAddressListForMnemonic*(self: Module, mnemonic: string, path: string, pageSize: int, pageNumber: int): (seq[wallet_account_service.DerivedAddressDto], string) = + return self.controller.getDerivedAddressListForMnemonic(mnemonic, path, pageSize, pageNumber) + + + diff --git a/src/app/modules/main/wallet_section/accounts/view.nim b/src/app/modules/main/wallet_section/accounts/view.nim index c35e344376..57032d24a4 100644 --- a/src/app/modules/main/wallet_section/accounts/view.nim +++ b/src/app/modules/main/wallet_section/accounts/view.nim @@ -1,11 +1,17 @@ -import NimQml +import NimQml, sequtils, strutils, sugar import ./model import ./item import ./io_interface +import ./generated_wallet_model +import ./generated_wallet_item +import ./derived_address_model +import ./derived_address_item const WATCH = "watch" const GENERATED = "generated" +const SEED = "seed" +const KEY = "key" QtObject: type @@ -15,10 +21,14 @@ QtObject: generated: Model watchOnly: Model imported: Model + generatedAccounts: GeneratedWalletModel + derivedAddresses: DerivedAddressModel modelVariant: QVariant generatedVariant: QVariant importedVariant: QVariant watchOnlyVariant: QVariant + generatedAccountsVariant: QVariant + derivedAddressesVariant: QVariant tmpAddress: string proc delete*(self: View) = @@ -30,6 +40,10 @@ QtObject: self.generatedVariant.delete self.watchOnly.delete self.watchOnlyVariant.delete + self.generatedAccounts.delete + self.generatedAccountsVariant.delete + self.derivedAddresses.delete + self.derivedAddressesVariant.delete self.QObject.delete proc newView*(delegate: io_interface.AccessInterface): View = @@ -44,6 +58,10 @@ QtObject: result.generatedVariant = newQVariant(result.generated) result.watchOnly = newModel() result.watchOnlyVariant = newQVariant(result.watchOnly) + result.generatedAccounts = newGeneratedWalletModel() + result.generatedAccountsVariant = newQVariant(result.generatedAccounts) + result.derivedAddresses = newDerivedAddressModel() + result.derivedAddressesVariant = newQVariant(result.derivedAddresses) proc load*(self: View) = self.delegate.viewDidLoad() @@ -84,6 +102,24 @@ QtObject: read = getGenereated notify = generatedChanged + proc generatedAccountsChanged*(self: View) {.signal.} + + proc getGeneratedAccounts(self: View): QVariant {.slot.} = + return self.generatedAccountsVariant + + QtProperty[QVariant] generatedAccounts: + read = getGeneratedAccounts + notify = generatedAccountsChanged + + proc derivedAddressesChanged*(self: View) {.signal.} + + proc getDerivedAddresses(self: View): QVariant {.slot.} = + return self.derivedAddressesVariant + + QtProperty[QVariant] derivedAddresses: + read = getDerivedAddresses + notify = derivedAddressesChanged + proc setItems*(self: View, items: seq[Item]) = self.model.setItems(items) @@ -98,19 +134,37 @@ QtObject: watchOnly.add(item) else: imported.add(item) - + self.watchOnly.setItems(watchOnly) self.imported.setItems(imported) self.generated.setItems(generated) - proc generateNewAccount*(self: View, password: string, accountName: string, color: string, emoji: string): string {.slot.} = - return self.delegate.generateNewAccount(password, accountName, color, emoji) + # create a list of imported seeds/default account created from where more accounts can be derived + var generatedAccounts: seq[GeneratedWalletItem] = @[] + var importedSeedIndex: int = 1 + for item in items: + if item.getWalletType() == "": + var generatedAccs: Model = newModel() + generatedAccs.setItems(generated.filter(x => cmpIgnoreCase(x.getDerivedFrom(), item.getDerivedFrom()) == 0)) + generatedAccounts.add(initGeneratedWalletItem("Default", "status", generatedAccs, item.getDerivedFrom())) + elif item.getWalletType() == SEED: + var generatedAccs1: Model = newModel() + var filterItems: seq[Item] = generated.filter(x => cmpIgnoreCase(x.getDerivedFrom(), item.getDerivedFrom()) == 0) + filterItems.insert(item, 0) + generatedAccs1.setItems(filterItems) + generatedAccounts.add(initGeneratedWalletItem("Seed " & $importedSeedIndex , "seed-phrase", generatedAccs1, item.getDerivedFrom())) + importedSeedIndex += 1 + self.generatedAccounts.setItems(generatedAccounts) + + + proc generateNewAccount*(self: View, password: string, accountName: string, color: string, emoji: string, path: string, derivedFrom: string): string {.slot.} = + return self.delegate.generateNewAccount(password, accountName, color, emoji, path, derivedFrom) proc addAccountsFromPrivateKey*(self: View, privateKey: string, password: string, accountName: string, color: string, emoji: string): string {.slot.} = return self.delegate.addAccountsFromPrivateKey(privateKey, password, accountName, color, emoji) - proc addAccountsFromSeed*(self: View, seedPhrase: string, password: string, accountName: string, color: string, emoji: string): string {.slot.} = - return self.delegate.addAccountsFromSeed(seedPhrase, password, accountName, color, emoji) + proc addAccountsFromSeed*(self: View, seedPhrase: string, password: string, accountName: string, color: string, emoji: string, path: string): string {.slot.} = + return self.delegate.addAccountsFromSeed(seedPhrase, password, accountName, color, emoji, path) proc addWatchOnlyAccount*(self: View, address: string, accountName: string, color: string, emoji: string): string {.slot.} = return self.delegate.addWatchOnlyAccount(address, accountName, color, emoji) @@ -129,3 +183,36 @@ QtObject: proc getAccountAssetsByAddress*(self: View): QVariant {.slot.} = return self.model.getAccountAssetsByAddress(self.tmpAddress) + + proc getDerivedAddressList*(self: View, password: string, derivedFrom: string, path: string, pageSize: int, pageNumber: int): string {.slot.} = + var items: seq[DerivedAddressItem] = @[] + let (result, error) = self.delegate.getDerivedAddressList(password, derivedfrom, path, pageSize, pageNumber) + for item in result: + items.add(initDerivedAddressItem(item.address, item.path, item.hasActivity)) + self.derivedAddresses.setItems(items) + self.derivedAddressesChanged() + return error + + proc getDerivedAddressListForMnemonic*(self: View, mnemonic: string, path: string, pageSize: int, pageNumber: int): string {.slot.} = + var items: seq[DerivedAddressItem] = @[] + let (result, error) = self.delegate.getDerivedAddressListForMnemonic(mnemonic, path, pageSize, pageNumber) + for item in result: + items.add(initDerivedAddressItem(item.address, item.path, item.hasActivity)) + self.derivedAddresses.setItems(items) + self.derivedAddressesChanged() + return error + + proc resetDerivedAddressModel*(self: View) {.slot.} = + var items: seq[DerivedAddressItem] = @[] + self.derivedAddresses.setItems(items) + self.derivedAddressesChanged() + + proc getDerivedAddressAtIndex*(self: View, index: int): string {.slot.} = + return self.derivedAddresses.getDerivedAddressAtIndex(index) + + proc getDerivedAddressPathAtIndex*(self: View, index: int): string {.slot.} = + return self.derivedAddresses.getDerivedAddressPathAtIndex(index) + + proc getDerivedAddressHasActivityAtIndex*(self: View, index: int): bool {.slot.} = + return self.derivedAddresses.getDerivedAddressHasActivityAtIndex(index) + diff --git a/src/app_service/service/accounts/service.nim b/src/app_service/service/accounts/service.nim index 5c519a1918..b41fa2b490 100644 --- a/src/app_service/service/accounts/service.nim +++ b/src/app_service/service/accounts/service.nim @@ -122,6 +122,14 @@ proc storeDerivedAccounts(self: Service, accountId, hashedPassword: string, result = toDerivedAccounts(response.result) +proc storeAccount(self: Service, accountId, hashedPassword: string): GeneratedAccountDto = + let response = status_account.storeAccounts(accountId, hashedPassword) + + if response.result.contains("error"): + raise newException(Exception, response.result["error"].getStr) + + result = toGeneratedAccountDto(response.result) + proc saveAccountAndLogin(self: Service, hashedPassword: string, account, subaccounts, settings, config: JsonNode): AccountDto = try: @@ -168,14 +176,16 @@ proc prepareSubaccountJsonObject(self: Service, account: GeneratedAccountDto, di "color": "#4360df", "wallet": true, "path": PATH_DEFAULT_WALLET, - "name": "Status account" + "name": "Status account", + "derived-from": account.address }, { "public-key": account.derivedAccounts.whisper.publicKey, "address": account.derivedAccounts.whisper.address, "name": if displayName == "": account.alias else: displayName, "path": PATH_WHISPER, - "chat": true + "chat": true, + "derived-from": "" } ] @@ -271,6 +281,7 @@ proc setupAccount*(self: Service, accountId, password, displayName: string): str return description let hashedPassword = hashString(password) + discard self.storeAccount(accountId, hashedPassword) discard self.storeDerivedAccounts(accountId, hashedPassword, PATHS) self.loggedInAccount = self.saveAccountAndLogin(hashedPassword, accountDataJson, diff --git a/src/app_service/service/wallet_account/derived_address.nim b/src/app_service/service/wallet_account/derived_address.nim new file mode 100644 index 0000000000..466360fd28 --- /dev/null +++ b/src/app_service/service/wallet_account/derived_address.nim @@ -0,0 +1,14 @@ +import json + +include ../../common/json_utils + +type DerivedAddressDto* = object + address*: string + path*: string + hasActivity*: bool + +proc toDerivedAddressDto*(jsonObj: JsonNode): DerivedAddressDto = + result = DerivedAddressDto() + discard jsonObj.getProp("address", result.address) + discard jsonObj.getProp("path", result.path) + discard jsonObj.getProp("hasActivity", result.hasActivity) diff --git a/src/app_service/service/wallet_account/dto.nim b/src/app_service/service/wallet_account/dto.nim index cb71b1dcc4..c5e91dfa15 100644 --- a/src/app_service/service/wallet_account/dto.nim +++ b/src/app_service/service/wallet_account/dto.nim @@ -26,6 +26,7 @@ type isChat*: bool tokens*: seq[WalletTokenDto] emoji*: string + derivedfrom*: string proc newDto*( name: string, @@ -36,7 +37,8 @@ proc newDto*( walletType: string, isWallet: bool, isChat: bool, - emoji: string + emoji: string, + derivedfrom: string ): WalletAccountDto = return WalletAccountDto( name: name, @@ -47,7 +49,8 @@ proc newDto*( walletType: walletType, isWallet: isWallet, isChat: isChat, - emoji: emoji + emoji: emoji, + derivedfrom: derivedfrom ) proc toWalletAccountDto*(jsonObj: JsonNode): WalletAccountDto = @@ -61,6 +64,7 @@ proc toWalletAccountDto*(jsonObj: JsonNode): WalletAccountDto = discard jsonObj.getProp("public-key", result.publicKey) discard jsonObj.getProp("type", result.walletType) discard jsonObj.getProp("emoji", result.emoji) + discard jsonObj.getProp("derived-from", result.derivedfrom) proc getCurrencyBalance*(self: WalletAccountDto): float64 = return self.tokens.map(t => t.currencyBalance).foldl(a + b, 0.0) diff --git a/src/app_service/service/wallet_account/service.nim b/src/app_service/service/wallet_account/service.nim index 8e55c4e34e..86a47e6d81 100644 --- a/src/app_service/service/wallet_account/service.nim +++ b/src/app_service/service/wallet_account/service.nim @@ -8,6 +8,7 @@ import ../network/service as network_service import ../../common/account_constants import dto +import derived_address import ../../../app/core/eventemitter import ../../../backend/accounts as status_go_accounts @@ -16,6 +17,7 @@ import ../../../backend/eth as status_go_eth import ../../../backend/cache export dto +export derived_address logScope: topics = "wallet-account-service" @@ -252,13 +254,15 @@ proc addNewAccountToLocalStore(self: Service) = self.accounts[newAccount.address] = newAccount self.events.emit(SIGNAL_WALLET_ACCOUNT_SAVED, AccountSaved(account: newAccount)) -proc generateNewAccount*(self: Service, password: string, accountName: string, color: string, emoji: string): string = +proc generateNewAccount*(self: Service, password: string, accountName: string, color: string, emoji: string, path: string, derivedFrom: string): string = try: - discard backend.generateAccount( + discard backend.generateAccountWithDerivedPath( hashPassword(password), accountName, color, - emoji) + emoji, + path, + derivedFrom) except Exception as e: return fmt"Error generating new account: {e.msg}" @@ -271,21 +275,21 @@ proc addAccountsFromPrivateKey*(self: Service, privateKey: string, password: str hashPassword(password), accountName, color, - emoji - ) + emoji) except Exception as e: return fmt"Error adding account with private key: {e.msg}" self.addNewAccountToLocalStore() -proc addAccountsFromSeed*(self: Service, mnemonic: string, password: string, accountName: string, color: string, emoji: string): string = +proc addAccountsFromSeed*(self: Service, mnemonic: string, password: string, accountName: string, color: string, emoji: string, path: string): string = try: - discard backend.addAccountWithMnemonic( + discard backend.addAccountWithMnemonicAndPath( mnemonic, hashPassword(password), accountName, color, - emoji + emoji, + path ) except Exception as e: return fmt"Error adding account with mnemonic: {e.msg}" @@ -349,3 +353,25 @@ proc updateWalletAccount*(self: Service, address: string, accountName: string, c account.emoji = emoji self.events.emit(SIGNAL_WALLET_ACCOUNT_UPDATED, WalletAccountUpdated(account: account)) + +proc getDerivedAddressList*(self: Service, password: string, derivedFrom: string, path: string, pageSize: int, pageNumber: int): (seq[DerivedAddressDto], string) = + var derivedAddress: seq[DerivedAddressDto] = @[] + var error: string = "" + try: + let response = status_go_accounts.getDerivedAddressList(hashPassword(password), derivedFrom, path, pageSize, pageNumber) + derivedAddress = response.result.getElems().map(x => x.toDerivedAddressDto()) + except Exception as e: + error = fmt"Error getting derived address list: {e.msg}" + return (derivedAddress, error) + +proc getDerivedAddressListForMnemonic*(self: Service, mnemonic: string, path: string, pageSize: int, pageNumber: int): (seq[DerivedAddressDto], string) = + var derivedAddress: seq[DerivedAddressDto] = @[] + var error: string = "" + try: + let response = status_go_accounts.getDerivedAddressListForMnemonic(mnemonic, path, pageSize, pageNumber) + derivedAddress = response.result.getElems().map(x => x.toDerivedAddressDto()) + except Exception as e: + error = fmt"Error getting derived address list for mnemonic: {e.msg}" + return (derivedAddress, error) + + diff --git a/src/backend/accounts.nim b/src/backend/accounts.nim index fa4c4e297f..29e794ab8b 100644 --- a/src/backend/accounts.nim +++ b/src/backend/accounts.nim @@ -288,3 +288,11 @@ proc deleteIdentityImage*(keyUID: string): RpcResponse[JsonNode] {.raises: [Exce proc setDisplayName*(displayName: string): RpcResponse[JsonNode] {.raises: [Exception].} = let payload = %* [displayName] result = core.callPrivateRPC("setDisplayName".prefix, payload) + +proc getDerivedAddressList*(password: string, derivedFrom: string, path: string, pageSize: int = 0, pageNumber: int = 6,): RpcResponse[JsonNode] {.raises: [Exception].} = + let payload = %* [password, derivedFrom, path, pageSize, pageNumber ] + result = core.callPrivateRPC("accounts_getDerivedAddressesForPath", payload) + +proc getDerivedAddressListForMnemonic*(mnemonic: string, path: string, pageSize: int = 0, pageNumber: int = 6,): RpcResponse[JsonNode] {.raises: [Exception].} = + let payload = %* [mnemonic, path, pageSize, pageNumber ] + result = core.callPrivateRPC("accounts_getDerivedAddressesForMenominicWithPath", payload) diff --git a/src/backend/backend.nim b/src/backend/backend.nim index e621bbb055..232e92c195 100644 --- a/src/backend/backend.nim +++ b/src/backend/backend.nim @@ -107,18 +107,21 @@ rpc(fetchPrices, "wallet"): symbols: seq[string] currency: string -rpc(generateAccount, "accounts"): +rpc(generateAccountWithDerivedPath, "accounts"): password: string name: string color: string emoji: string + path: string + derivedFrom: string -rpc(addAccountWithMnemonic, "accounts"): +rpc(addAccountWithMnemonicAndPath, "accounts"): mnemonic: string password: string name: string color: string emoji: string + path: string rpc(addAccountWithPrivateKey, "accounts"): privateKey: string @@ -194,4 +197,4 @@ rpc(addDappPermissions, "permissions"): rpc(deleteDappPermissionsByNameAndAddress, "permissions"): dapp: string - address: string \ No newline at end of file + address: string diff --git a/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml b/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml index 3cf60f273b..8dd0c0a589 100644 --- a/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml +++ b/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml @@ -6,6 +6,7 @@ import StatusQ.Popups 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import utils 1.0 +import shared.stores 1.0 import "../controls" import "../stores" diff --git a/ui/app/AppLayouts/Wallet/panels/DerivationPathsPanel.qml b/ui/app/AppLayouts/Wallet/panels/DerivationPathsPanel.qml new file mode 100644 index 0000000000..8ffce36cf4 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/DerivationPathsPanel.qml @@ -0,0 +1,58 @@ +import QtQuick 2.12 + +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 + +import utils 1.0 +import "../stores" + +StatusSelect { + id: derivationPathSelect + + property string path: "" + + function reset() { + derivationPathSelectedItem.title = DerivationPathModel.derivationPaths.get(0).name + derivationPathSelectedItem.subTitle = DerivationPathModel.derivationPaths.get(0).path + } + + label: qsTr("Derivation Path") + selectMenu.width: 351 + menuAlignment: StatusSelect.MenuAlignment.Left + model: DerivationPathModel.derivationPaths + selectedItemComponent: StatusListItem { + id: derivationPathSelectedItem + implicitWidth: parent.width + statusListItemTitle.wrapMode: Text.NoWrap + statusListItemTitle.width: parent.width - Style.current.padding + statusListItemTitle.elide: Qt.ElideMiddle + statusListItemTitle.anchors.left: undefined + statusListItemTitle.anchors.right: undefined + icon.background.color: "transparent" + border.width: 1 + border.color: Theme.palette.baseColor2 + title: DerivationPathModel.derivationPaths.get(0).name + subTitle: DerivationPathModel.derivationPaths.get(0).path + Component.onCompleted: { + derivationPathSelect.path = Qt.binding(function() { return derivationPathSelectedItem.subTitle}) + } + } + selectMenu.delegate: StatusListItem { + implicitWidth: parent.width + statusListItemTitle.wrapMode: Text.NoWrap + statusListItemTitle.width: parent.width - Style.current.padding + statusListItemTitle.elide: Qt.ElideMiddle + statusListItemTitle.anchors.left: undefined + statusListItemTitle.anchors.right: undefined + title: model.name + subTitle: model.path + onClicked: { + derivationPathSelectedItem.title = title + derivationPathSelectedItem.subTitle = subTitle + derivationPathSelect.selectMenu.close() + } + } +} + + diff --git a/ui/app/AppLayouts/Wallet/panels/DerivedAddressesPanel.qml b/ui/app/AppLayouts/Wallet/panels/DerivedAddressesPanel.qml new file mode 100644 index 0000000000..d2fe47dfcd --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/DerivedAddressesPanel.qml @@ -0,0 +1,153 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups 0.1 +import StatusQ.Components 0.1 + +import utils 1.0 + +import "../stores" + +Item { + id: derivedAddresses + + property string pathSubFix: "" + function reset() { + RootStore.resetDerivedAddressModel() + selectedDerivedAddress.pathSubFix = 0 + selectedDerivedAddress.title = "---" + selectedDerivedAddress.subTitle = qsTr("No activity") + } + + QtObject { + id: _internal + property int pageSize: 6 + property int noOfPages: Math.ceil(RootStore.derivedAddressesList.count/pageSize) + property int lastPageSize: RootStore.derivedAddressesList.count - ((noOfPages -1) * pageSize) + property bool isLastPage: stackLayout.currentIndex == (noOfPages - 1) + + // dimensions + property int popupWidth: 359 + property int maxAddressWidth: 102 + } + + Connections { + target: RootStore.derivedAddressesList + onModelReset: { + _internal.pageSize = 0 + _internal.pageSize = 6 + } + } + + ColumnLayout { + id: layout + width: parent.width + spacing: 7 + StatusBaseText { + id: inputLabel + width: parent.width + text: qsTr("Account") + font.pixelSize: 15 + color: selectedDerivedAddress.enabled ? Theme.palette.directColor1 : Theme.palette.baseColor1 + } + StatusListItem { + id: selectedDerivedAddress + property int pathSubFix: 0 + implicitWidth: parent.width + color: "transparent" + border.width: 1 + border.color: Theme.palette.baseColor2 + title: "---" + subTitle: qsTr("No activity") + statusListItemTitle.wrapMode: Text.NoWrap + statusListItemTitle.width: _internal.maxAddressWidth + statusListItemTitle.elide: Qt.ElideMiddle + statusListItemTitle.anchors.left: undefined + statusListItemTitle.anchors.right: undefined + components: [ + StatusIcon { + width: 24 + height: 24 + icon: "chevron-down" + color: Theme.palette.baseColor1 + } + ] + onClicked: derivedAddressPopup.popup(derivedAddresses.x - layout.width - Style.current.bigPadding , derivedAddresses.y + layout.height + 8) + enabled: RootStore.derivedAddressesList.count > 0 + Component.onCompleted: derivedAddresses.pathSubFix = Qt.binding(function() { return pathSubFix}) + } + } + + StatusPopupMenu { + id: derivedAddressPopup + width: _internal.popupWidth + contentItem: Column { + StackLayout { + id: stackLayout + Layout.fillWidth:true + Layout.fillHeight: true + Repeater { + id: pageModel + model: _internal.noOfPages + delegate: Page { + id: page + contentItem: ColumnLayout { + Repeater { + id: repeater + model: _internal.isLastPage ? _internal.lastPageSize : _internal.pageSize + delegate: StatusListItem { + id: element + property int actualIndex: index + (stackLayout.currentIndex* _internal.pageSize) + implicitWidth: derivedAddressPopup.width + statusListItemTitle.wrapMode: Text.NoWrap + statusListItemTitle.width: _internal.maxAddressWidth + statusListItemTitle.elide: Qt.ElideMiddle + statusListItemTitle.anchors.left: undefined + statusListItemTitle.anchors.right: undefined + title: RootStore.getDerivedAddressData(actualIndex) + subTitle: RootStore.getDerivedAddressHasActivityData(actualIndex) ? qsTr("Has Activity"): qsTr("No Activity") + components: [ + StatusBaseText { + text: element.actualIndex + font.pixelSize: 15 + color: Theme.palette.baseColor1 + } + ] + onClicked: { + selectedDerivedAddress.title = title + selectedDerivedAddress.subTitle = subTitle + selectedDerivedAddress.pathSubFix = actualIndex + derivedAddressPopup.close() + } + Component.onCompleted: { + if(index === 0) { + selectedDerivedAddress.title = title + selectedDerivedAddress.subTitle = subTitle + selectedDerivedAddress.pathSubFix = actualIndex + stackLayout.currentIndex = 0 + } + } + } + } + } + } + } + } + + PageIndicator { + id: pageIndicator + anchors.horizontalCenter: parent.horizontalCenter + interactive: true + currentIndex: stackLayout.currentIndex + count: stackLayout.count + onCurrentIndexChanged: stackLayout.currentIndex = currentIndex + } + } + } +} + + + diff --git a/ui/app/AppLayouts/Wallet/panels/ImportPrivateKeyPanel.qml b/ui/app/AppLayouts/Wallet/panels/ImportPrivateKeyPanel.qml new file mode 100644 index 0000000000..03c5c81a04 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/ImportPrivateKeyPanel.qml @@ -0,0 +1,69 @@ +import QtQuick 2.12 + +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Controls.Validators 0.1 + +import utils 1.0 +import "../stores" + +StatusInput { + id: privateKey + + function resetMe() { + _internal.errorString = "" + privateKey.text = "" + privateKey.reset() + reset() + } + + function validateMe() { + if (privateKey.text === "") { + //% "You need to enter a private key" + _internal.errorString = qsTrId("you-need-to-enter-a-private-key") + } else if (!Utils.isPrivateKey(privateKey.text)) { + //% "Enter a valid private key (64 characters hexadecimal string)" + _internal.errorString = qsTrId("enter-a-valid-private-key-(64-characters-hexadecimal-string)") + } else { + _internal.errorString = "" + } + return _internal.errorString === "" + } + + QtObject { + id: _internal + property int privateKeyCharLimit: 66 + property string errorString: "" + } + + //% "Private key" + label: qsTrId("private-key") + charLimit: _internal.privateKeyCharLimit + input.multiline: true + input.minimumHeight: 80 + input.maximumHeight: 108 + //% "Paste the contents of your private key" + input.placeholderText: qsTrId("paste-the-contents-of-your-private-key") + errorMessage: _internal.errorString + validators: [ + StatusMinLengthValidator { + minLength: 1 + //% "You need to enter a private key" + errorMessage: qsTrId("you-need-to-enter-a-private-key") + }, + StatusValidator { + property var validate: function (value) { + return Utils.isPrivateKey(value) + } + //% "Enter a valid private key (64 characters hexadecimal string)" + errorMessage: qsTrId("enter-a-valid-private-key-(64-characters-hexadecimal-string)") + } + ] + onVisibleChanged: { + if(visible) + privateKey.input.edit.forceActiveFocus(); + } +} + + diff --git a/ui/app/AppLayouts/Wallet/panels/ImportSeedPhrasePanel.qml b/ui/app/AppLayouts/Wallet/panels/ImportSeedPhrasePanel.qml new file mode 100644 index 0000000000..476b02a858 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/ImportSeedPhrasePanel.qml @@ -0,0 +1,210 @@ +import QtQuick 2.12 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 + +import utils 1.0 +import shared.stores 1.0 + +import "../stores" + +GridView { + id: grid + + property bool isValid: false + property string mnemonicString: "" + property int preferredHeight: (cellHeight * model/2) + footerItem.height + + function reset() { + _internal.errorString = "" + mnemonicString = "" + _internal.mnemonicInput = []; + if (!grid.atXBeginning) { + grid.positionViewAtBeginning(); + } + for(var i = 0; i < grid.model; i++) { + if(grid.itemAtIndex(i)) { + grid.itemAtIndex(i).textEdit.text = "" + grid.itemAtIndex(i).textEdit.reset() + } + } + } + + function validate() { + _internal.errorString = "" + if (!Utils.isMnemonic(mnemonicString)) { + //% "Invalid seed phrase" + _internal.errorString = qsTrId("custom-seed-phrase") + } else { + _internal.errorString = RootStore.vaildateMnemonic(mnemonicString) + const regex = new RegExp('word [a-z]+ not found in the dictionary', 'i'); + if (regex.test(_internal.errorString)) { + //% "Invalid seed phrase" + _internal.errorString = qsTrId("custom-seed-phrase") + '. ' + + //% "This seed phrase doesn't match our supported dictionary. Check for misspelled words." + qsTrId("custom-seed-phrase-text-1") + } + } + return _internal.errorString === "" + } + + QtObject { + id: _internal + property int seedPhraseInputHeight: 44 + property int seedPhraseInputWidth: 220 + property var mnemonicInput: [] + property string errorString: "" + readonly property int twelveWordsModel: 12 + readonly property int twentyFourWordsModel: 24 + + function getSeedPhraseString() { + var seedPhrase = "" + for(var i = 0; i < grid.model; i++) { + if(!!grid.itemAtIndex(i)) { + seedPhrase += grid.itemAtIndex(i).text + " " + } + } + return seedPhrase + } + } + + cellHeight: _internal.seedPhraseInputHeight + Style.current.halfPadding + cellWidth: _internal.seedPhraseInputWidth + Style.current.halfPadding + interactive: false + z: 100000 + + model: _internal.twelveWordsModel + + onModelChanged: { + mnemonicString = ""; + _internal.mnemonicInput = []; + } + + + onIsValidChanged: { + if(isValid) { + mnemonicString = _internal.getSeedPhraseString() + } + } + + onVisibleChanged: { + if(visible) { + grid.itemAtIndex(0).textEdit.input.edit.forceActiveFocus() + } + } + + delegate: StatusSeedPhraseInput { + id: statusSeedInput + width: _internal.seedPhraseInputWidth + height: _internal.seedPhraseInputHeight + textEdit.errorMessageCmp.visible: false + textEdit.input.anchors.topMargin: 11 + leftComponentText: index + 1 + inputList: BIP39_en { } + property int itemIndex: index + z: (grid.currentIndex === index) ? 150000000 : 0 + onDoneInsertingWord: { + _internal.mnemonicInput.push({"pos": leftComponentText, "seed": word.replace(/\s/g, '')}); + for (var j = 0; j < _internal.mnemonicInput.length; j++) { + if (_internal.mnemonicInput[j].pos === leftComponentText && _internal.mnemonicInput[j].seed !== word) { + _internal.mnemonicInput[j].seed = word; + } + } + //remove duplicates + var valueArr = _internal.mnemonicInput.map(function(item){ return item.pos }); + var isDuplicate = valueArr.some(function(item, idx){ + if (valueArr.indexOf(item) !== idx) { + _internal.mnemonicInput.splice(idx, 1); + } + return valueArr.indexOf(item) !== idx + }); + for (var i = !grid.atXBeginning ? 12 : 0; i < grid.count; i++) { + if (parseInt(grid.itemAtIndex(i).leftComponentText) === (parseInt(leftComponentText)+1)) { + grid.currentIndex = grid.itemAtIndex(i).itemIndex; + grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(); + if (grid.currentIndex === 11) { + grid.positionViewAtEnd(); + if (grid.count === 20) { + grid.contentX = 1500; + } + } + } + } + grid.isValid = (_internal.mnemonicInput.length === grid.model); + } + onEditClicked: { + grid.currentIndex = index; + grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus(); + } + onKeyPressed: { + if (event.key === Qt.Key_Tab || event.key === Qt.Key_Right) { + for (var i = !grid.atXBeginning ? 12 : 0; i < grid.count; i++) { + if (parseInt(grid.itemAtIndex(i).leftComponentText) === ((parseInt(leftComponentText)+1) <= grid.count ? (parseInt(leftComponentText)+1) : grid.count)) { + grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(); + textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit; + } + } + } else if (event.key === Qt.Key_Left) { + for (var i = !grid.atXBeginning ? 12 : 0; i < grid.count; i++) { + if (parseInt(grid.itemAtIndex(i).leftComponentText) === ((parseInt(leftComponentText)-1) >= 0 ? (parseInt(leftComponentText)-1) : 0)) { + grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(); + } + } + } else if (event.key === Qt.Key_Down) { + grid.itemAtIndex((index+1 < grid.count) ? (index+1) : (grid.count-1)).textEdit.input.edit.forceActiveFocus(); + } else if (event.key === Qt.Key_Up) { + grid.itemAtIndex((index-1 >= 0) ? (index-1) : 0).textEdit.input.edit.forceActiveFocus(); + } + + if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) { + var wordIndex = _internal.mnemonicInput.findIndex(x => x.pos === leftComponentText); + if (wordIndex > -1) { + _internal.mnemonicInput.splice(wordIndex , 1); + grid.isValid = _internal.mnemonicInput.length === grid.model + } + } + + grid.currentIndex = index; + } + } + footer: Item { + width: grid.width - Style.current.padding + height: button.height + errorMessage.height + Style.current.padding*2 + StatusBaseText { + id: errorMessage + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: Style.current.padding + + height: visible ? implicitHeight : 0 + visible: !!text + text: _internal.errorString + + font.pixelSize: 12 + color: Theme.palette.dangerColor1 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + StatusButton { + id: button + anchors.top: errorMessage.bottom + anchors.topMargin: Style.current.padding + anchors.horizontalCenter: parent.horizontalCenter + text: grid.model === _internal.twelveWordsModel ? qsTr("Use 24 word seed phrase"): + qsTr("Use 12 word seed phrase") + onClicked: { + if(grid.model === _internal.twelveWordsModel) { + grid.model = _internal.twentyFourWordsModel + } + else { + grid.model = _internal.twelveWordsModel + } + } + } + } +} + + diff --git a/ui/app/AppLayouts/Wallet/panels/SelectGeneratedAccount.qml b/ui/app/AppLayouts/Wallet/panels/SelectGeneratedAccount.qml new file mode 100644 index 0000000000..901d64a666 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/SelectGeneratedAccount.qml @@ -0,0 +1,136 @@ +import QtQuick 2.12 +import QtQml.Models 2.14 + +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Utils 0.1 + +import utils 1.0 +import "../stores" + +StatusSelect { + id: selectAccountType + + property int addAccountType + property string derivedFromAddress: "" + + enum AddAccountType { + GenerateNew, + ImportSeedPhrase, + ImportPrivateKey, + WatchOnly + } + + function resetMe() { + _internal.getGeneratedAccountsModel() + addAccountType = SelectGeneratedAccount.AddAccountType.GenerateNew + } + + Connections { + target: RootStore.generatedAccountsViewModel + onModelReset: { + _internal.delegateModel.model = RootStore.generatedAccountsViewModel + _internal.getGeneratedAccountsModel() + } + } + + QtObject { + id: _internal + property string importSeedPhraseString : qsTr("Import new Seed Phrase") + property string importPrivateKeyString : qsTr("Import new Private Key") + //% "Add a watch-only address" + property string addWatchOnlyAccountString : qsTrId("add-a-watch-account") + + property var delegateModel: DelegateModel { + model: RootStore.generatedAccountsViewModel + onModelUpdated: { + _internal.getGeneratedAccountsModel() + } + } + property ListModel generatedAccountsModel: ListModel{} + + function getGeneratedAccountsModel() { + if(generatedAccountsModel) { + generatedAccountsModel.clear() + for (var row = 0; row < _internal.delegateModel.model.count; row++) { + var item = _internal.delegateModel.items.get(row).model; + generatedAccountsModel.append({"name": item.name, "iconName": item.iconName, "generatedModel": item.generatedModel, "derivedfrom": item.derivedfrom, "isHeader": false}) + if(row === 0 && _internal.delegateModel.model.count > 1) { + generatedAccountsModel.append({"name": qsTr("Imported"), "iconName": "", "derivedfrom": "", "isHeader": true}) + } + } + generatedAccountsModel.append({"name": qsTr("Add new"), "iconName": "", "derivedfrom": "", "isHeader": true}) + generatedAccountsModel.append({"name": _internal.importSeedPhraseString, "iconName": "seed-phrase", "derivedfrom": "", "isHeader": false}) + generatedAccountsModel.append({"name": _internal.importPrivateKeyString, "iconName": "password", "derivedfrom": "", "isHeader": false}) + generatedAccountsModel.append({"name": _internal.addWatchOnlyAccountString, "iconName": "show", "derivedfrom": "", "isHeader": false}) + } + } + } + + label: qsTr("Origin") + model: _internal.generatedAccountsModel + selectedItemComponent: StatusListItem { + id: selectedItem + icon.background.color: "transparent" + border.width: 1 + border.color: Theme.palette.baseColor2 + tagsDelegate: StatusListItemTag { + color: model.color + height: Style.current.bigPadding + radius: 6 + closeButtonVisible: false + icon.emoji: model.emoji + icon.emojiSize: Emoji.size.verySmall + icon.isLetterIdenticon: true + title: model.name + titleText.font.pixelSize: 12 + titleText.color: Theme.palette.indirectColor1 + } + } + selectMenu.delegate: StatusListItem { + id: defaultListItem + title: model.name + icon.name: model.iconName + tagsModel : model.generatedModel + enabled: !model.isHeader + icon.background.color: "transparent" + icon.color: model.generatedModel ? Theme.palette.primaryColor1 : Theme.palette.directColor5 + tagsDelegate: StatusListItemTag { + color: model.color + height: 24 + radius: 6 + closeButtonVisible: false + icon.emoji: model.emoji + icon.emojiSize: Emoji.size.verySmall + icon.isLetterIdenticon: true + title: model.name + titleText.font.pixelSize: 12 + titleText.color: Theme.palette.indirectColor1 + } + onClicked: { + selectAccountType.addAccountType = (model.name === _internal.importSeedPhraseString) ? SelectGeneratedAccount.AddAccountType.ImportSeedPhrase : + (model.name === _internal.importPrivateKeyString) ? SelectGeneratedAccount.AddAccountType.ImportPrivateKey : + (model.name === _internal.addWatchOnlyAccountString) ? SelectGeneratedAccount.AddAccountType.WatchOnly : + SelectGeneratedAccount.AddAccountType.GenerateNew + selectedItem.title = model.name + selectedItem.icon.name = model.iconName + selectedItem.tagsModel = model.generatedModel + selectedItem.enabled = !model.isHeader + + selectAccountType.derivedFromAddress = model.derivedfrom + selectMenu.close() + } + Component.onCompleted: { + if(index === 0) { + selectedItem.title = model.name + selectedItem.icon.name = model.iconName + selectedItem.tagsModel = model.generatedModel + selectedItem.enabled = !model.isHeader + selectAccountType.derivedFromAddress = model.derivedfrom + } + } + } +} + + diff --git a/ui/app/AppLayouts/Wallet/popups/AddAccountModal.qml b/ui/app/AppLayouts/Wallet/popups/AddAccountModal.qml index 52686a792f..c1254a3e70 100644 --- a/ui/app/AppLayouts/Wallet/popups/AddAccountModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/AddAccountModal.qml @@ -17,6 +17,7 @@ import shared.controls 1.0 import "../stores" import "../views" +import "../panels" StatusModal { id: popup @@ -31,6 +32,40 @@ StatusModal { //% "Generate an account" header.title: qsTrId("generate-a-new-account") + QtObject { + id: _internal + + property int numOfItems: 100 + property int pageNumber: 1 + function getDerivedAddressList() { + if(advancedSelection.expandableItem) { + var errMessage = "" + if(advancedSelection.expandableItem.addAccountType === SelectGeneratedAccount.AddAccountType.ImportSeedPhrase && + !!advancedSelection.expandableItem.path && + !!advancedSelection.expandableItem.mnemonicText) { + errMessage = RootStore.getDerivedAddressListForMnemonic(advancedSelection.expandableItem.mnemonicText, advancedSelection.expandableItem.path, numOfItems, pageNumber) + _internal.showPasswordError(errMessage) + } + else if(!!advancedSelection.expandableItem.path && !!advancedSelection.expandableItem.derivedFromAddress && (passwordInput.text.length >= 6)) { + errMessage = RootStore.getDerivedAddressList(passwordInput.text, advancedSelection.expandableItem.derivedFromAddress, advancedSelection.expandableItem.path, numOfItems, pageNumber) + _internal.showPasswordError(errMessage) + } + } + } + + function showPasswordError(errMessage) { + if (errMessage) { + if (Utils.isInvalidPasswordMessage(errMessage)) { + //% "Wrong password" + popup.passwordValidationError = qsTrId("wrong-password") + } else { + accountError.text = errMessage; + accountError.open(); + } + } + } + } + function validate() { if (passwordInput.text === "") { //% "You need to enter a password" @@ -41,8 +76,8 @@ StatusModal { } else { passwordValidationError = "" } - return passwordValidationError === "" && accountNameInput.valid - } + return passwordValidationError === "" && accountNameInput.valid + } onOpened: { passwordValidationError = ""; @@ -96,6 +131,10 @@ StatusModal { validationError: popup.passwordValidationError inputLabel.font.pixelSize: 15 inputLabel.font.weight: Font.Normal + onTextChanged: { + popup.passwordValidationError = "" + _internal.getDerivedAddressList() + } } } @@ -124,7 +163,6 @@ StatusModal { StatusColorSelectorGrid { id: colorSelectionGrid anchors.horizontalCenter: parent.horizontalCenter - //% "color" titleText: qsTr("color").toUpperCase() } @@ -148,7 +186,6 @@ StatusModal { anchors.horizontalCenter: parent.horizontalCenter width: parent.width - //% "Advanced" primaryText: qsTr("Advanced") type: StatusExpandableItem.Type.Tertiary expandable: true @@ -156,6 +193,7 @@ StatusModal { width: parent.width Layout.margins: Style.current.padding Component.onCompleted: advancedSelection.isValid = Qt.binding(function(){return isValid}) + onCalculateDerivedPath: _internal.getDerivedAddressList() } } } @@ -192,35 +230,26 @@ StatusModal { var errMessage = "" if(advancedSelection.expandableItem) { switch(advancedSelection.expandableItem.addAccountType) { - case AdvancedAddAccountView.AddAccountType.GenerateNew: - errMessage = RootStore.generateNewAccount(passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, emoji) + case SelectGeneratedAccount.AddAccountType.GenerateNew: + errMessage = RootStore.generateNewAccount(passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.icon.emoji, advancedSelection.expandableItem.completePath, advancedSelection.expandableItem.derivedFromAddress) break - case AdvancedAddAccountView.AddAccountType.ImportSeedPhrase: - errMessage = RootStore.addAccountsFromSeed(advancedSelection.expandableItem.mnemonicText, passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, emoji) + case SelectGeneratedAccount.AddAccountType.ImportSeedPhrase: + errMessage = RootStore.addAccountsFromSeed(advancedSelection.expandableItem.mnemonicText, passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.icon.emoji, advancedSelection.expandableItem.completePath) break - case AdvancedAddAccountView.AddAccountType.ImportPrivateKey: - errMessage = RootStore.addAccountsFromPrivateKey(advancedSelection.expandableItem.privateKey, passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, emoji) + case SelectGeneratedAccount.AddAccountType.ImportPrivateKey: + errMessage = RootStore.addAccountsFromPrivateKey(advancedSelection.expandableItem.privateKey, passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.icon.emoji) break - case AdvancedAddAccountView.AddAccountType.WatchOnly: + case SelectGeneratedAccount.AddAccountType.WatchOnly: errMessage = RootStore.addWatchOnlyAccount(advancedSelection.expandableItem.watchAddress, accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.icon.emoji) break } } else { - errMessage = RootStore.generateNewAccount(passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, emoji) + errMessage = RootStore.generateNewAccount(passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.icon.emoji, advancedSelection.expandableItem.completePath, advancedSelection.expandableItem.derivedFromAddress) } loading = false - if (errMessage) { - Global.playErrorSound(); - if (Utils.isInvalidPasswordMessage(errMessage)) { - //% "Wrong password" - popup.passwordValidationError = qsTrId("wrong-password") - } else { - accountError.text = errMessage; - accountError.open(); - } - return - } + _internal.showPasswordError(errMessage) + popup.afterAddAccount(); popup.close(); } diff --git a/ui/app/AppLayouts/Wallet/stores/DerivationPathModel.qml b/ui/app/AppLayouts/Wallet/stores/DerivationPathModel.qml new file mode 100644 index 0000000000..f006e7116e --- /dev/null +++ b/ui/app/AppLayouts/Wallet/stores/DerivationPathModel.qml @@ -0,0 +1,37 @@ +pragma Singleton + +import QtQuick 2.13 + +QtObject { + id: root + property ListModel derivationPaths: ListModel { + ListElement { + name: "Default" + path: "m/44'/60'/0'/0" + } + ListElement { + name: "Ethereum Classic" + path: "m/44'/61'/0'/0" + } + ListElement { + name: "Ethereum (Ledger)" + path: "m/44'/60'/0'" + } + ListElement { + name: "Ethereum Classic (Ledger)" + path: "m/44'/60'/160720'/0" + } + ListElement { + name: "Ethereum Classic (Ledger, Vintage MEW)" + path: "m/44'/60'/160720'/0'" + } + ListElement { + name: "Ethereum (KeepKey)" + path: "m/44'/60'" + } + ListElement { + name: "Ethereum Classic (KeepKey)" + path: "m/44'/61'" + } + } +} diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index 5d79336eb4..c727822bc4 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -28,6 +28,9 @@ QtObject { property var savedAddresses: walletSectionSavedAddresses.model + // Used for new wallet account generation + property var generatedAccountsViewModel: walletSectionAccounts.generatedAccounts + property var derivedAddressesList: walletSectionAccounts.derivedAddresses property var layer1Networks: networksModule.layer1 property var layer2Networks: networksModule.layer2 @@ -100,16 +103,16 @@ QtObject { walletSection.switchAccount(newIndex) } - function generateNewAccount(password, accountName, color, emoji) { - return walletSectionAccounts.generateNewAccount(password, accountName, color, emoji) + function generateNewAccount(password, accountName, color, emoji, path, derivedFrom) { + return walletSectionAccounts.generateNewAccount(password, accountName, color, emoji, path, derivedFrom) } function addAccountsFromPrivateKey(privateKey, password, accountName, color, emoji) { return walletSectionAccounts.addAccountsFromPrivateKey(privateKey, password, accountName, color, emoji) } - function addAccountsFromSeed(seedPhrase, password, accountName, color, emoji) { - return walletSectionAccounts.addAccountsFromSeed(seedPhrase, password, accountName, color, emoji) + function addAccountsFromSeed(seedPhrase, password, accountName, color, emoji, path) { + return walletSectionAccounts.addAccountsFromSeed(seedPhrase, password, accountName, color, emoji, path) } function addWatchOnlyAccount(address, accountName,color, emoji) { @@ -172,4 +175,32 @@ QtObject { function copyToClipboard(text) { globalUtils.copyToClipboard(text) } + + function getDerivedAddressList(password, derivedFrom, path, pageSize , pageNumber) { + return walletSectionAccounts.getDerivedAddressList(password, derivedFrom, path, pageSize , pageNumber) + } + + function getDerivedAddressData(index) { + return walletSectionAccounts.getDerivedAddressAtIndex(index) + } + + function getDerivedAddressPathData(index) { + return walletSectionAccounts.getDerivedAddressPathAtIndex(index) + } + + function getDerivedAddressHasActivityData(index) { + return walletSectionAccounts.getDerivedAddressHasActivityAtIndex(index) + } + + function getDerivedAddressListForMnemonic(mnemonic, path, pageSize , pageNumber) { + return walletSectionAccounts.getDerivedAddressListForMnemonic(mnemonic, path, pageSize , pageNumber) + } + + function resetDerivedAddressModel() { + walletSectionAccounts.resetDerivedAddressModel() + } + + function vaildateMnemonic(mnemonic) { + return onboardingModule.validateMnemonic(mnemonic) + } } diff --git a/ui/app/AppLayouts/Wallet/stores/qmldir b/ui/app/AppLayouts/Wallet/stores/qmldir index 3b0c907239..3ac17c6028 100644 --- a/ui/app/AppLayouts/Wallet/stores/qmldir +++ b/ui/app/AppLayouts/Wallet/stores/qmldir @@ -1 +1,2 @@ singleton RootStore 1.0 RootStore.qml +singleton DerivationPathModel 1.0 DerivationPathModel.qml diff --git a/ui/app/AppLayouts/Wallet/views/AdvancedAddAccountView.qml b/ui/app/AppLayouts/Wallet/views/AdvancedAddAccountView.qml index eac9c990e5..a97b6858bf 100644 --- a/ui/app/AppLayouts/Wallet/views/AdvancedAddAccountView.qml +++ b/ui/app/AppLayouts/Wallet/views/AdvancedAddAccountView.qml @@ -7,329 +7,125 @@ import StatusQ.Core.Theme 0.1 import StatusQ.Controls 0.1 import StatusQ.Popups 0.1 import StatusQ.Components 0.1 -import StatusQ.Core.Utils 0.1 import StatusQ.Controls.Validators 0.1 import utils 1.0 import "../stores" +import "../panels" ColumnLayout { id: advancedSection - property alias privateKey: privateKey.text - property int addAccountType: AdvancedAddAccountView.AddAccountType.GenerateNew - property string mnemonicText: getSeedPhraseString() + property int addAccountType: SelectGeneratedAccount.AddAccountType.GenerateNew + property string derivedFromAddress: "" + property string mnemonicText: "" + property alias privateKey: importPrivateKeyPanel.text + property string path: "" + property string pathSubFix: "" + property string completePath: path + "/" + pathSubFix property alias watchAddress: addressInput.text - property string errorString: "" - property bool isValid: addAccountType === AdvancedAddAccountView.AddAccountType.ImportSeedPhrase ? grid.isValid : - addAccountType === AdvancedAddAccountView.AddAccountType.ImportPrivateKey ? (privateKey.text !== "" && privateKey.valid) : - advancedSection.addAccountType === AdvancedAddAccountView.AddAccountType.WatchOnly ? (addressInput.text !== "" && addressInput.valid) : true + property bool isValid: addAccountType === SelectGeneratedAccount.AddAccountType.ImportSeedPhrase ? importSeedPhrasePanel.isValid : + addAccountType === SelectGeneratedAccount.AddAccountType.ImportPrivateKey ? (importPrivateKeyPanel.text !== "" && importPrivateKeyPanel.valid) : + addAccountType === SelectGeneratedAccount.AddAccountType.WatchOnly ? (addressInput.text !== "" && addressInput.valid) : true - enum AddAccountType { - GenerateNew, - ImportSeedPhrase, - ImportPrivateKey, - WatchOnly - } + signal calculateDerivedPath() function reset() { - mnemonicText = "" - errorString = "" - select.currentIndex = 0 - addAccountType = AdvancedAddAccountView.AddAccountType.GenerateNew - privateKey.text = "" - privateKey.reset() + //reset selectGeneratedAccount + selectGeneratedAccount.resetMe() + + // reset privateKey + importPrivateKeyPanel.resetMe() + + // reset importSeedPhrasePanel + importSeedPhrasePanel.reset() + + // reset derivation path + derivationPathsPanel.reset() + + // reset derviedAccountsList + derivedAddressesPanel.reset() + + // reset watch only address input addressInput.text = "" addressInput.reset() - for(var i = 0; i < grid.model; i++) { - if(grid.itemAtIndex(i)) { - grid.itemAtIndex(i).textEdit.text = "" - grid.itemAtIndex(i).textEdit.reset() - } - } } function validate() { - errorString = ""; - if(addAccountType == AdvancedAddAccountView.AddAccountType.ImportSeedPhrase) { - mnemonicText = getSeedPhraseString() - - if (!Utils.isMnemonic(mnemonicText)) { - //% "Invalid seed phrase" - errorString = qsTrId("custom-seed-phrase") - } else { - errorString = onboardingModule.validateMnemonic(mnemonicText) - const regex = new RegExp('word [a-z]+ not found in the dictionary', 'i'); - if (regex.test(errorString)) { - //% "Invalid seed phrase" - errorString = qsTrId("custom-seed-phrase") + '. ' + - //% "This seed phrase doesn't match our supported dictionary. Check for misspelled words." - qsTrId("custom-seed-phrase-text-1") - } - } - return errorString === "" + if(addAccountType === SelectGeneratedAccount.AddAccountType.ImportSeedPhrase) { + // validate mnemonic + return importSeedPhrasePanel.validate() } - else if(addAccountType == AdvancedAddAccountView.AddAccountType.ImportPrivateKey) { - if (privateKey.text === "") { - //% "You need to enter a private key" - errorString = qsTrId("you-need-to-enter-a-private-key") - } else if (!Utils.isPrivateKey(privateKey.text)) { - //% "Enter a valid private key (64 characters hexadecimal string)" - errorString = qsTrId("enter-a-valid-private-key-(64-characters-hexadecimal-string)") - } else { - errorString = "" - } - return errorString === "" + else if(addAccountType === SelectGeneratedAccount.AddAccountType.ImportPrivateKey) { + // validate privateKey + return importPrivateKeyPanel.validateMe() } - else if(advancedSection.addAccountType === AdvancedAddAccountView.AddAccountType.WatchOnly) { + else if(addAccountType === SelectGeneratedAccount.AddAccountType.WatchOnly) { return addressInput.valid } return true } - function getSeedPhraseString() { - var seedPhrase = "" - for(var i = 0; i < grid.model; i++) { - seedPhrase += grid.itemAtIndex(i).text + " " + onPathChanged: { + if(addAccountType === SelectGeneratedAccount.AddAccountType.ImportSeedPhrase) { + if(importSeedPhrasePanel.isValid) { + calculateDerivedPath() + } + } + else { + calculateDerivedPath() } - return seedPhrase } - QtObject { - id: _internal - property int seedPhraseInputHeight: 44 - property int seedPhraseInputWidth: 220 + onDerivedFromAddressChanged: { + // reset derviedAccountsList + derivedAddressesPanel.reset() + + if(addAccountType === SelectGeneratedAccount.AddAccountType.ImportSeedPhrase) { + if(importSeedPhrasePanel.isValid) { + calculateDerivedPath() + } + } + else { + calculateDerivedPath() + } } spacing: Style.current.padding - StatusSelect { - id: select - //% "Origin" - label: qsTr("Origin") + SelectGeneratedAccount { + id: selectGeneratedAccount Layout.margins: Style.current.padding - property int currentIndex: 0 - selectedItemComponent: StatusListItem { - id: selectedItem - icon.background.color: "transparent" - border.width: 1 - border.color: Theme.palette.baseColor2 - tagsDelegate: StatusListItemTag { - color: model.color - height: Style.current.bigPadding - radius: 6 - closeButtonVisible: false - icon.emoji: model.emoji - icon.emojiSize: Emoji.size.verySmall - icon.isLetterIdenticon: true - title: model.name - titleText.font.pixelSize: 12 - titleText.color: Theme.palette.indirectColor1 - } - } - model: ListModel { - Component.onCompleted: { - //% "Default" - append({"name": qsTr("Default"), "iconName": "status", "accountsModel": RootStore.generatedAccounts, "enabled": true}) - //% "Add new" - append({"name": qsTr("Add new"), "iconName": "", "enabled": false}) - //% "Import new Seed Phrase" - append({"name": qsTr("Import new Seed Phrase"), "iconName": "seed-phrase", "enabled": true}) - //% "Import new Private Key" - append({"name": qsTr("Import new Private Key"), "iconName": "password", "enabled": true}) - //% "Import new Private Key" - append({"name": qsTrId("add-a-watch-account"), "iconName": "show", "enabled": true}) - selectedItem.title = Qt.binding(function() {return get(select.currentIndex).name}) - selectedItem.icon.name = Qt.binding(function() {return get(select.currentIndex).iconName}) - selectedItem.tagsModel = Qt.binding(function() {return get(select.currentIndex).accountsModel}) - } - } - selectMenu.delegate: StatusListItem { - id: defaultListItem - title: model.name - icon.name: model.iconName - tagsModel : model.accountsModel - enabled: model.enabled - icon.background.color: "transparent" - icon.color: model.accountsModel ? Theme.palette.primaryColor1 : Theme.palette.directColor5 - tagsDelegate: StatusListItemTag { - color: model.color - height: 24 - radius: 6 - closeButtonVisible: false - icon.emoji: model.emoji - icon.emojiSize: Emoji.size.verySmall - icon.isLetterIdenticon: true - title: model.name - titleText.font.pixelSize: 12 - titleText.color: Theme.palette.indirectColor1 - } - onClicked: { - advancedSection.addAccountType = (index === 2) ? AdvancedAddAccountView.AddAccountType.ImportSeedPhrase : - (index === 3) ? AdvancedAddAccountView.AddAccountType.ImportPrivateKey : - (index === 4) ? AdvancedAddAccountView.AddAccountType.WatchOnly : - AdvancedAddAccountView.AddAccountType.GenerateNew - select.currentIndex = index - select.selectMenu.close() - } + Component.onCompleted: { + advancedSection.addAccountType = Qt.binding(function() {return addAccountType}) + advancedSection.derivedFromAddress = Qt.binding(function() {return derivedFromAddress}) } } - StatusInput { - id: privateKey - //% "Private key" - label: qsTrId("private-key") - charLimit: 66 - input.multiline: true - input.minimumHeight: 80 - input.maximumHeight: 108 - //% "Paste the contents of your private key" - input.placeholderText: qsTrId("paste-the-contents-of-your-private-key") - visible: advancedSection.addAccountType === AdvancedAddAccountView.AddAccountType.ImportPrivateKey && advancedSection.visible - errorMessage: advancedSection.errorString - validators: [ - StatusMinLengthValidator { - minLength: 1 - //% "You need to enter a private key" - errorMessage: qsTrId("you-need-to-enter-a-private-key") - }, - StatusValidator { - property var validate: function (value) { - return Utils.isPrivateKey(value) - } - //% "Enter a valid private key (64 characters hexadecimal string)" - errorMessage: qsTrId("enter-a-valid-private-key-(64-characters-hexadecimal-string)") - } - ] - onVisibleChanged: { - if(visible) - privateKey.input.edit.forceActiveFocus(); - } + ImportPrivateKeyPanel { + id: importPrivateKeyPanel + visible: advancedSection.addAccountType === SelectGeneratedAccount.AddAccountType.ImportPrivateKey && advancedSection.visible } - GridView { - id: grid + ImportSeedPhrasePanel { + id: importSeedPhrasePanel Layout.preferredWidth: parent.width - Layout.preferredHeight: visible ? (cellHeight * model/2) + footerItem.height: 0 + Layout.preferredHeight: visible ? importSeedPhrasePanel.preferredHeight: 0 Layout.leftMargin: Style.current.padding Layout.rightMargin: Style.current.padding - visible: advancedSection.addAccountType === AdvancedAddAccountView.AddAccountType.ImportSeedPhrase && advancedSection.visible - cellHeight: _internal.seedPhraseInputHeight + Style.current.halfPadding - cellWidth: _internal.seedPhraseInputWidth + Style.current.halfPadding - model: 12 - interactive: false - property bool isValid: checkIsValid() - function checkIsValid() { - var valid = model > 0 ? true: false - for(var i = 0; i < model; i++) { - if(grid.itemAtIndex(i)) - valid &= grid.itemAtIndex(i).isValid - } - return valid - } - - onVisibleChanged: { - if(visible) - grid.itemAtIndex(0).textEdit.input.edit.forceActiveFocus(); - } - - // To-do Alex has introduced a model for bip39 dictonary, need to use it once its available - // https://github.com/status-im/status-desktop/pull/5058 - delegate: StatusSeedPhraseInput { - id: statusSeedInput - width: _internal.seedPhraseInputWidth - height: _internal.seedPhraseInputHeight - textEdit.errorMessageCmp.visible: false - textEdit.input.anchors.topMargin: 11 - leftComponentText: index + 1 - property bool isValid: !!text - onIsValidChanged: { - grid.isValid = grid.checkIsValid() - } - onTextChanged: { - if (text !== "") { - grid.currentIndex = index; - } - } - // To-do Alex has introduced a model for bip39 dictonary, need to use it once its available - // https://github.com/status-im/status-desktop/pull/5058 - // onDoneInsertingWord: { - // advancedSection.mnemonicText += (index === 0) ? word : (" " + word); - // for (var i = !grid.atXBeginning ? 12 : 0; i < grid.count; i++) { - // if (parseInt(grid.itemAtIndex(i).leftComponentText) === (parseInt(leftComponentText)+1)) { - // grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(); - // } - // } - // } - onEditClicked: { - grid.currentIndex = index; - grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus(); - } - onKeyPressed: { - if (event.key === Qt.Key_Tab || event.key === Qt.Key_Right) { - for (var i = !grid.atXBeginning ? 12 : 0; i < grid.count; i++) { - if (parseInt(grid.itemAtIndex(i).leftComponentText) === ((parseInt(leftComponentText)+1) <= grid.count ? (parseInt(leftComponentText)+1) : grid.count)) { - grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(); - textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit; - } - } - } else if (event.key === Qt.Key_Left) { - for (var i = !grid.atXBeginning ? 12 : 0; i < grid.count; i++) { - if (parseInt(grid.itemAtIndex(i).leftComponentText) === ((parseInt(leftComponentText)-1) >= 0 ? (parseInt(leftComponentText)-1) : 0)) { - grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(); - } - } - } else if (event.key === Qt.Key_Down) { - grid.itemAtIndex((index+1 < grid.count) ? (index+1) : (grid.count-1)).textEdit.input.edit.forceActiveFocus(); - } else if (event.key === Qt.Key_Up) { - grid.itemAtIndex((index-1 >= 0) ? (index-1) : 0).textEdit.input.edit.forceActiveFocus(); - } - } - textEdit.validators: [ - StatusMinLengthValidator { - errorMessage: qsTr("Enter a valid word") - minLength: 3 - } - ] - } - footer: Item { - width: grid.width - Style.current.padding - height: button.height + errorMessage.height + 16*2 - StatusBaseText { - id: errorMessage - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: Style.current.padding - - height: visible ? implicitHeight : 0 - visible: !!text - text: errorString - - font.pixelSize: 12 - color: Theme.palette.dangerColor1 - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - } - StatusButton { - id: button - visible: false - anchors.top: errorMessage.bottom - anchors.topMargin: Style.current.padding - anchors.horizontalCenter: parent.horizontalCenter - //% "Use 24 word seed phrase" - text: grid.model === 12 ? qsTr("Use 24 word seed phrase"): - qsTr("Use 12 word seed phrase") - onClicked: grid.model = grid.model === 12 ? 24 : 12 + visible: advancedSection.addAccountType === SelectGeneratedAccount.AddAccountType.ImportSeedPhrase && advancedSection.visible + onMnemonicStringChanged: { + advancedSection.mnemonicText = mnemonicString + if(isValid) { + calculateDerivedPath() } } } StatusInput { id: addressInput - visible: advancedSection.addAccountType === AdvancedAddAccountView.AddAccountType.WatchOnly && advancedSection.visible + visible: advancedSection.addAccountType === SelectGeneratedAccount.AddAccountType.WatchOnly && advancedSection.visible //% "Enter address..." input.placeholderText: qsTrId("enter-address...") //% "Account address" @@ -351,37 +147,19 @@ ColumnLayout { Layout.margins: Style.current.padding Layout.preferredWidth: parent.width spacing: Style.current.bigPadding - visible: advancedSection.addAccountType !== AdvancedAddAccountView.AddAccountType.WatchOnly && - advancedSection.addAccountType !== AdvancedAddAccountView.AddAccountType.ImportPrivateKey - StatusSelect { + visible: advancedSection.addAccountType !== SelectGeneratedAccount.AddAccountType.ImportPrivateKey && + advancedSection.addAccountType !== SelectGeneratedAccount.AddAccountType.WatchOnly + DerivationPathsPanel { + id: derivationPathsPanel Layout.preferredWidth: 213 - //% "Origin" - label: qsTr("Derivation Path") - selectedItemComponent: StatusListItem { - width: parent.width - icon.background.color: "transparent" - border.width: 1 - border.color: Theme.palette.baseColor2 - title: "Default" - subTitle: "m/44’/61’/0’/1" - enabled: false - } - enabled: false + Layout.alignment: Qt.AlignTop + Component.onCompleted: advancedSection.path = Qt.binding(function() { return derivationPathsPanel.path}) } - StatusSelect { + DerivedAddressesPanel { + id: derivedAddressesPanel Layout.preferredWidth: 213 - //% "Origin" - label: qsTr("Account") - width: parent.width - enabled: false - selectedItemComponent: StatusListItem { - icon.background.color: "transparent" - border.width: 1 - border.color: Theme.palette.baseColor2 - title: "0x1234...abcd" - subTitle: "No activity" - enabled: false - } + Layout.alignment: Qt.AlignTop + Component.onCompleted: advancedSection.pathSubFix = Qt.binding(function() { return derivedAddressesPanel.pathSubFix}) } } } diff --git a/ui/app/AppLayouts/Onboarding/stores/BIP39_en.qml b/ui/imports/shared/stores/BIP39_en.qml similarity index 100% rename from ui/app/AppLayouts/Onboarding/stores/BIP39_en.qml rename to ui/imports/shared/stores/BIP39_en.qml diff --git a/ui/app/AppLayouts/Onboarding/stores/english.txt b/ui/imports/shared/stores/english.txt similarity index 100% rename from ui/app/AppLayouts/Onboarding/stores/english.txt rename to ui/imports/shared/stores/english.txt diff --git a/ui/imports/shared/stores/qmldir b/ui/imports/shared/stores/qmldir index 3b0c907239..fc9c9a5e2d 100644 --- a/ui/imports/shared/stores/qmldir +++ b/ui/imports/shared/stores/qmldir @@ -1 +1,2 @@ singleton RootStore 1.0 RootStore.qml +BIP39_en 1.0 BIP39_en.qml diff --git a/vendor/status-go b/vendor/status-go index e1d98de244..b83f4a6c83 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit e1d98de2440a8a631e8df7516a6a4cb13c26f1a6 +Subproject commit b83f4a6c83d4f2856a44e5fee4137f4f8ffbbaad