feat(@desktop/syncing): make a not operable account fully operable, part 1

- handles recovered keypairs

Closes the first part of #11779
This commit is contained in:
Sale Djenic 2023-08-04 14:41:57 +02:00 committed by saledjenic
parent ec3231ef7e
commit 23fa2f5df3
52 changed files with 1342 additions and 181 deletions

View File

@ -98,8 +98,5 @@ method getKeycardModule*(self: AccessInterface): QVariant {.base.} =
method walletModuleDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method getWalletAccountsModule*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method getWalletNetworksModule*(self: AccessInterface): QVariant {.base.} =
method getWalletModule*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -111,7 +111,7 @@ proc newModule*(delegate: delegate_interface.AccessInterface,
result.keycardModule = keycard_module.newModule(result, events, keycardService, settingsService, networkService,
privacyService, accountsService, walletAccountService, keychainService)
result.walletModule = wallet_module.newModule(result, events, walletAccountService, settingsService, networkService)
result.walletModule = wallet_module.newModule(result, events, accountsService, walletAccountService, settingsService, networkService)
singletonInstance.engine.setRootContextProperty("profileSectionModule", result.viewVariant)
@ -276,8 +276,5 @@ method getKeycardModule*(self: Module): QVariant =
method walletModuleDidLoad*(self: Module) =
self.checkIfModuleDidLoad()
method getWalletAccountsModule*(self: Module): QVariant =
return self.walletModule.getAccountsModuleAsVariant()
method getWalletNetworksModule*(self: Module): QVariant =
return self.walletModule.getNetworksModuleAsVariant()
method getWalletModule*(self: Module): QVariant =
return self.walletModule.getModuleAsVariant()

View File

@ -81,12 +81,7 @@ QtObject:
QtProperty[QVariant] keycardModule:
read = getKeycardModule
proc getWalletAccountsModule(self: View): QVariant {.slot.} =
return self.delegate.getWalletAccountsModule()
QtProperty[QVariant] walletAccountsModule:
read = getWalletAccountsModule
proc getWalletNetworksModule(self: View): QVariant {.slot.} =
return self.delegate.getWalletNetworksModule()
QtProperty[QVariant] walletNetworksModule:
read = getWalletNetworksModule
proc getWalletModule(self: View): QVariant {.slot.} =
return self.delegate.getWalletModule()
QtProperty[QVariant] walletModule:
read = getWalletModule

View File

@ -130,6 +130,10 @@ method load*(self: Module) =
let areTestNetworksEnabled = self.controller.areTestNetworksEnabled()
self.view.onUpdatedAccount(walletAccountToWalletAccountItem(args.account, keycardAccount, areTestNetworksEnabled))
self.events.on(SIGNAL_KEYPAIR_OPERABILITY_CHANGED) do(e:Args):
let args = KeypairArgs(e)
self.view.onUpdatedKeypairOperability(args.keypair.keyUid, AccountFullyOperable)
self.events.on(SIGNAL_NEW_KEYCARD_SET) do(e: Args):
let args = KeycardArgs(e)
if not args.success:

View File

@ -51,6 +51,9 @@ QtObject:
self.accounts.onUpdatedAccount(account)
self.keyPairModel.onUpdatedAccount(account.keyUid, account.address, account.name, account.colorId, account.emoji)
proc onUpdatedKeypairOperability*(self: View, keyUid, operability: string) =
self.keyPairModel.onUpdatedKeypairOperability(keyUid, operability)
proc onPreferredSharingChainsUpdated*(self: View, keyUid, address, prodPreferredChainIds, testPreferredChainIds: string) =
self.keyPairModel.onPreferredSharingChainsUpdated(keyUid, address, prodPreferredChainIds, testPreferredChainIds)

View File

@ -0,0 +1,31 @@
import io_interface
import app/core/eventemitter
import app_service/service/wallet_account/service as wallet_account_service
import app_service/service/devices/service as devices_service
type
Controller* = ref object of RootObj
delegate: io_interface.AccessInterface
events: EventEmitter
walletAccountService: wallet_account_service.Service
proc newController*(
delegate: io_interface.AccessInterface,
events: EventEmitter,
walletAccountService: wallet_account_service.Service,
): Controller =
result = Controller()
result.delegate = delegate
result.events = events
result.walletAccountService = walletAccountService
proc delete*(self: Controller) =
discard
proc init*(self: Controller) =
self.events.on(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE) do(e:Args):
let data = LocalPairingStatus(e)
self.delegate.onLocalPairingStatusUpdate(data)
proc hasPairedDevices*(self: Controller): bool =
return self.walletAccountService.hasPairedDevices()

View File

@ -1,4 +1,6 @@
import NimQml
import app/modules/shared_modules/keypair_import/module as keypair_import_module
import app_service/service/devices/service as devices_service
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
@ -16,18 +18,36 @@ method isLoaded*(self: AccessInterface): bool {.base.} =
# View Delegate Interface
# Delegate for the view must be declared here due to use of QtObject and multi
# inheritance, which is not well supported in Nim.
method viewDidLoad*(self: AccessInterface) {.base.} =
method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
# Methods called by submodules of this module
method accountsModuleDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method getAccountsModule*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method networksModuleDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method getAccountsModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
method getNetworksModule*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method getNetworksModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
method getKeypairImportModule*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method onKeypairImportModuleLoaded*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method destroyKeypairImportPopup*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method runKeypairImportPopup*(self: AccessInterface, keyUid: string, importOption: ImportOption) {.base.} =
raise newException(ValueError, "No implementation available")
method hasPairedDevices*(self: AccessInterface): bool {.base.} =
raise newException(ValueError, "No implementation available")
method onLocalPairingStatusUpdate*(self: AccessInterface, data: LocalPairingStatus) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,16 +1,20 @@
import NimQml, chronicles
import ./io_interface as io_interface
import ./controller, ./view
import ../io_interface as delegate_interface
import ./accounts/module as accounts_module
import ./networks/module as networks_module
import ../../../../global/global_singleton
import ../../../../core/eventemitter
import ../../../../../app_service/service/wallet_account/service as wallet_account_service
import ../../../../../app_service/service/network/service as network_service
import ../../../../../app_service/service/settings/service as settings_service
import app/global/global_singleton
import app/core/eventemitter
import app/modules/shared_modules/keypair_import/module as keypair_import_module
import app_service/service/accounts/service as accounts_service
import app_service/service/wallet_account/service as wallet_account_service
import app_service/service/network/service as network_service
import app_service/service/settings/service as settings_service
import app_service/service/devices/service as devices_service
logScope:
topics = "profile-section-wallet-module"
@ -21,43 +25,56 @@ export io_interface
type
Module* = ref object of io_interface.AccessInterface
delegate: delegate_interface.AccessInterface
controller: Controller
view: View
viewVariant: QVariant
events: EventEmitter
moduleLoaded: bool
accountsService: accounts_service.Service
walletAccountService: wallet_account_service.Service
accountsModule: accounts_module.AccessInterface
networksModule: networks_module.AccessInterface
keypairImportModule: keypair_import_module.AccessInterface
proc newModule*(
delegate: delegate_interface.AccessInterface,
events: EventEmitter,
accountsService: accounts_service.Service,
walletAccountService: wallet_account_service.Service,
settingsService: settings_service.Service,
networkService: network_service.Service,
): Module =
result = Module()
result.delegate = delegate
result.controller = controller.newController(result, events, walletAccountService)
result.view = newView(result)
result.viewVariant = newQVariant(result.view)
result.events = events
result.moduleLoaded = false
result.accountsService = accountsService
result.walletAccountService = walletAccountService
result.accountsModule = accounts_module.newModule(result, events, walletAccountService, networkService)
result.networksModule = networks_module.newModule(result, events, networkService, walletAccountService, settingsService)
method delete*(self: Module) =
self.controller.delete
self.view.delete
self.viewVariant.delete
self.accountsModule.delete
self.networksModule.delete
if not self.keypairImportModule.isNil:
self.keypairImportModule.delete
method load*(self: Module) =
self.controller.init()
self.accountsModule.load()
self.networksModule.load()
method isLoaded*(self: Module): bool =
return self.moduleLoaded
method getAccountsModuleAsVariant*(self: Module): QVariant =
return self.accountsModule.getModuleAsVariant()
method getNetworksModuleAsVariant*(self: Module): QVariant =
return self.networksModule.getModuleAsVariant()
method getModuleAsVariant*(self: Module): QVariant =
return self.viewVariant
proc checkIfModuleDidLoad(self: Module) =
if(not self.accountsModule.isLoaded()):
@ -75,5 +92,37 @@ method viewDidLoad*(self: Module) =
method accountsModuleDidLoad*(self: Module) =
self.checkIfModuleDidLoad()
method getAccountsModule*(self: Module): QVariant =
return self.accountsModule.getModuleAsVariant()
method networksModuleDidLoad*(self: Module) =
self.checkIfModuleDidLoad()
method getNetworksModule*(self: Module): QVariant =
return self.networksModule.getModuleAsVariant()
method destroyKeypairImportPopup*(self: Module) =
if self.keypairImportModule.isNil:
return
self.view.emitDestroyKeypairImportPopup()
self.keypairImportModule.delete
self.keypairImportModule = nil
method runKeypairImportPopup*(self: Module, keyUid: string, importOption: ImportOption) =
self.keypairImportModule = keypair_import_module.newModule(self, self.events, self.accountsService, self.walletAccountService)
self.keypairImportModule.load(keyUid, importOption)
method getKeypairImportModule*(self: Module): QVariant =
if self.keypairImportModule.isNil:
return newQVariant()
return self.keypairImportModule.getModuleAsVariant()
method onKeypairImportModuleLoaded*(self: Module) =
self.view.emitDisplayKeypairImportPopup()
method hasPairedDevices*(self: Module): bool =
return self.controller.hasPairedDevices()
method onLocalPairingStatusUpdate*(self: Module, data: LocalPairingStatus) =
if data.state == LocalPairingState.Finished:
self.view.emitHasPairedDevicesChangedSignal()

View File

@ -0,0 +1,52 @@
import NimQml
import ./io_interface
from app/modules/shared_modules/keypair_import/module import ImportOption
QtObject:
type
View* = ref object of QObject
delegate: io_interface.AccessInterface
proc delete*(self: View) =
self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View =
new(result, delete)
result.QObject.setup
result.delegate = delegate
proc getAccountsModule(self: View): QVariant {.slot.} =
return self.delegate.getAccountsModule()
QtProperty[QVariant] accountsModule:
read = getAccountsModule
proc getNetworksModule(self: View): QVariant {.slot.} =
return self.delegate.getNetworksModule()
QtProperty[QVariant] networksModule:
read = getNetworksModule
proc runKeypairImportPopup*(self: View, keyUid: string, importOption: int) {.slot.} =
self.delegate.runKeypairImportPopup(keyUid, ImportOption(importOption))
proc getKeypairImportModule(self: View): QVariant {.slot.} =
return self.delegate.getKeypairImportModule()
QtProperty[QVariant] keypairImportModule:
read = getKeypairImportModule
proc displayKeypairImportPopup*(self: View) {.signal.}
proc emitDisplayKeypairImportPopup*(self: View) =
self.displayKeypairImportPopup()
proc destroyKeypairImportPopup*(self: View) {.signal.}
proc emitDestroyKeypairImportPopup*(self: View) =
self.destroyKeypairImportPopup()
proc hasPairedDevicesChanged*(self: View) {.signal.}
proc emitHasPairedDevicesChangedSignal*(self: View) =
self.hasPairedDevicesChanged()
proc getHasPairedDevices(self: View): bool {.slot.} =
return self.delegate.hasPairedDevices()
QtProperty[bool] hasPairedDevices:
read = getHasPairedDevices
notify = hasPairedDevicesChanged

View File

@ -10,82 +10,62 @@ export keypair_item
logScope:
topics = "shared-keypairs"
proc buildKeyPairsList*(keypairs: seq[KeypairDto], excludeAlreadyMigratedPairs: bool,
excludePrivateKeyKeypairs: bool, areTestNetworksEnabled: bool = false): seq[KeyPairItem] =
var items: seq[KeyPairItem]
for kp in keypairs:
if kp.accounts.len == 0:
proc buildKeypairItem*(keypair: KeypairDto, areTestNetworksEnabled: bool): KeyPairItem =
if keypair.accounts.len == 0:
## we should never be here
error "there must not be any keypair without accounts", keyUid=kp.keyUid
error "there must not be any keypair without accounts", keyUid=keypair.keyUid
return
let publicKey = kp.accounts[0].publicKey # in case of other but the profile keypair we take public key of first account as keypair's public key
let kpMigrated = kp.keycards.len > 0
if excludeAlreadyMigratedPairs and kpMigrated:
continue
if kp.keypairType == KeypairTypeProfile:
var item = newKeyPairItem(keyUid = kp.keyUid,
pubKey = singletonInstance.userProfile.getPubKey(),
let publicKey = keypair.accounts[0].publicKey # in case of other but the profile keypair we take public key of first account as keypair's public key
var item = newKeyPairItem(keyUid = keypair.keyUid,
pubKey = publicKey,
locked = false,
name = singletonInstance.userProfile.getName(),
image = singletonInstance.userProfile.getIcon(),
name = keypair.name,
image = "",
icon = "",
pairType = KeyPairType.Profile,
derivedFrom = kp.derivedFrom,
lastUsedDerivationIndex = kp.lastUsedDerivationIndex,
migratedToKeycard = kpMigrated,
syncedFrom = kp.syncedFrom)
for acc in kp.accounts:
pairType = KeyPairType.Unknown,
derivedFrom = keypair.derivedFrom,
lastUsedDerivationIndex = keypair.lastUsedDerivationIndex,
migratedToKeycard = keypair.keycards.len > 0,
syncedFrom = keypair.syncedFrom)
if keypair.keypairType == KeypairTypeProfile:
item.setPubKey(singletonInstance.userProfile.getPubKey())
item.setName(singletonInstance.userProfile.getName())
item.setImage(singletonInstance.userProfile.getIcon())
item.setPairType(KeyPairType.Profile.int)
elif keypair.keypairType == KeypairTypeSeed:
item.setIcon(if item.getMigratedToKeycard(): "keycard" else: "key_pair_seed_phrase")
item.setPairType(KeyPairType.SeedImport.int)
elif keypair.keypairType == KeypairTypeKey:
item.setIcon(if item.getMigratedToKeycard(): "keycard" else: "objects")
item.setPairType(KeyPairType.PrivateKeyImport.int)
for acc in keypair.accounts:
if acc.isChat:
continue
var icon = ""
if acc.emoji.len == 0:
icon = "wallet"
item.addAccount(newKeyPairAccountItem(acc.name, acc.path, acc.address, acc.publicKey, acc.emoji, acc.colorId,
icon, newCurrencyAmount(), balanceFetched = true, operability = acc.operable, acc.isWallet, areTestNetworksEnabled, acc.prodPreferredChainIds, acc.testPreferredChainIds))
icon, newCurrencyAmount(), balanceFetched = true, operability = acc.operable, acc.isWallet, areTestNetworksEnabled,
acc.prodPreferredChainIds, acc.testPreferredChainIds))
return item
proc buildKeyPairsList*(keypairs: seq[KeypairDto], excludeAlreadyMigratedPairs: bool,
excludePrivateKeyKeypairs: bool, areTestNetworksEnabled: bool = false): seq[KeyPairItem] =
var items: seq[KeyPairItem]
for kp in keypairs:
let item = buildKeypairItem(kp, areTestNetworksEnabled)
if item.isNil:
continue
if excludeAlreadyMigratedPairs and item.getMigratedToKeycard():
continue
if item.getPairType() == KeypairType.Profile.int:
items.insert(item, 0) # Status Account must be at first place
continue
if kp.keypairType == KeypairTypeSeed:
var item = newKeyPairItem(keyUid = kp.keyUid,
pubKey = publicKey,
locked = false,
name = kp.name,
image = "",
icon = if kpMigrated: "keycard" else: "key_pair_seed_phrase",
pairType = KeyPairType.SeedImport,
derivedFrom = kp.derivedFrom,
lastUsedDerivationIndex = kp.lastUsedDerivationIndex,
migratedToKeycard = kpMigrated,
syncedFrom = kp.syncedFrom)
for acc in kp.accounts:
var icon = ""
if acc.emoji.len == 0:
icon = "wallet"
item.addAccount(newKeyPairAccountItem(acc.name, acc.path, acc.address, acc.publicKey, acc.emoji, acc.colorId,
icon, newCurrencyAmount(), balanceFetched = true, operability = acc.operable, acc.isWallet, areTestNetworksEnabled, acc.prodPreferredChainIds, acc.testPreferredChainIds))
if item.getPairType() == KeypairType.PrivateKeyImport.int and excludePrivateKeyKeypairs:
continue
items.add(item)
continue
if kp.keypairType == KeypairTypeKey:
if excludePrivateKeyKeypairs:
continue
var item = newKeyPairItem(keyUid = kp.keyUid,
pubKey = publicKey,
locked = false,
name = kp.name,
image = "",
icon = if kpMigrated: "keycard" else: "objects",
pairType = KeyPairType.PrivateKeyImport,
derivedFrom = kp.derivedFrom,
lastUsedDerivationIndex = kp.lastUsedDerivationIndex,
migratedToKeycard = kpMigrated,
syncedFrom = kp.syncedFrom)
for acc in kp.accounts:
var icon = ""
if acc.emoji.len == 0:
icon = "wallet"
item.addAccount(newKeyPairAccountItem(acc.name, acc.path, acc.address, acc.publicKey, acc.emoji, acc.colorId,
icon, newCurrencyAmount(), balanceFetched = true, operability = acc.operable, acc.isWallet, areTestNetworksEnabled, acc.prodPreferredChainIds, acc.testPreferredChainIds))
items.add(item)
continue
if items.len == 0:
debug "sm_there is no any key pair for the logged in user that is not already migrated to a keycard"
return items

View File

@ -125,9 +125,8 @@ QtObject:
self.items[i].setEmoji(emoji)
return
proc updateOperabilityForAddress*(self: KeyPairAccountModel, address: string, operability: string) =
proc updateOperabilityForAllAddresses*(self: KeyPairAccountModel, operability: string) =
for i in 0 ..< self.items.len:
if cmpIgnoreCase(self.items[i].getAddress(), address) == 0:
self.items[i].setOperability(operability)
proc setBalanceForAddress*(self: KeyPairAccountModel, address: string, balance: CurrencyAmount) =

View File

@ -270,8 +270,8 @@ QtObject:
self.accounts.updatePreferredSharingChainsForAddress(address, prodPreferredChainIds, testPreferredChainIds)
proc setBalanceForAddress*(self: KeyPairItem, address: string, balance: CurrencyAmount) =
self.accounts.setBalanceForAddress(address, balance)
proc updateOperabilityForAccountWithAddress*(self: KeyPairItem, address: string, operability: string) =
self.accounts.updateOperabilityForAddress(address, operability)
proc updateOperabilityForAllAddresses*(self: KeyPairItem, operability: string) =
self.accounts.updateOperabilityForAllAddresses(operability)
self.operabilityChanged()
proc setItem*(self: KeyPairItem, item: KeyPairItem) =

View File

@ -82,6 +82,12 @@ QtObject:
item.getAccountsModel().updateDetailsForAddressIfTheyAreSet(address, name, colorId, emoji)
break
proc onUpdatedKeypairOperability*(self: KeyPairModel, keyUid, operability: string) =
for item in self.items:
if keyUid == item.getKeyUid():
item.updateOperabilityForAllAddresses(operability)
break
proc onPreferredSharingChainsUpdated*(self: KeyPairModel,keyUid, address, prodPreferredChainIds, testPreferredChainIds: string) =
for item in self.items:
if keyUid == item.getKeyUid():

View File

@ -1,7 +1,7 @@
import NimQml, Tables, strutils, sequtils, sugar, chronicles
import io_interface
import view, controller, derived_address_model
import view, controller
import internal/[state, state_factory]
import ../../../core/eventemitter
@ -9,7 +9,7 @@ import ../../../core/eventemitter
import ../../../global/global_singleton
import ../../shared/keypairs
import ../../shared_models/[keypair_model]
import ../../shared_models/[keypair_model, derived_address_model]
import ../../shared_modules/keycard_popup/module as keycard_shared_module
import ../../../../app_service/common/account_constants

View File

@ -1,8 +1,7 @@
import NimQml
import io_interface
import derived_address_model
import internal/[state, state_wrapper]
import ../../shared_models/[keypair_model, keypair_item]
import ../../shared_models/[keypair_model, keypair_item, derived_address_model]
QtObject:
type

View File

@ -0,0 +1,96 @@
import times, chronicles
import io_interface
import app/core/eventemitter
import app_service/service/accounts/service as accounts_service
import app_service/service/wallet_account/service as wallet_account_service
import app/modules/shared_modules/keycard_popup/io_interface as keycard_shared_module
logScope:
topics = "wallet-keycard-import-controller"
const UNIQUE_WALLET_SECTION_KEYPAIR_IMPORT_MODULE_IDENTIFIER* = "WalletSection-KeypairImportModule"
type
Controller* = ref object of RootObj
delegate: io_interface.AccessInterface
events: EventEmitter
accountsService: accounts_service.Service
walletAccountService: wallet_account_service.Service
uniqueFetchingDetailsId: string
tmpPrivateKey: string
tmpSeedPhrase: string
tmpGeneratedAccount: GeneratedAccountDto
proc newController*(delegate: io_interface.AccessInterface,
events: EventEmitter,
accountsService: accounts_service.Service,
walletAccountService: wallet_account_service.Service):
Controller =
result = Controller()
result.delegate = delegate
result.events = events
result.accountsService = accountsService
result.walletAccountService = walletAccountService
proc delete*(self: Controller) =
discard
proc init*(self: Controller) =
self.events.on(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args):
let args = SharedKeycarModuleArgs(e)
if args.uniqueIdentifier != UNIQUE_WALLET_SECTION_KEYPAIR_IMPORT_MODULE_IDENTIFIER:
return
self.delegate.onUserAuthenticated(args.pin, args.password, args.keyUid)
self.events.on(SIGNAL_WALLET_ACCOUNT_ADDRESS_DETAILS_FETCHED) do(e:Args):
var args = DerivedAddressesArgs(e)
if args.uniqueId != self.uniqueFetchingDetailsId:
return
self.delegate.onAddressDetailsFetched(args.derivedAddresses, args.error)
proc closeKeypairImportPopup*(self: Controller) =
self.delegate.closeKeypairImportPopup()
proc setPrivateKey*(self: Controller, value: string) =
self.tmpPrivateKey = value
proc getPrivateKey*(self: Controller): string =
return self.tmpPrivateKey
proc setSeedPhrase*(self: Controller, value: string) =
self.tmpSeedPhrase = value
proc getSeedPhrase*(self: Controller): string =
return self.tmpSeedPhrase
proc authenticateLoggedInUser*(self: Controller) =
let data = SharedKeycarModuleAuthenticationArgs(uniqueIdentifier: UNIQUE_WALLET_SECTION_KEYPAIR_IMPORT_MODULE_IDENTIFIER)
self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data)
proc getKeypairByKeyUid*(self: Controller, keyUid: string): KeypairDto =
return self.walletAccountService.getKeypairByKeyUid(keyUid)
proc createAccountFromPrivateKey*(self: Controller, privateKey: string): GeneratedAccountDto =
self.setPrivateKey(privateKey)
self.tmpGeneratedAccount = self.accountsService.createAccountFromPrivateKey(privateKey)
return self.tmpGeneratedAccount
proc createAccountFromSeedPhrase*(self: Controller, seedPhrase: string): GeneratedAccountDto =
self.setSeedPhrase(seedPhrase)
self.tmpGeneratedAccount = self.accountsService.createAccountFromMnemonic(seedPhrase)
return self.tmpGeneratedAccount
proc getGeneratedAccount*(self: Controller): GeneratedAccountDto =
return self.tmpGeneratedAccount
proc fetchDetailsForAddresses*(self: Controller, addresses: seq[string]) =
self.uniqueFetchingDetailsId = $now().toTime().toUnix()
self.walletAccountService.fetchDetailsForAddresses(self.uniqueFetchingDetailsId, addresses)
proc makePrivateKeyKeypairFullyOperable*(self: Controller, keyUid, privateKey, password: string, doPasswordHashing: bool): string =
return self.walletAccountService.makePrivateKeyKeypairFullyOperable(keyUid, privateKey, password, doPasswordHashing)
proc makeSeedPhraseKeypairFullyOperable*(self: Controller, keyUid, mnemonic, password: string, doPasswordHashing: bool): string =
return self.walletAccountService.makeSeedPhraseKeypairFullyOperable(keyUid, mnemonic, password, doPasswordHashing)

View File

@ -0,0 +1,12 @@
type
ImportPrivateKeyState* = ref object of State
proc newImportPrivateKeyState*(backState: State): ImportPrivateKeyState =
result = ImportPrivateKeyState()
result.setup(StateType.ImportPrivateKey, backState)
proc delete*(self: ImportPrivateKeyState) =
self.State.delete
method executePrePrimaryStateCommand*(self: ImportPrivateKeyState, controller: Controller) =
controller.authenticateLoggedInUser()

View File

@ -0,0 +1,12 @@
type
ImportSeedPhraseState* = ref object of State
proc newImportSeedPhraseState*(backState: State): ImportSeedPhraseState =
result = ImportSeedPhraseState()
result.setup(StateType.ImportSeedPhrase, backState)
proc delete*(self: ImportSeedPhraseState) =
self.State.delete
method executePrePrimaryStateCommand*(self: ImportSeedPhraseState, controller: Controller) =
controller.authenticateLoggedInUser()

View File

@ -0,0 +1,81 @@
import ../controller
type StateType* {.pure.} = enum
NoState = "NoState"
Main = "Main"
ImportSeedPhrase = "ImportSeedPhrase"
ImportPrivateKey = "ImportPrivateKey"
## This is the base class for all states
## We should not instance of this class (in c++ this will be an abstract class).
type
State* {.pure inheritable.} = ref object of RootObj
stateType: StateType
backState: State
proc setup*(self: State, stateType: StateType, backState: State) =
self.stateType = stateType
self.backState = backState
## `stateType` - detemines the state this instance describes
## `backState` - the sate (instance) we're moving to if user clicks "back" button,
## in case we should not display "back" button for this state, set it to `nil`
proc newState*(self: State, stateType: StateType, backState: State): State =
result = State()
result.setup(stateType, backState)
proc delete*(self: State) =
discard
## Returns state type
method stateType*(self: State): StateType {.inline base.} =
self.stateType
## Returns back state instance
method getBackState*(self: State): State {.inline base.} =
self.backState
## Returns true if we should display "back" button, otherwise false
method displayBackButton*(self: State): bool {.inline base.} =
return not self.backState.isNil
## Returns next state instance if "primary" action is triggered
method getNextPrimaryState*(self: State, controller: Controller): State {.inline base.} =
return nil
## Returns next state instance if "secondary" action is triggered
method getNextSecondaryState*(self: State, controller: Controller): State {.inline base.} =
return nil
## Returns next state instance in case the "tertiary" action is triggered
method getNextTertiaryState*(self: State, controller: Controller): State {.inline base.} =
return nil
## Returns next state instance in case the "quaternary" action is triggered
method getNextQuaternaryState*(self: State, controller: Controller): State {.inline base.} =
return nil
## This method is executed if "cancel" action is triggered (invalidates current flow)
method executeCancelCommand*(self: State, controller: Controller) {.inline base.} =
controller.closeKeypairImportPopup()
## This method is executed before back state is set, if "back" action is triggered
method executePreBackStateCommand*(self: State, controller: Controller) {.inline base.} =
discard
## This method is executed before primary state is set, if "primary" action is triggered
method executePrePrimaryStateCommand*(self: State, controller: Controller) {.inline base.} =
discard
## This method is executed before secondary state is set, if "secondary" action is triggered
method executePreSecondaryStateCommand*(self: State, controller: Controller) {.inline base.} =
discard
## This method is executed in case "tertiary" action is triggered
method executePreTertiaryStateCommand*(self: State, controller: Controller) {.inline base.} =
discard
## This method is executed in case "quaternary" action is triggered
method executePreQuaternaryStateCommand*(self: State, controller: Controller) {.inline base.} =
discard

View File

@ -0,0 +1,21 @@
import chronicles
import ../controller
import state
logScope:
topics = "keypair-import-module-state-factory"
# Forward declaration
proc createState*(stateToBeCreated: StateType, backState: State): State
include import_private_key_state
include import_seed_phrase_state
proc createState*(stateToBeCreated: StateType, backState: State): State =
if stateToBeCreated == StateType.ImportPrivateKey:
return newImportPrivateKeyState(backState)
if stateToBeCreated == StateType.ImportSeedPhrase:
return newImportSeedPhraseState(backState)
error "Keypair import - no implementation available for state", state=stateToBeCreated

View File

@ -0,0 +1,62 @@
import NimQml
import state
QtObject:
type StateWrapper* = ref object of QObject
stateObj: State
proc delete*(self: StateWrapper) =
self.QObject.delete
proc newStateWrapper*(): StateWrapper =
new(result, delete)
result.QObject.setup()
proc stateWrapperChanged*(self:StateWrapper) {.signal.}
proc setStateObj*(self: StateWrapper, stateObj: State) =
self.stateObj = stateObj
self.stateWrapperChanged()
proc getStateObj*(self: StateWrapper): State =
return self.stateObj
proc getStateType(self: StateWrapper): string {.slot.} =
if(self.stateObj.isNil):
return $StateType.NoState
return $self.stateObj.stateType()
QtProperty[string] stateType:
read = getStateType
notify = stateWrapperChanged
proc getDisplayBackButton(self: StateWrapper): bool {.slot.} =
if(self.stateObj.isNil):
return false
return self.stateObj.displayBackButton()
QtProperty[bool] displayBackButton:
read = getDisplayBackButton
notify = stateWrapperChanged
proc backActionClicked*(self: StateWrapper) {.signal.}
proc doBackAction*(self: StateWrapper) {.slot.} =
self.backActionClicked()
proc cancelActionClicked*(self: StateWrapper) {.signal.}
proc doCancelAction*(self: StateWrapper) {.slot.} =
self.cancelActionClicked()
proc primaryActionClicked*(self: StateWrapper) {.signal.}
proc doPrimaryAction*(self: StateWrapper) {.slot.} =
self.primaryActionClicked()
proc secondaryActionClicked*(self: StateWrapper) {.signal.}
proc doSecondaryAction*(self: StateWrapper) {.slot.} =
self.secondaryActionClicked()
proc tertiaryActionClicked*(self: StateWrapper) {.signal.}
proc doTertiaryAction*(self: StateWrapper) {.slot.} =
self.tertiaryActionClicked()
proc quaternaryActionClicked*(self: StateWrapper) {.signal.}
proc doQuaternaryAction*(self: StateWrapper) {.slot.} =
self.quaternaryActionClicked()

View File

@ -0,0 +1,51 @@
import Tables, NimQml
import app_service/service/wallet_account/dto/derived_address_dto
type ImportOption* {.pure.}= enum
SeedPhrase = 1,
PrivateKey = 2
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
method delete*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method load*(self: AccessInterface, keyUid: string, importOption: ImportOption) {.base.} =
raise newException(ValueError, "No implementation available")
method closeKeypairImportPopup*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method onBackActionClicked*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onPrimaryActionClicked*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onCancelActionClicked*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onUserAuthenticated*(self: AccessInterface, pin: string, password: string, keyUid: string) {.base.} =
raise newException(ValueError, "No implementation available")
method changePrivateKey*(self: AccessInterface, privateKey: string) {.base.} =
raise newException(ValueError, "No implementation available")
method changeSeedPhrase*(self: AccessInterface, seedPhrase: string) {.base.} =
raise newException(ValueError, "No implementation available")
method validSeedPhrase*(self: AccessInterface, seedPhrase: string): bool {.base.} =
raise newException(ValueError, "No implementation available")
method onAddressDetailsFetched*(self: AccessInterface, derivedAddresses: seq[DerivedAddressDto], error: string) {.base.} =
raise newException(ValueError, "No implementation available")
type
DelegateInterface* = concept c
c.onKeypairImportModuleLoaded()
c.destroyKeypairImportPopup()

View File

@ -0,0 +1,188 @@
import NimQml, strutils, chronicles
import io_interface
import view, controller
import internal/[state, state_factory]
import app/global/global_singleton
import app/core/eventemitter
import app/modules/shared/keypairs
import app/modules/shared_models/[derived_address_model]
import app_service/service/accounts/service as accounts_service
import app_service/service/wallet_account/service as wallet_account_service
export io_interface
logScope:
topics = "wallet-keypair-import-module"
type
Module*[T: io_interface.DelegateInterface] = ref object of io_interface.AccessInterface
delegate: T
events: EventEmitter
view: View
viewVariant: QVariant
controller: Controller
proc newModule*[T](delegate: T,
events: EventEmitter,
accountsService: accounts_service.Service,
walletAccountService: wallet_account_service.Service):
Module[T] =
result = Module[T]()
result.delegate = delegate
result.view = newView(result)
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events, accountsService, walletAccountService)
method delete*[T](self: Module[T]) =
self.view.delete
self.viewVariant.delete
self.controller.delete
method closeKeypairImportPopup*[T](self: Module[T]) =
self.delegate.destroyKeypairImportPopup()
method getModuleAsVariant*[T](self: Module[T]): QVariant =
return self.viewVariant
method load*[T](self: Module[T], keyUid: string, importOption: ImportOption) =
self.controller.init()
let keypair = self.controller.getKeypairByKeyUid(keyUid)
if keypair.isNil:
error "trying to import an unknown keypair"
self.closeKeypairImportPopup()
return
let keypairItem = buildKeypairItem(keypair, areTestNetworksEnabled = false) # testnetworks are irrelevant in this context
if keypairItem.isNil:
error "cannot generate keypair item for provided keypair"
self.closeKeypairImportPopup()
return
self.view.setSelectedKeypair(keypairItem)
if importOption == ImportOption.PrivateKey:
self.view.setCurrentState(newImportPrivateKeyState(nil))
elif importOption == ImportOption.SeedPhrase:
self.view.setCurrentState(newImportSeedPhraseState(nil))
self.delegate.onKeypairImportModuleLoaded()
method onBackActionClicked*[T](self: Module[T]) =
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "ki_cannot resolve current state"
return
debug "ki_back_action", currState=currStateObj.stateType()
currStateObj.executePreBackStateCommand(self.controller)
let backState = currStateObj.getBackState()
if backState.isNil:
return
self.view.setCurrentState(backState)
debug "ki_back_action - set state", newCurrState=backState.stateType()
method onCancelActionClicked*[T](self: Module[T]) =
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "ki_cannot resolve current state"
return
debug "ki_cancel_action", currState=currStateObj.stateType()
currStateObj.executeCancelCommand(self.controller)
method onPrimaryActionClicked*[T](self: Module[T]) =
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "ki_cannot resolve current state"
return
debug "ki_primary_action", currState=currStateObj.stateType()
currStateObj.executePrePrimaryStateCommand(self.controller)
let nextState = currStateObj.getNextPrimaryState(self.controller)
if nextState.isNil:
return
self.view.setCurrentState(nextState)
debug "ki_primary_action - set state", setCurrState=nextState.stateType()
proc authenticateLoggedInUser[T](self: Module[T]) =
self.controller.authenticateLoggedInUser()
method changePrivateKey*[T](self: Module[T], privateKey: string) =
self.view.setPrivateKeyAccAddress(newDerivedAddressItem())
if privateKey.len == 0:
return
let genAccDto = self.controller.createAccountFromPrivateKey(privateKey)
if genAccDto.address.len == 0:
error "unable to resolve an address from the provided private key"
return
let kp = self.view.getSelectedKeypair()
if kp.isNil:
# should never be here
return
if kp.getKeyUid() != genAccDto.keyUid:
self.view.setEnteredPrivateKeyMatchTheKeypair(false)
error "entered private key doesn't refer to a keyapir being imported"
return
self.view.setEnteredPrivateKeyMatchTheKeypair(true)
self.view.setPrivateKeyAccAddress(newDerivedAddressItem(order = 0, address = genAccDto.address, publicKey = genAccDto.publicKey))
self.controller.fetchDetailsForAddresses(@[genAccDto.address])
method changeSeedPhrase*[T](self: Module[T], seedPhrase: string) =
if seedPhrase.len == 0:
return
let genAccDto = self.controller.createAccountFromSeedPhrase(seedPhrase)
if seedPhrase.len > 0 and genAccDto.address.len == 0:
error "unable to create an account from the provided seed phrase"
return
method validSeedPhrase*[T](self: Module[T], seedPhrase: string): bool =
let genAccDto = self.controller.createAccountFromSeedPhrase(seedPhrase)
let kp = self.view.getSelectedKeypair()
if kp.isNil:
# should never be here
return false
return kp.getKeyUid() == genAccDto.keyUid
method onAddressDetailsFetched*[T](self: Module[T], derivedAddresses: seq[DerivedAddressDto], error: string) =
if error.len > 0:
error "ki_fetching address details error", err=error
return
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "ki_cannot resolve current state"
return
# we always receive responses one by one
if derivedAddresses.len == 1:
var addressDetailsItem = newDerivedAddressItem(order = 0,
address = derivedAddresses[0].address,
publicKey = derivedAddresses[0].publicKey,
path = derivedAddresses[0].path,
alreadyCreated = derivedAddresses[0].alreadyCreated,
hasActivity = derivedAddresses[0].hasActivity,
loaded = true)
if currStateObj.stateType() == StateType.ImportPrivateKey:
if cmpIgnoreCase(self.view.getPrivateKeyAccAddress().getAddress(), addressDetailsItem.getAddress()) == 0:
self.view.setPrivateKeyAccAddress(addressDetailsItem)
return
error "ki_unknown error, since the length of the response is not expected", length=derivedAddresses.len
method onUserAuthenticated*[T](self: Module[T], pin: string, password: string, keyUid: string) =
if password.len == 0:
info "ki_unsuccessful authentication"
return
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "ki_cannot resolve current state"
return
if currStateObj.stateType() == StateType.ImportPrivateKey:
let res = self.controller.makePrivateKeyKeypairFullyOperable(self.controller.getGeneratedAccount().keyUid,
self.controller.getPrivateKey(),
password,
doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser())
if res.len > 0:
error "ki_unable to make a keypair operable"
return
if currStateObj.stateType() == StateType.ImportSeedPhrase:
let res = self.controller.makeSeedPhraseKeypairFullyOperable(self.controller.getGeneratedAccount().keyUid,
self.controller.getSeedPhrase(),
password,
doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser())
if res.len > 0:
error "ki_unable to make a keypair operable"
return
self.closeKeypairImportPopup()

View File

@ -0,0 +1,102 @@
import NimQml
import io_interface
import internal/[state, state_wrapper]
import app/modules/shared_models/[keypair_item, derived_address_model]
QtObject:
type
View* = ref object of QObject
delegate: io_interface.AccessInterface
currentState: StateWrapper
currentStateVariant: QVariant
selectedKeypair: KeyPairItem
selectedKeypairVariant: QVariant
privateKeyAccAddress: DerivedAddressItem
privateKeyAccAddressVariant: QVariant
enteredPrivateKeyMatchTheKeypair: bool
proc delete*(self: View) =
self.currentStateVariant.delete
self.currentState.delete
self.selectedKeypair.delete
self.selectedKeypairVariant.delete
self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View =
new(result, delete)
result.QObject.setup
result.delegate = delegate
result.currentState = newStateWrapper()
result.currentStateVariant = newQVariant(result.currentState)
result.selectedKeypair = newKeyPairItem()
result.selectedKeypairVariant = newQVariant(result.selectedKeypair)
result.privateKeyAccAddress = newDerivedAddressItem()
result.privateKeyAccAddressVariant = newQVariant(result.privateKeyAccAddress)
result.enteredPrivateKeyMatchTheKeypair = false
signalConnect(result.currentState, "backActionClicked()", result, "onBackActionClicked()", 2)
signalConnect(result.currentState, "cancelActionClicked()", result, "onCancelActionClicked()", 2)
signalConnect(result.currentState, "primaryActionClicked()", result, "onPrimaryActionClicked()", 2)
proc currentStateObj*(self: View): State =
return self.currentState.getStateObj()
proc setCurrentState*(self: View, state: State) =
self.currentState.setStateObj(state)
proc getCurrentState(self: View): QVariant {.slot.} =
return self.currentStateVariant
QtProperty[QVariant] currentState:
read = getCurrentState
proc onBackActionClicked*(self: View) {.slot.} =
self.delegate.onBackActionClicked()
proc onCancelActionClicked*(self: View) {.slot.} =
self.delegate.onCancelActionClicked()
proc onPrimaryActionClicked*(self: View) {.slot.} =
self.delegate.onPrimaryActionClicked()
proc getSelectedKeypair*(self: View): KeyPairItem =
return self.selectedKeypair
proc getSelectedKeypairAsVariant*(self: View): QVariant {.slot.} =
return self.selectedKeypairVariant
QtProperty[QVariant] selectedKeypair:
read = getSelectedKeypairAsVariant
proc setSelectedKeypair*(self: View, item: KeyPairItem) =
self.selectedKeypair.setItem(item)
proc getPrivateKeyAccAddress*(self: View): DerivedAddressItem =
return self.privateKeyAccAddress
proc privateKeyAccAddressChanged*(self: View) {.signal.}
proc getPrivateKeyAccAddressVariant*(self: View): QVariant {.slot.} =
return self.privateKeyAccAddressVariant
QtProperty[QVariant] privateKeyAccAddress:
read = getPrivateKeyAccAddressVariant
notify = privateKeyAccAddressChanged
proc setPrivateKeyAccAddress*(self: View, item: DerivedAddressItem) =
self.privateKeyAccAddress.setItem(item)
self.privateKeyAccAddressChanged()
proc changePrivateKey*(self: View, privateKey: string) {.slot.} =
self.delegate.changePrivateKey(privateKey)
proc changeSeedPhrase*(self: View, seedPhrase: string) {.slot.} =
self.delegate.changeSeedPhrase(seedPhrase)
proc validSeedPhrase*(self: View, seedPhrase: string): bool {.slot.} =
return self.delegate.validSeedPhrase(seedPhrase)
proc enteredPrivateKeyMatchTheKeypairChanged(self: View) {.signal.}
proc getEnteredPrivateKeyMatchTheKeypair*(self: View): bool {.slot.} =
return self.enteredPrivateKeyMatchTheKeypair
QtProperty[bool] enteredPrivateKeyMatchTheKeypair:
read = getEnteredPrivateKeyMatchTheKeypair
notify = enteredPrivateKeyMatchTheKeypairChanged
proc setEnteredPrivateKeyMatchTheKeypair*(self: View, value: bool) =
self.enteredPrivateKeyMatchTheKeypair = value
self.enteredPrivateKeyMatchTheKeypairChanged()

View File

@ -215,6 +215,15 @@ proc removeAccountFromLocalStoreAndNotify(self: Service, address: string, notify
if notify:
self.events.emit(SIGNAL_WALLET_ACCOUNT_DELETED, AccountArgs(account: acc))
proc updateKeypairOperabilityInLocalStoreAndNotify(self: Service, keyUid: string) =
let kp = self.getKeypairByKeyUid(keyUid)
if kp.isNil:
error "there is no known keypair", keyUid=keyUid, procName="updateKeypairOperabilityInLocalStoreAndNotify"
return
for acc in kp.accounts:
acc.operable = AccountFullyOperable
self.events.emit(SIGNAL_KEYPAIR_OPERABILITY_CHANGED, KeypairArgs(keypair: kp))
proc updateAccountsPositions(self: Service) =
let dbAccounts = getAccountsFromDb()
for dbAcc in dbAccounts:
@ -305,6 +314,24 @@ proc addNewPrivateKeyKeypair*(self: Service, privateKey, password: string, doPas
error "error: ", procName="addNewPrivateKeyKeypair", errName=e.name, errDesription=e.msg
return e.msg
proc makePrivateKeyKeypairFullyOperable*(self: Service, keyUid, privateKey, password: string, doPasswordHashing: bool): string =
if password.len == 0:
error "for making a private key keypair fully operable, password must be provided"
return
var finalPassword = password
if doPasswordHashing:
finalPassword = utils.hashPassword(password)
try:
var response = status_go_accounts.makePrivateKeyKeypairFullyOperable(privateKey, finalPassword)
if not response.error.isNil:
error "status-go error", procName="makePrivateKeyKeypairFullyOperable", errCode=response.error.code, errDesription=response.error.message
return response.error.message
self.updateKeypairOperabilityInLocalStoreAndNotify(keyUid)
return ""
except Exception as e:
error "error: ", procName="makePrivateKeyKeypairFullyOperable", errName=e.name, errDesription=e.msg
return e.msg
## Mandatory fields for all accounts: `address`, `keyUid`, `walletType`, `path`, `publicKey`, `name`, `emoji`, `colorId`
proc addNewSeedPhraseKeypair*(self: Service, seedPhrase, password: string, doPasswordHashing: bool,
keyUid, keypairName, rootWalletMasterKey: string, accounts: seq[WalletAccountDto]): string =
@ -328,6 +355,24 @@ proc addNewSeedPhraseKeypair*(self: Service, seedPhrase, password: string, doPas
error "error: ", procName="addNewSeedPhraseKeypair", errName=e.name, errDesription=e.msg
return e.msg
proc makeSeedPhraseKeypairFullyOperable*(self: Service, keyUid, mnemonic, password: string, doPasswordHashing: bool): string =
if password.len == 0:
error "for making a private key keypair fully operable, password must be provided"
return
var finalPassword = password
if doPasswordHashing:
finalPassword = utils.hashPassword(password)
try:
var response = status_go_accounts.makeSeedPhraseKeypairFullyOperable(mnemonic, finalPassword)
if not response.error.isNil:
error "status-go error", procName="makeSeedPhraseKeypairFullyOperable", errCode=response.error.code, errDesription=response.error.message
return response.error.message
self.updateKeypairOperabilityInLocalStoreAndNotify(keyUid)
return ""
except Exception as e:
error "error: ", procName="makeSeedPhraseKeypairFullyOperable", errName=e.name, errDesription=e.msg
return e.msg
proc getRandomMnemonic*(self: Service): string =
try:
let response = status_go_accounts.getRandomMnemonic()
@ -591,3 +636,6 @@ proc getCurrencyFormat*(self: Service, symbol: string): CurrencyFormatDto =
proc areTestNetworksEnabled*(self: Service): bool =
return self.settingsService.areTestNetworksEnabled()
proc hasPairedDevices*(self: Service): bool =
return hasPairedDevices()

View File

@ -19,6 +19,7 @@ const SIGNAL_WALLET_ACCOUNT_PREFERRED_SHARING_CHAINS_UPDATED* = "walletAccount/p
const SIGNAL_KEYPAIR_SYNCED* = "keypairSynced"
const SIGNAL_KEYPAIR_NAME_CHANGED* = "keypairNameChanged"
const SIGNAL_KEYPAIR_DELETED* = "keypairDeleted"
const SIGNAL_KEYPAIR_OPERABILITY_CHANGED* = "keypairOperabilityChanged"
const SIGNAL_NEW_KEYCARD_SET* = "newKeycardSet"
const SIGNAL_KEYCARD_REBUILD* = "keycardRebuild"

View File

@ -238,6 +238,11 @@ proc importMnemonic*(mnemonic, password: string):
let payload = %* [mnemonic, password]
return core.callPrivateRPC("accounts_importMnemonic", payload)
proc makeSeedPhraseKeypairFullyOperable*(mnemonic, password: string):
RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %* [mnemonic, password]
return core.callPrivateRPC("accounts_makeSeedPhraseKeypairFullyOperable", payload)
proc createAccountFromMnemonicAndDeriveAccountsForPaths*(mnemonic: string, paths: seq[string]): RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %* {
"mnemonicPhrase": mnemonic,
@ -258,6 +263,11 @@ proc importPrivateKey*(privateKey, password: string):
let payload = %* [privateKey, password]
return core.callPrivateRPC("accounts_importPrivateKey", payload)
proc makePrivateKeyKeypairFullyOperable*(privateKey, password: string):
RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %* [privateKey, password]
return core.callPrivateRPC("accounts_makePrivateKeyKeypairFullyOperable", payload)
proc createAccountFromPrivateKey*(privateKey: string): RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %* {"privateKey": privateKey}
try:

View File

@ -21,6 +21,8 @@ Rectangle {
signal goToAccountView(var account)
signal toggleIncludeWatchOnlyAccount()
signal runImportViaSeedPhraseFlow()
signal runImportViaPrivateKeyFlow()
signal runRenameKeypairFlow()
signal runRemoveKeypairFlow()
@ -80,6 +82,8 @@ Rectangle {
menuLoader.active = false
}
keyPair: root.keyPair
onRunImportViaSeedPhraseFlow: root.runImportViaSeedPhraseFlow()
onRunImportViaPrivateKeyFlow: root.runImportViaPrivateKeyFlow()
onRunRenameKeypairFlow: root.runRenameKeypairFlow()
onRunRemoveKeypairFlow: root.runRemoveKeypairFlow()
}

View File

@ -10,6 +10,8 @@ StatusMenu {
property var keyPair
signal runImportViaSeedPhraseFlow()
signal runImportViaPrivateKeyFlow()
signal runRenameKeypairFlow()
signal runRemoveKeypairFlow()
@ -56,15 +58,16 @@ StatusMenu {
StatusAction {
text: enabled? root.keyPair.pairType === Constants.keypair.type.privateKeyImport? qsTr("Import via entering private key") : qsTr("Import via entering seed phrase") : ""
enabled: !!root.keyPair &&
root.keyPair.operability === Constants.keypair.operability.nonOperable &&
(root.keyPair.pairType === Constants.keypair.type.seedImport ||
root.keyPair.pairType === Constants.keypair.type.privateKeyImport)
icon.name: enabled? root.keyPair.pairType === Constants.keypair.type.privateKeyImport? "objects" : "key_pair_seed_phrase" : ""
icon.color: Theme.palette.primaryColor1
onTriggered: {
if (root.keyPair.pairType === Constants.keypair.type.privateKeyImport)
console.warn("TODO: run import via private key flow")
root.runImportViaPrivateKeyFlow()
else
console.warn("TODO: run import via seed phrase flow")
root.runImportViaSeedPhraseFlow()
}
}

View File

@ -56,8 +56,7 @@ QtObject {
}
property WalletStore walletStore: WalletStore {
accountsModule: profileSectionModuleInst.walletAccountsModule
networksModule: profileSectionModuleInst.walletNetworksModule
walletModule: profileSectionModuleInst.walletModule
}
property KeycardStore keycardStore: KeycardStore {

View File

@ -5,8 +5,9 @@ import utils 1.0
QtObject {
id: root
property var accountsModule
property var networksModule
property var walletModule
property var accountsModule: root.walletModule.accountsModule
property var networksModule: root.walletModule.networksModule
property var accountSensitiveSettings: Global.appIsReady? localAccountSensitiveSettings : null
@ -58,9 +59,18 @@ QtObject {
}
function runAddAccountPopup() {
// TODO:
// - `runAddAccountPopup` should be part of `root.walletModule`
// - `AddAccountPopup {}` should be moved from `MainView` to `WalletView`
// - `Edit account` popup opened from the wallet settings should be the same as one opened from the wallet section
// - `walletSection` should not be used in the context of wallet settings
walletSection.runAddAccountPopup(false)
}
function runKeypairImportPopup(keyUid, importOption) {
root.walletModule.runKeypairImportPopup(keyUid, importOption)
}
function evaluateRpcEndPoint(url) {
return networksModule.fetchChainIdForUrl(url)
}

View File

@ -11,6 +11,7 @@ import utils 1.0
import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.popups.keypairimport 1.0
import shared.status 1.0
import "../controls"
@ -252,5 +253,31 @@ SettingsContentBase {
}
onLoaded: removeKeypairPopup.item.open()
}
Connections {
target: root.walletStore.walletModule
function onDisplayKeypairImportPopup() {
keypairImport.active = true
}
function onDestroyKeypairImportPopup() {
keypairImport.active = false
}
}
Loader {
id: keypairImport
active: false
asynchronous: true
sourceComponent: KeypairImportPopup {
store.keypairImportModule: root.walletStore.walletModule.keypairImportModule
}
onLoaded: {
keypairImport.item.open()
}
}
}
}

View File

@ -6,6 +6,7 @@ import shared.status 1.0
import shared.panels 1.0
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import shared.popups 1.0
@ -41,6 +42,10 @@ Column {
}
}
component Spacer: Item {
height: 8
}
Loader {
id: addAccount
@ -91,9 +96,42 @@ Column {
Separator {}
Item {
Spacer {
visible: root.walletStore.walletModule.hasPairedDevices
width: parent.width
}
Rectangle {
visible: root.walletStore.walletModule.hasPairedDevices
height: 102
width: parent.width
color: Theme.palette.transparent
radius: 8
border.width: 1
border.color: Theme.palette.baseColor5
Column {
anchors.fill: parent
padding: 16
spacing: 8
StatusBaseText {
text: qsTr("Import keypairs from this device to your other synced devices")
font.pixelSize: 15
}
StatusButton {
text: qsTr("Show encrypted QR of keypairs on device")
icon.name: "qr"
onClicked: {
console.warn("TODO: run generate qr code flow...")
}
}
}
}
Spacer {
width: parent.width
height: 8
}
Column {
@ -112,6 +150,12 @@ Column {
onToggleIncludeWatchOnlyAccount: walletStore.toggleIncludeWatchOnlyAccount()
onRunRenameKeypairFlow: root.runRenameKeypairFlow(model)
onRunRemoveKeypairFlow: root.runRemoveKeypairFlow(model)
onRunImportViaSeedPhraseFlow: {
root.walletStore.runKeypairImportPopup(model.keyPair.keyUid, Constants.keypairImportPopup.importOption.seedPhrase)
}
onRunImportViaPrivateKeyFlow: {
root.walletStore.runKeypairImportPopup(model.keyPair.keyUid, Constants.keypairImportPopup.importOption.privateKey)
}
}
}
}

View File

@ -71,7 +71,7 @@ ColumnLayout {
function pasteWords () {
const clipboardText = globalUtils.getFromClipboard()
// Split words separated by commas and or blank spaces (spaces, enters, tabs)
const words = clipboardText.split(/[, \s]+/)
const words = clipboardText.trim().split(/[, \s]+/)
let index = d.tabs.indexOf(words.length)
if (index === -1) {

View File

@ -9,6 +9,7 @@ import utils 1.0
import "./stores"
import "./states"
import "../common"
StatusModal {
id: root
@ -255,7 +256,7 @@ StatusModal {
}
onClicked: {
root.store.submitAddAccount(null)
root.store.submitPopup(null)
}
}
]

View File

@ -10,6 +10,7 @@ import StatusQ.Components 0.1
import utils 1.0
import "../stores"
import "../../common"
GridLayout {
id: root
@ -77,7 +78,7 @@ GridLayout {
}
onEditingFinished: {
root.store.submitAddAccount(null)
root.store.submitPopup(null)
}
input.rightComponent: StatusIcon {

View File

@ -9,6 +9,7 @@ import StatusQ.Controls.Validators 0.1
import utils 1.0
import "../stores"
import "../../common"
Column {
id: root
@ -53,7 +54,7 @@ Column {
}
onKeyPressed: {
root.store.submitAddAccount(event)
root.store.submitPopup(event)
}
}

View File

@ -41,7 +41,7 @@ Item {
}
onKeyPressed: {
root.store.submitAddAccount(event)
root.store.submitPopup(event)
}
}

View File

@ -151,7 +151,7 @@ Item {
}
onKeyPressed: {
root.store.submitAddAccount(event)
root.store.submitPopup(event)
}
}
}

View File

@ -12,6 +12,7 @@ import utils 1.0
import "../stores"
import "../panels"
import "../../common"
Item {
id: root
@ -140,7 +141,7 @@ Item {
}
onKeyPressed: {
root.store.submitAddAccount(event)
root.store.submitPopup(event)
}
onValidChanged: {

View File

@ -1,9 +1,12 @@
import QtQuick 2.13
import utils 1.0
QtObject {
import "../../common"
BasePopupStore {
id: root
isAddAccountPopup: true
required property var addAccountModule
required property var emojiPopup
@ -19,13 +22,11 @@ QtObject {
property var derivedAddressModel: root.addAccountModule.derivedAddressModel
property var selectedDerivedAddress: root.addAccountModule.selectedDerivedAddress
property var watchOnlyAccAddress: root.addAccountModule.watchOnlyAccAddress
property var privateKeyAccAddress: root.addAccountModule.privateKeyAccAddress
privateKeyAccAddress: root.addAccountModule.privateKeyAccAddress
property bool editMode: root.addAccountModule.editMode
property bool disablePopup: root.addAccountModule.disablePopup
property bool accountNameIsValid: false
property bool enteredSeedPhraseIsValid: false
property bool enteredPrivateKeyIsValid: false
property bool addingNewMasterKeyConfirmed: false
property bool seedPhraseRevealed: false
property bool seedPhraseWord1Valid: false
@ -82,7 +83,7 @@ QtObject {
return root.addAccountModule.getStoredSelectedColorId()
}
function submitAddAccount(event) {
submitPopup: function(event) {
if (!root.primaryPopupButtonEnabled) {
return
}
@ -116,11 +117,11 @@ QtObject {
root.addAccountModule.changeWatchOnlyAccountAddress("")
}
readonly property var changePrivateKeyPostponed: Backpressure.debounce(root, 400, function (privateKey) {
changePrivateKeyPostponed: Backpressure.debounce(root, 400, function (privateKey) {
root.addAccountModule.changePrivateKey(privateKey)
})
function cleanPrivateKey() {
cleanPrivateKey: function() {
root.enteredPrivateKeyIsValid = false
root.addAccountModule.newKeyPairName = ""
root.addAccountModule.changePrivateKey("")
@ -152,11 +153,11 @@ QtObject {
root.addAccountModule.startScanningForActivity()
}
function validSeedPhrase(seedPhrase) {
validSeedPhrase: function(seedPhrase) {
return root.addAccountModule.validSeedPhrase(seedPhrase)
}
function changeSeedPhrase(seedPhrase) {
changeSeedPhrase: function(seedPhrase) {
root.addAccountModule.changeSeedPhrase(seedPhrase)
}
@ -186,10 +187,6 @@ QtObject {
}
}
function getFromClipboard() {
return globalUtils.getFromClipboard()
}
readonly property bool primaryPopupButtonEnabled: {
if (!root.addAccountModule || !root.currentState || root.disablePopup) {
return false

View File

@ -12,6 +12,7 @@ Row {
property var addressDetailsItem
property bool defaultMessageCondition: true
property string defaultMessage: ""
property bool alreadyCreatedAccountIsAnError: true
StatusIcon {
id: icon
@ -36,7 +37,7 @@ Row {
if (!root.addressDetailsItem || !root.addressDetailsItem.loaded) {
return qsTr("Scanning for activity...")
}
if (root.addressDetailsItem.alreadyCreated) {
if (root.alreadyCreatedAccountIsAnError && root.addressDetailsItem.alreadyCreated) {
return qsTr("Already added")
}
if (root.addressDetailsItem.hasActivity) {
@ -48,7 +49,7 @@ Row {
if (root.defaultMessageCondition || !root.addressDetailsItem || !root.addressDetailsItem.loaded) {
return Theme.palette.baseColor1
}
if (root.addressDetailsItem.alreadyCreated) {
if (root.alreadyCreatedAccountIsAnError && root.addressDetailsItem.alreadyCreated) {
return Theme.palette.dangerColor1
}
if (root.addressDetailsItem.hasActivity) {

View File

@ -17,6 +17,7 @@ Column {
property bool addressResolved: true
property bool displayDetails: true
property bool displayCopyButton: true
property bool alreadyCreatedAccountIsAnError: true
spacing: Style.current.halfPadding
@ -44,5 +45,6 @@ Column {
addressDetailsItem: root.addressDetailsItem
defaultMessage: ""
defaultMessageCondition: !root.addressResolved
alreadyCreatedAccountIsAnError: root.alreadyCreatedAccountIsAnError
}
}

View File

@ -0,0 +1,26 @@
import QtQuick 2.13
import utils 1.0
QtObject {
id: root
// store properties
required property bool isAddAccountPopup
property bool enteredPrivateKeyIsValid: false
property bool enteredPrivateKeyMatchTheKeypair: true
property bool enteredSeedPhraseIsValid: false
// backend properties
required property var privateKeyAccAddress
// functions
property var changePrivateKeyPostponed: function(){}
property var cleanPrivateKey: function(){}
property var submitPopup: function(){}
property var changeSeedPhrase: function(){}
property var validSeedPhrase: function(){}
function getFromClipboard() {
return globalUtils.getFromClipboard()
}
}

View File

@ -11,13 +11,10 @@ import StatusQ.Popups 0.1
import utils 1.0
import "../stores"
import "../panels"
Item {
id: root
property AddAccountStore store
property BasePopupStore store
QtObject {
id: d
@ -38,8 +35,10 @@ Item {
spacing: Style.current.halfPadding
StatusBaseText {
text: qsTr("Private key")
width: parent.width
text: root.store.isAddAccountPopup? qsTr("Private key") : qsTr("Private key for %1 keypair").arg(root.store.selectedKeypair.name)
font.pixelSize: Constants.addAccountPopup.labelFontSize1
elide: Text.ElideRight
}
GridLayout {
@ -68,7 +67,7 @@ Item {
}
onPressed: {
root.store.submitAddAccount(event)
root.store.submitPopup(event)
}
StatusButton {
@ -97,11 +96,17 @@ Item {
StatusBaseText {
Layout.alignment: Qt.AlignRight
visible: privKeyInput.text !== "" && !root.store.enteredPrivateKeyIsValid
visible: privKeyInput.text !== "" && !(root.store.enteredPrivateKeyIsValid && root.store.enteredPrivateKeyMatchTheKeypair)
wrapMode: Text.WordWrap
font.pixelSize: 12
color: Theme.palette.dangerColor1
text: qsTr("Private key invalid")
text: {
if (!root.store.enteredPrivateKeyIsValid)
return qsTr("Private key invalid")
if (!root.store.enteredPrivateKeyMatchTheKeypair)
return qsTr("This is not the correct private key")
return ""
}
}
}
}
@ -136,17 +141,18 @@ Item {
addressDetailsItem: root.store.privateKeyAccAddress
addressResolved: d.addressResolved
displayCopyButton: false
alreadyCreatedAccountIsAnError: root.store.isAddAccountPopup
}
StatusModalDivider {
width: parent.width
visible: d.addressResolved
visible: root.store.isAddAccountPopup && d.addressResolved
}
Column {
width: parent.width
spacing: Style.current.halfPadding
visible: d.addressResolved
visible: root.store.isAddAccountPopup && d.addressResolved
StatusInput {
objectName: "AddAccountPopup-PrivateKeyName"
@ -154,9 +160,12 @@ Item {
label: qsTr("Key name")
charLimit: Constants.addAccountPopup.keyPairNameMaxLength
placeholderText: qsTr("Enter a name")
text: root.store.addAccountModule.newKeyPairName
text: root.store.isAddAccountPopup? root.store.addAccountModule.newKeyPairName : ""
onTextChanged: {
if (!root.store.isAddAccountPopup) {
return
}
if (text.trim() == "") {
root.store.addAccountModule.newKeyPairName = ""
return
@ -165,7 +174,7 @@ Item {
}
onKeyPressed: {
root.store.submitAddAccount(event)
root.store.submitPopup(event)
}
}

View File

@ -12,13 +12,10 @@ import StatusQ.Popups 0.1
import utils 1.0
import shared.panels 1.0 as SharedPanels
import "../stores"
import "../panels"
Item {
id: root
property AddAccountStore store
property BasePopupStore store
Column {
anchors.top: parent.top
@ -29,8 +26,10 @@ Item {
StatusBaseText {
text: qsTr("Enter seed phrase")
width: parent.width
text: root.store.isAddAccountPopup? qsTr("Enter seed phrase") : qsTr("Enter seed phrase for %1 keypair").arg(root.store.selectedKeypair.name)
font.pixelSize: Constants.addAccountPopup.labelFontSize1
elide: Text.ElideRight
}
SharedPanels.EnterSeedPhrase {
@ -47,24 +46,28 @@ Item {
}
root.store.enteredSeedPhraseIsValid = valid
if (!enterSeedPhrase.isSeedPhraseValid(seedPhrase)) {
enterSeedPhrase.setWrongSeedPhraseMessage(qsTr("The entered seed phrase is already added"))
let err = qsTr("The entered seed phrase is already added")
if (!root.store.isAddAccountPopup) {
err = qsTr("This is not the correct seed phrase for %1 key").arg(root.store.selectedKeypair.name)
}
enterSeedPhrase.setWrongSeedPhraseMessage(err)
}
}
onSubmitSeedPhrase: {
root.store.submitAddAccount()
root.store.submitPopup()
}
}
StatusModalDivider {
width: parent.width
visible: root.store.enteredSeedPhraseIsValid
visible: root.store.isAddAccountPopup && root.store.enteredSeedPhraseIsValid
}
Column {
width: parent.width
spacing: Style.current.halfPadding
visible: root.store.enteredSeedPhraseIsValid
visible: root.store.isAddAccountPopup && root.store.enteredSeedPhraseIsValid
StatusInput {
objectName: "AddAccountPopup-ImportedSeedPhraseKeyName"
@ -72,9 +75,12 @@ Item {
label: qsTr("Key name")
charLimit: Constants.addAccountPopup.keyPairNameMaxLength
placeholderText: qsTr("Enter a name")
text: root.store.addAccountModule.newKeyPairName
text: root.store.isAddAccountPopup? root.store.addAccountModule.newKeyPairName : ""
onTextChanged: {
if (!root.store.isAddAccountPopup) {
return
}
if (text.trim() == "") {
root.store.addAccountModule.newKeyPairName = ""
return
@ -83,7 +89,7 @@ Item {
}
onKeyPressed: {
root.store.submitAddAccount(event)
root.store.submitPopup(event)
}
}

View File

@ -0,0 +1,127 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Popups 0.1
import StatusQ.Controls 0.1
import utils 1.0
import "./stores"
import "../common"
StatusModal {
id: root
property KeypairImportStore store: KeypairImportStore { }
width: Constants.keypairImportPopup.popupWidth
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
headerSettings.title: {
return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name)
}
onClosed: {
root.store.currentState.doCancelAction()
}
StatusScrollView {
id: scrollView
anchors.fill: parent
padding: 0
contentWidth: availableWidth
Item {
id: content
implicitWidth: loader.implicitWidth
implicitHeight: loader.implicitHeight
width: scrollView.availableWidth
Loader {
id: loader
width: parent.width
sourceComponent: {
switch (root.store.currentState.stateType) {
case Constants.keypairImportPopup.state.importPrivateKey:
return keypairImportPrivateKeyComponent
case Constants.keypairImportPopup.state.importSeedPhrase:
return keypairImportSeedPhraseComponent
}
return undefined
}
onLoaded: {
content.height = Qt.binding(function(){return item.height})
}
}
Component {
id: keypairImportPrivateKeyComponent
EnterPrivateKey {
height: Constants.keypairImportPopup.contentHeight
store: root.store
}
}
Component {
id: keypairImportSeedPhraseComponent
EnterSeedPhrase {
height: Constants.keypairImportPopup.contentHeight
store: root.store
}
}
}
}
leftButtons: [
StatusBackButton {
id: backButton
visible: root.store.currentState.displayBackButton
height: Constants.keypairImportPopup.footerButtonsHeight
width: height
onClicked: {
root.store.currentState.doBackAction()
}
}
]
rightButtons: [
StatusButton {
id: primaryButton
height: Constants.keypairImportPopup.footerButtonsHeight
text: {
switch (root.store.currentState.stateType) {
case Constants.keypairImportPopup.state.importPrivateKey:
case Constants.keypairImportPopup.state.importSeedPhrase:
return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name)
}
return ""
}
visible: text !== ""
enabled: root.store.primaryPopupButtonEnabled
icon.name: {
if (root.store.userProfileUsingBiometricLogin) {
return "touch-id"
}
if (root.store.userProfileIsKeycardUser) {
return "keycard"
}
return "password"
}
onClicked: {
root.store.submitPopup(null)
}
}
]
}

View File

@ -0,0 +1 @@
KeypairImportPopup 1.0 KeypairImportPopup.qml

View File

@ -0,0 +1,68 @@
import QtQuick 2.13
import utils 1.0
import "../../common"
BasePopupStore {
id: root
isAddAccountPopup: false
required property var keypairImportModule
property bool userProfileIsKeycardUser: userProfile.isKeycardUser
property bool userProfileUsingBiometricLogin: userProfile.usingBiometricLogin
// Module Properties
property var currentState: root.keypairImportModule.currentState
property var selectedKeypair: root.keypairImportModule.selectedKeypair
enteredPrivateKeyMatchTheKeypair: root.keypairImportModule.enteredPrivateKeyMatchTheKeypair
privateKeyAccAddress: root.keypairImportModule.privateKeyAccAddress
submitPopup: function(event) {
if (!root.primaryPopupButtonEnabled) {
return
}
if(!event) {
root.currentState.doPrimaryAction()
}
else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
event.accepted = true
root.currentState.doPrimaryAction()
}
}
changePrivateKeyPostponed: Backpressure.debounce(root, 400, function (privateKey) {
root.keypairImportModule.changePrivateKey(privateKey)
})
cleanPrivateKey: function() {
root.enteredPrivateKeyIsValid = false
root.keypairImportModule.changePrivateKey("")
}
function validSeedPhrase(seedPhrase) {
return root.keypairImportModule.validSeedPhrase(seedPhrase)
}
function changeSeedPhrase(seedPhrase) {
root.keypairImportModule.changeSeedPhrase(seedPhrase)
}
readonly property bool primaryPopupButtonEnabled: {
if (root.currentState.stateType === Constants.keypairImportPopup.state.importPrivateKey) {
return root.enteredPrivateKeyIsValid &&
root.enteredPrivateKeyMatchTheKeypair &&
!!root.privateKeyAccAddress &&
root.privateKeyAccAddress.loaded &&
root.privateKeyAccAddress.alreadyCreated &&
root.privateKeyAccAddress.address !== ""
}
if (root.currentState.stateType === Constants.keypairImportPopup.state.importSeedPhrase) {
return root.enteredSeedPhraseIsValid
}
return true
}
}

View File

@ -739,6 +739,23 @@ QtObject {
}
}
readonly property QtObject keypairImportPopup: QtObject {
readonly property int popupWidth: 480
readonly property int contentHeight: 626
readonly property int footerButtonsHeight: 44
readonly property QtObject importOption: QtObject {
readonly property int seedPhrase: 1
readonly property int privateKey: 2
}
readonly property QtObject state: QtObject {
readonly property string noState: "NoState"
readonly property string importSeedPhrase: "ImportSeedPhrase"
readonly property string importPrivateKey: "ImportPrivateKey"
}
}
readonly property QtObject localPairingAction: QtObject {
readonly property int actionUnknown: 0
readonly property int actionConnect: 1