feat(@desktop/wallet): new "Add Account" modal added

Closes: #9733
This commit is contained in:
Sale Djenic 2023-03-22 16:48:44 +01:00 committed by saledjenic
parent 204f47229c
commit 223e4f1bc2
54 changed files with 4931 additions and 359 deletions

View File

@ -0,0 +1,229 @@
import times, os, chronicles
import uuids
import io_interface
import ../../../../../app_service/service/accounts/service as accounts_service
import ../../../../../app_service/service/wallet_account/service as wallet_account_service
import ../../../../../app_service/service/keycard/service as keycard_service
import ../../../shared_modules/keycard_popup/io_interface as keycard_shared_module
import ../../../../core/eventemitter
logScope:
topics = "wallet-add-account-controller"
const UNIQUE_WALLET_SECTION_ADD_ACCOUNTS_MODULE_IDENTIFIER* = "WalletSection-AddAccountsModule"
type
Controller* = ref object of RootObj
delegate: io_interface.AccessInterface
events: EventEmitter
accountsService: accounts_service.Service
walletAccountService: wallet_account_service.Service
keycardService: keycard_service.Service
connectionIds: seq[UUID]
connectionKeycardResponse: UUID
tmpAuthenticatedKeyUid: string
tmpPin: string
tmpPassword: string
tmpSeedPhrase: string
tmpPaths: seq[string]
tmpGeneratedAccount: GeneratedAccountDto
uniqueFetchingDetailsId: string
## Forward declaration
proc disconnectKeycardReponseSignal(self: Controller)
proc newController*(delegate: io_interface.AccessInterface,
events: EventEmitter,
accountsService: accounts_service.Service,
walletAccountService: wallet_account_service.Service,
keycardService: keycard_service.Service):
Controller =
result = Controller()
result.delegate = delegate
result.events = events
result.accountsService = accountsService
result.walletAccountService = walletAccountService
result.keycardService = keycardService
proc disconnectAll*(self: Controller) =
self.disconnectKeycardReponseSignal()
for id in self.connectionIds:
self.events.disconnect(id)
proc delete*(self: Controller) =
self.disconnectAll()
proc init*(self: Controller) =
var handlerId = self.events.onWithUUID(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args):
let args = SharedKeycarModuleArgs(e)
if args.uniqueIdentifier != UNIQUE_WALLET_SECTION_ADD_ACCOUNTS_MODULE_IDENTIFIER:
return
self.delegate.onUserAuthenticated(args.pin, args.password, args.keyUid)
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESSES_FETCHED) do(e:Args):
var args = DerivedAddressesArgs(e)
self.delegate.onDerivedAddressesFetched(args.derivedAddresses, args.error)
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESSES_FROM_MNEMONIC_FETCHED) do(e:Args):
var args = DerivedAddressesArgs(e)
self.delegate.onDerivedAddressesFromMnemonicFetched(args.derivedAddresses, args.error)
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_DERIVED_ADDRESSES_FROM_NOT_IMPORTED_MNEMONIC_FETCHED) do(e:Args):
var args = DerivedAddressesFromNotImportedMnemonicArgs(e)
self.delegate.onAddressesFromNotImportedMnemonicFetched(args.derivations, args.error)
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(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)
self.connectionIds.add(handlerId)
proc setAuthenticatedKeyUid*(self: Controller, value: string) =
self.tmpAuthenticatedKeyUid = value
proc getAuthenticatedKeyUid*(self: Controller): string =
return self.tmpAuthenticatedKeyUid
proc setPin*(self: Controller, value: string) =
self.tmpPin = value
proc getPin*(self: Controller): string =
return self.tmpPin
proc setPassword*(self: Controller, value: string) =
self.tmpPassword = value
proc getPassword*(self: Controller): string =
return self.tmpPassword
proc setSeedPhrase*(self: Controller, value: string) =
self.tmpSeedPhrase = value
proc getSeedPhrase*(self: Controller): string =
return self.tmpSeedPhrase
proc closeAddAccountPopup*(self: Controller) =
self.delegate.closeAddAccountPopup()
proc getWalletAccounts*(self: Controller): seq[WalletAccountDto] =
return self.walletAccountService.fetchAccounts()
proc getAllMigratedKeyPairs*(self: Controller): seq[KeyPairDto] =
return self.walletAccountService.getAllMigratedKeyPairs()
proc addAccount*(self: Controller) =
self.delegate.addAccount()
proc authenticateOrigin*(self: Controller, keyUid = "") =
let data = SharedKeycarModuleAuthenticationArgs(uniqueIdentifier: UNIQUE_WALLET_SECTION_ADD_ACCOUNTS_MODULE_IDENTIFIER,
keyUid: keyUid)
self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data)
method fetchDerivedAddresses*(self: Controller, derivedFrom: string, paths: seq[string])=
var hashPassword = true
if self.getPin().len > 0:
hashPassword = false
self.walletAccountService.fetchDerivedAddresses(self.getPassword(), derivedFrom, paths, hashPassword)
proc getRandomMnemonic*(self: Controller): string =
return self.walletAccountService.getRandomMnemonic()
proc fetchDetailsForAddresses*(self: Controller, addresses: seq[string]) =
self.uniqueFetchingDetailsId = $now().toTime().toUnix()
self.walletAccountService.fetchDetailsForAddresses(self.uniqueFetchingDetailsId, addresses)
proc addWalletAccount*(self: Controller, createKeystoreFile, doPasswordHashing: bool, name, keyPairName, address, path: string,
lastUsedDerivationIndex: int, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji: string): bool =
var password: string
if createKeystoreFile:
password = self.getPassword()
if password.len == 0:
info "cannot create keystore file if provided password is empty", name=name, address=address
return false
let err = self.walletAccountService.addWalletAccount(password, doPasswordHashing, name, keyPairName, address, path,
lastUsedDerivationIndex, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji)
if err.len > 0:
info "adding wallet account failed", name=name, address=address
return false
return true
proc addNewPrivateKeyAccount*(self: Controller, privateKey: string, doPasswordHashing: bool, name, keyPairName,
address, path: string, lastUsedDerivationIndex: int, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji: string): bool =
let password = self.getPassword()
if password.len == 0:
info "cannot create keystore file if provided password is empty", name=name, address=address
return false
let err = self.walletAccountService.addNewPrivateKeyAccount(privateKey, password, doPasswordHashing, name, keyPairName, address, path,
lastUsedDerivationIndex, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji)
if err.len > 0:
info "adding new wallet account from private key failed", name=name, address=address
return false
return true
proc addNewSeedPhraseAccount*(self: Controller, seedPhrase: string, doPasswordHashing: bool, name, keyPairName,
address, path: string, lastUsedDerivationIndex: int, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji: string): bool =
let password = self.getPassword()
if password.len == 0:
info "cannot create keystore file if provided password is empty", name=name, address=address
return false
let err = self.walletAccountService.addNewSeedPhraseAccount(seedPhrase, password, doPasswordHashing, name, keyPairName, address, path,
lastUsedDerivationIndex, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji)
if err.len > 0:
info "adding new wallet account from seed phrase failed", name=name, address=address
return false
return true
proc getKeyUidForSeedPhrase*(self: Controller, seedPhrase: string): string =
let acc = self.accountsService.createAccountFromMnemonic(seedPhrase)
return acc.keyUid
proc createAccountFromSeedPhrase*(self: Controller, seedPhrase: string): GeneratedAccountDto =
self.setSeedPhrase(seedPhrase)
self.tmpGeneratedAccount = self.accountsService.createAccountFromMnemonic(seedPhrase)
return self.tmpGeneratedAccount
proc fetchAddressesFromNotImportedSeedPhrase*(self: Controller, seedPhrase: string, paths: seq[string] = @[]) =
self.accountsService.fetchAddressesFromNotImportedMnemonic(seedPhrase, paths)
proc createAccountFromPrivateKey*(self: Controller, privateKey: string): GeneratedAccountDto =
self.tmpGeneratedAccount = self.accountsService.createAccountFromPrivateKey(privateKey)
return self.tmpGeneratedAccount
proc getGeneratedAccount*(self: Controller): GeneratedAccountDto =
return self.tmpGeneratedAccount
proc buildNewPrivateKeyKeypairAndAddItToOrigin*(self: Controller) =
self.delegate.buildNewPrivateKeyKeypairAndAddItToOrigin()
proc buildNewSeedPhraseKeypairAndAddItToOrigin*(self: Controller) =
self.delegate.buildNewSeedPhraseKeypairAndAddItToOrigin()
proc disconnectKeycardReponseSignal(self: Controller) =
self.events.disconnect(self.connectionKeycardResponse)
proc connectKeycardReponseSignal(self: Controller) =
self.connectionKeycardResponse = self.events.onWithUUID(SIGNAL_KEYCARD_RESPONSE) do(e: Args):
let args = KeycardArgs(e)
self.disconnectKeycardReponseSignal()
self.delegate.onDerivedAddressesFromKeycardFetched(args.flowType, args.flowEvent, self.tmpPaths)
proc cancelCurrentFlow*(self: Controller) =
self.keycardService.cancelCurrentFlow()
# in most cases we're running another flow after canceling the current one,
# this way we're giving to the keycard some time to cancel the current flow
sleep(200)
proc fetchAddressesFromKeycard*(self: Controller, bip44Paths: seq[string]) =
self.cancelCurrentFlow()
self.connectKeycardReponseSignal()
self.tmpPaths = bip44Paths
self.keycardService.startExportPublicFlow(bip44Paths, exportMasterAddr=true, exportPrivateAddr=false, pin=self.getPin())

View File

@ -0,0 +1,114 @@
import NimQml, strformat
QtObject:
type DerivedAddressItem* = ref object of QObject
order: int
address: string
path: string
hasActivity: bool
alreadyCreated: bool
loaded: bool
proc delete*(self: DerivedAddressItem) =
self.QObject.delete
proc newDerivedAddressItem*(
order: int = 0,
address: string = "",
path: string = "",
alreadyCreated: bool = false,
hasActivity: bool = false,
loaded: bool = false
): DerivedAddressItem =
new(result, delete)
result.QObject.setup
result.order = order
result.address = address
result.path = path
result.alreadyCreated = alreadyCreated
result.hasActivity = hasActivity
result.loaded = loaded
proc `$`*(self: DerivedAddressItem): string =
result = fmt"""DerivedAddressItem(
order: {self.order},
address: {self.address},
path: {self.path},
alreadyCreated: {self.alreadyCreated},
hasActivity: {self.hasActivity},
loaded: {self.loaded}
]"""
proc orderChanged*(self: DerivedAddressItem) {.signal.}
proc getOrder*(self: DerivedAddressItem): int {.slot.} =
return self.order
proc setOrder*(self: DerivedAddressItem, value: int) {.slot.} =
self.order = value
self.orderChanged()
QtProperty[int] order:
read = getOrder
write = setOrder
notify = orderChanged
proc addressChanged*(self: DerivedAddressItem) {.signal.}
proc getAddress*(self: DerivedAddressItem): string {.slot.} =
return self.address
proc setAddress*(self: DerivedAddressItem, value: string) {.slot.} =
self.address = value
self.addressChanged()
QtProperty[string] address:
read = getAddress
write = setAddress
notify = addressChanged
proc pathChanged*(self: DerivedAddressItem) {.signal.}
proc getPath*(self: DerivedAddressItem): string {.slot.} =
return self.path
proc setPath*(self: DerivedAddressItem, value: string) {.slot.} =
self.path = value
self.pathChanged()
QtProperty[string] path:
read = getPath
write = setPath
notify = pathChanged
proc alreadyCreatedChanged*(self: DerivedAddressItem) {.signal.}
proc getAlreadyCreated*(self: DerivedAddressItem): bool {.slot.} =
return self.alreadyCreated
proc setAlreadyCreated*(self: DerivedAddressItem, value: bool) {.slot.} =
self.alreadyCreated = value
self.alreadyCreatedChanged()
QtProperty[bool] alreadyCreated:
read = getAlreadyCreated
write = setAlreadyCreated
notify = alreadyCreatedChanged
proc hasActivityChanged*(self: DerivedAddressItem) {.signal.}
proc getHasActivity*(self: DerivedAddressItem): bool {.slot.} =
return self.hasActivity
proc setHasActivity*(self: DerivedAddressItem, value: bool) {.slot.} =
self.hasActivity = value
self.hasActivityChanged()
QtProperty[bool] hasActivity:
read = getHasActivity
write = setHasActivity
notify = hasActivityChanged
proc loadedChanged*(self: DerivedAddressItem) {.signal.}
proc getLoaded*(self: DerivedAddressItem): bool {.slot.} =
return self.loaded
proc setLoaded*(self: DerivedAddressItem, value: bool) {.slot.} =
self.loaded = value
self.loadedChanged()
QtProperty[bool] loaded:
read = getLoaded
write = setLoaded
notify = loadedChanged
proc setItem*(self: DerivedAddressItem, item: DerivedAddressItem) =
self.setOrder(item.getOrder())
self.setAddress(item.getAddress())
self.setPath(item.getPath())
self.setAlreadyCreated(item.getAlreadyCreated())
self.setHasActivity(item.getHasActivity())
self.setLoaded(item.getLoaded())

View File

@ -0,0 +1,121 @@
import NimQml, Tables, strutils, sequtils, sugar, strformat
import ./derived_address_item
export derived_address_item
type
ModelRole {.pure.} = enum
AddressDetails = UserRole + 1
QtObject:
type
DerivedAddressModel* = ref object of QAbstractListModel
items: seq[DerivedAddressItem]
proc delete(self: DerivedAddressModel) =
self.items = @[]
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.items.len:
result &= fmt"""[{i}]:({$self.items[i]})"""
proc countChanged(self: DerivedAddressModel) {.signal.}
proc getCount(self: DerivedAddressModel): int {.slot.} =
self.items.len
QtProperty[int] count:
read = getCount
notify = countChanged
proc loadedCountChanged(self: DerivedAddressModel) {.signal.}
proc getLoadedCount(self: DerivedAddressModel): int {.slot.} =
return self.items.filter(x => x.getLoaded()).len
QtProperty[int] loadedCount:
read = getLoadedCount
notify = loadedCountChanged
method rowCount(self: DerivedAddressModel, index: QModelIndex = nil): int =
return self.items.len
method roleNames(self: DerivedAddressModel): Table[int, string] =
{
ModelRole.AddressDetails.int: "addressDetails"
}.toTable
method data(self: DerivedAddressModel, index: QModelIndex, role: int): QVariant =
if (not index.isValid):
return
if (index.row < 0 or index.row >= self.items.len):
return
let item = self.items[index.row]
let enumRole = role.ModelRole
case enumRole:
of ModelRole.AddressDetails:
result = newQVariant(item)
proc reset*(self: DerivedAddressModel) =
self.beginResetModel()
self.items = @[]
self.endResetModel()
self.countChanged()
self.loadedCountChanged()
proc setItems*(self: DerivedAddressModel, items: seq[DerivedAddressItem]) =
self.beginResetModel()
self.items = items
self.endResetModel()
self.countChanged()
self.loadedCountChanged()
proc getItemByAddress*(self: DerivedAddressModel, address: string): DerivedAddressItem =
for it in self.items:
if it.getAddress() == address:
return it
return nil
proc updateDetailsForAddressAndBubbleItToTop*(self: DerivedAddressModel, address: string, hasActivity: bool) =
var item: DerivedAddressItem
for i in 0 ..< self.items.len:
if cmpIgnoreCase(self.items[i].getAddress(), address) == 0:
item = self.items[i]
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
self.beginRemoveRows(parentModelIndex, i, i)
self.items.delete(i)
self.endRemoveRows()
break
if item.isNil:
return
var indexToInsertTo = 0
for i in 0 ..< self.items.len:
if not self.items[i].getLoaded():
break
indexToInsertTo.inc
let parentModelIndex = newQModelIndex()
defer: parentModelIndex.delete
self.beginInsertRows(parentModelIndex, indexToInsertTo, indexToInsertTo)
self.items.insert(
newDerivedAddressItem(item.getOrder(), item.getAddress(), item.getPath(), item.getAlreadyCreated(), hasActivity, loaded = true),
indexToInsertTo
)
self.endInsertRows()
self.countChanged() # we need this to trigger bindings on the qml side
self.loadedCountChanged()
proc getAllAddresses*(self: DerivedAddressModel): seq[string] =
return self.items.map(x => x.getAddress())

View File

@ -0,0 +1,21 @@
type
ConfirmAddingNewMasterKeyState* = ref object of State
proc newConfirmAddingNewMasterKeyState*(backState: State): ConfirmAddingNewMasterKeyState =
result = ConfirmAddingNewMasterKeyState()
result.setup(StateType.ConfirmAddingNewMasterKey, backState)
proc delete*(self: ConfirmAddingNewMasterKeyState) =
self.State.delete
method executePrePrimaryStateCommand*(self: ConfirmAddingNewMasterKeyState, controller: Controller) =
let seedPhrase = controller.getRandomMnemonic()
let genAccDto = controller.createAccountFromSeedPhrase(seedPhrase)
if genAccDto.address.len == 0:
# should never be here
error "unable to create an account from the provided seed phrase"
controller.closeAddAccountPopup()
return
method getNextPrimaryState*(self: ConfirmAddingNewMasterKeyState, controller: Controller): State =
return createState(StateType.DisplaySeedPhrase, self)

View File

@ -0,0 +1,12 @@
type
ConfirmSeedPhraseBackupState* = ref object of State
proc newConfirmSeedPhraseBackupState*(backState: State): ConfirmSeedPhraseBackupState =
result = ConfirmSeedPhraseBackupState()
result.setup(StateType.ConfirmSeedPhraseBackup, backState)
proc delete*(self: ConfirmSeedPhraseBackupState) =
self.State.delete
method getNextPrimaryState*(self: ConfirmSeedPhraseBackupState, controller: Controller): State =
return createState(StateType.EnterKeypairName, self)

View File

@ -0,0 +1,12 @@
type
DisplaySeedPhraseState* = ref object of State
proc newDisplaySeedPhraseState*(backState: State): DisplaySeedPhraseState =
result = DisplaySeedPhraseState()
result.setup(StateType.DisplaySeedPhrase, backState)
proc delete*(self: DisplaySeedPhraseState) =
self.State.delete
method getNextPrimaryState*(self: DisplaySeedPhraseState, controller: Controller): State =
return createState(StateType.EnterSeedPhraseWord1, self)

View File

@ -0,0 +1,15 @@
type
EnterKeypairNameState* = ref object of State
proc newEnterKeypairNameState*(backState: State): EnterKeypairNameState =
result = EnterKeypairNameState()
result.setup(StateType.EnterKeypairName, backState)
proc delete*(self: EnterKeypairNameState) =
self.State.delete
method executePrePrimaryStateCommand*(self: EnterKeypairNameState, controller: Controller) =
controller.buildNewSeedPhraseKeypairAndAddItToOrigin()
method getNextPrimaryState*(self: EnterKeypairNameState, controller: Controller): State =
return createState(StateType.Main, nil)

View File

@ -0,0 +1,15 @@
type
EnterPrivateKeyState* = ref object of State
proc newEnterPrivateKeyState*(backState: State): EnterPrivateKeyState =
result = EnterPrivateKeyState()
result.setup(StateType.EnterPrivateKey, backState)
proc delete*(self: EnterPrivateKeyState) =
self.State.delete
method executePrePrimaryStateCommand*(self: EnterPrivateKeyState, controller: Controller) =
controller.buildNewPrivateKeyKeypairAndAddItToOrigin()
method getNextPrimaryState*(self: EnterPrivateKeyState, controller: Controller): State =
return createState(StateType.Main, nil)

View File

@ -0,0 +1,15 @@
type
EnterSeedPhraseState* = ref object of State
proc newEnterSeedPhraseState*(backState: State): EnterSeedPhraseState =
result = EnterSeedPhraseState()
result.setup(StateType.EnterSeedPhrase, backState)
proc delete*(self: EnterSeedPhraseState) =
self.State.delete
method executePrePrimaryStateCommand*(self: EnterSeedPhraseState, controller: Controller) =
controller.buildNewSeedPhraseKeypairAndAddItToOrigin()
method getNextPrimaryState*(self: EnterSeedPhraseState, controller: Controller): State =
return createState(StateType.Main, nil)

View File

@ -0,0 +1,12 @@
type
EnterSeedPhraseWord1State* = ref object of State
proc newEnterSeedPhraseWord1State*(backState: State): EnterSeedPhraseWord1State =
result = EnterSeedPhraseWord1State()
result.setup(StateType.EnterSeedPhraseWord1, backState)
proc delete*(self: EnterSeedPhraseWord1State) =
self.State.delete
method getNextPrimaryState*(self: EnterSeedPhraseWord1State, controller: Controller): State =
return createState(StateType.EnterSeedPhraseWord2, self)

View File

@ -0,0 +1,12 @@
type
EnterSeedPhraseWord2State* = ref object of State
proc newEnterSeedPhraseWord2State*(backState: State): EnterSeedPhraseWord2State =
result = EnterSeedPhraseWord2State()
result.setup(StateType.EnterSeedPhraseWord2, backState)
proc delete*(self: EnterSeedPhraseWord2State) =
self.State.delete
method getNextPrimaryState*(self: EnterSeedPhraseWord2State, controller: Controller): State =
return createState(StateType.ConfirmSeedPhraseBackup, self)

View File

@ -0,0 +1,15 @@
type
MainState* = ref object of State
proc newMainState*(backState: State): MainState =
result = MainState()
result.setup(StateType.Main, backState)
proc delete*(self: MainState) =
self.State.delete
method executePrePrimaryStateCommand*(self: MainState, controller: Controller) =
controller.addAccount()
method getNextSecondaryState*(self: MainState, controller: Controller): State =
return createState(StateType.SelectMasterKey, self)

View File

@ -0,0 +1,18 @@
type
SelectMasterKeyState* = ref object of State
proc newSelectMasterKeyState*(backState: State): SelectMasterKeyState =
result = SelectMasterKeyState()
result.setup(StateType.SelectMasterKey, backState)
proc delete*(self: SelectMasterKeyState) =
self.State.delete
method getNextPrimaryState*(self: SelectMasterKeyState, controller: Controller): State =
return createState(StateType.EnterSeedPhrase, self)
method getNextSecondaryState*(self: SelectMasterKeyState, controller: Controller): State =
return createState(StateType.EnterPrivateKey, self)
method getNextTertiaryState*(self: SelectMasterKeyState, controller: Controller): State =
return createState(StateType.ConfirmAddingNewMasterKey, self)

View File

@ -0,0 +1,88 @@
import ../controller
type StateType* {.pure.} = enum
NoState = "NoState"
Main = "Main"
SelectMasterKey = "SelectMasterKey"
EnterSeedPhrase = "EnterSeedPhrase"
EnterSeedPhraseWord1 = "EnterSeedPhraseWord1"
EnterSeedPhraseWord2 = "EnterSeedPhraseWord2"
EnterPrivateKey = "EnterPrivateKey"
EnterKeypairName = "EnterKeypairName"
DisplaySeedPhrase = "DisplaySeedPhrase"
ConfirmAddingNewMasterKey = "ConfirmAddingNewMasterKey"
ConfirmSeedPhraseBackup = "ConfirmSeedPhraseBackup"
## 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.closeAddAccountPopup()
## 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,45 @@
import chronicles
import ../controller
import state
logScope:
topics = "add-account-module-state-factory"
# Forward declaration
proc createState*(stateToBeCreated: StateType, backState: State): State
include confirm_adding_new_master_key_state
include confirm_seed_phrase_backup_state
include display_seed_phrase_state
include enter_keypair_name_state
include enter_private_key_state
include enter_seed_phrase_state
include enter_seed_phrase_word_1_state
include enter_seed_phrase_word_2_state
include main_state
include select_master_key_sate
proc createState*(stateToBeCreated: StateType, backState: State): State =
if stateToBeCreated == StateType.ConfirmAddingNewMasterKey:
return newConfirmAddingNewMasterKeyState(backState)
if stateToBeCreated == StateType.ConfirmSeedPhraseBackup:
return newConfirmSeedPhraseBackupState(backState)
if stateToBeCreated == StateType.DisplaySeedPhrase:
return newDisplaySeedPhraseState(backState)
if stateToBeCreated == StateType.EnterKeypairName:
return newEnterKeypairNameState(backState)
if stateToBeCreated == StateType.EnterPrivateKey:
return newEnterPrivateKeyState(backState)
if stateToBeCreated == StateType.EnterSeedPhrase:
return newEnterSeedPhraseState(backState)
if stateToBeCreated == StateType.EnterSeedPhraseWord1:
return newEnterSeedPhraseWord1State(backState)
if stateToBeCreated == StateType.EnterSeedPhraseWord2:
return newEnterSeedPhraseWord2State(backState)
if stateToBeCreated == StateType.Main:
return newMainState(backState)
if stateToBeCreated == StateType.SelectMasterKey:
return newSelectMasterKeyState(backState)
error "Add account - 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,105 @@
import Tables, NimQml
import ../../../../../app_service/service/accounts/dto/generated_accounts
import ../../../../../app_service/service/wallet_account/derived_address
from ../../../../../app_service/service/keycard/service import KeycardEvent
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
method delete*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method load*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method closeAddAccountPopup*(self: AccessInterface, switchToAccWithAddress: string = "") {.base.} =
raise newException(ValueError, "No implementation available")
method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method getSeedPhrase*(self: AccessInterface): string {.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 onSecondaryActionClicked*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onTertiaryActionClicked*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onQuaternaryActionClicked*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onCancelActionClicked*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method addAccount*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method authenticateForEditingDerivationPath*(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 changeSelectedOrigin*(self: AccessInterface, keyUid: string) {.base.} =
raise newException(ValueError, "No implementation available")
method changeDerivationPath*(self: AccessInterface, path: string) {.base.} =
raise newException(ValueError, "No implementation available")
method changeSelectedDerivedAddress*(self: AccessInterface, address: string) {.base.} =
raise newException(ValueError, "No implementation available")
method changeWatchOnlyAccountAddress*(self: AccessInterface, address: 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 resetDerivationPath*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onDerivedAddressesFetched*(self: AccessInterface, derivedAddresses: seq[DerivedAddressDto], error: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onDerivedAddressesFromMnemonicFetched*(self: AccessInterface, derivedAddresses: seq[DerivedAddressDto], error: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onAddressesFromNotImportedMnemonicFetched*(self: AccessInterface, derivations: Table[string, DerivedAccountDetails], error: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onAddressDetailsFetched*(self: AccessInterface, derivedAddresses: seq[DerivedAddressDto], error: string) {.base.} =
raise newException(ValueError, "No implementation available")
method startScanningForActivity*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onDerivedAddressesFromKeycardFetched*(self: AccessInterface, keycardFlowType: string, keycardEvent: KeycardEvent,
paths: seq[string]) {.base.} =
raise newException(ValueError, "No implementation available")
method buildNewPrivateKeyKeypairAndAddItToOrigin*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method buildNewSeedPhraseKeypairAndAddItToOrigin*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
type
DelegateInterface* = concept c
c.onAddAccountModuleLoaded()
c.destroyAddAccountPopup(string)

View File

@ -0,0 +1,636 @@
import NimQml, Tables, strutils, sequtils, sugar, chronicles
import io_interface
import view, controller, derived_address_model
import internal/[state, state_factory]
import ../../../../core/eventemitter
import ../../../../global/global_singleton
import ../../../shared/keypairs
import ../../../shared_models/[keypair_model]
import ../../../shared_modules/keycard_popup/module as keycard_shared_module
import ../../../../../app_service/common/account_constants
import ../../../../../app_service/service/accounts/service as accounts_service
import ../../../../../app_service/service/wallet_account/service as wallet_account_service
import ../../../../../app_service/service/keycard/service as keycard_service
export io_interface
const Label_NewWatchOnlyAccount = "LABEL-NEW-WATCH-ONLY-ACCOUNT"
const Label_Existing = "LABEL-EXISTING"
const Label_ImportNew = "LABEL-IMPORT-NEW"
const Label_OptionAddNewMasterKey = "LABEL-OPTION-ADD-NEW-MASTER-KEY"
const Label_OptionAddWatchOnlyAcc = "LABEL-OPTION-ADD-WATCH-ONLY-ACC"
const MaxNumOfGeneratedAddresses = 100
const NumOfGeneratedAddressesRegular = MaxNumOfGeneratedAddresses
const NumOfGeneratedAddressesKeycard = 10
logScope:
topics = "wallet-add-account-module"
type
AuthenticationReason {.pure.} = enum
AddingAccount = 0
EditingDerivationPath
type
Module*[T: io_interface.DelegateInterface] = ref object of io_interface.AccessInterface
delegate: T
events: EventEmitter
view: View
viewVariant: QVariant
controller: Controller
accountsService: accounts_service.Service
walletAccountService: wallet_account_service.Service
authenticationReason: AuthenticationReason
## Forward declaration
proc doAddingAccount[T](self: Module[T])
proc newModule*[T](delegate: T,
events: EventEmitter,
keycardService: keycard_service.Service,
accountsService: accounts_service.Service,
walletAccountService: wallet_account_service.Service):
Module[T] =
result = Module[T]()
result.delegate = delegate
result.events = events
result.walletAccountService = walletAccountService
result.view = newView(result)
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events, accountsService, walletAccountService, keycardService)
result.authenticationReason = AuthenticationReason.AddingAccount
method delete*[T](self: Module[T]) =
self.view.delete
self.viewVariant.delete
self.controller.delete
method load*[T](self: Module[T]) =
self.controller.init()
self.view.setCurrentState(newMainState(nil))
var items = keypairs.buildKeyPairsList(self.controller.getWalletAccounts(), self.controller.getAllMigratedKeyPairs(),
excludeAlreadyMigratedPairs = false)
if items.len == 0:
error "list of identified keypairs is empty, but it must have at least a profile keypair"
return
if items.len > 1:
var item = newKeyPairItem(keyUid = Label_Existing)
items.insert(item, 1)
var item = newKeyPairItem(keyUid = Label_ImportNew)
items.add(item)
item = newKeyPairItem(keyUid = Label_OptionAddNewMasterKey)
item.setIcon("objects")
items.add(item)
item = newKeyPairItem(keyUid = Label_OptionAddWatchOnlyAcc)
item.setName(Label_NewWatchOnlyAccount)
item.setIcon("objects")
items.add(item)
self.view.setDisablePopup(false)
self.view.setOriginModelItems(items)
self.changeSelectedOrigin(items[0].getKeyUid())
self.delegate.onAddAccountModuleLoaded()
proc tryKeycardSync[T](self: Module[T]) =
if self.controller.getPin().len == 0:
return
let dataForKeycardToSync = SharedKeycarModuleArgs(
pin: self.controller.getPin(),
keyUid: self.controller.getAuthenticatedKeyUid()
)
self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_TRY_KEYCARD_SYNC, dataForKeycardToSync)
method closeAddAccountPopup*[T](self: Module[T], switchToAccWithAddress: string = "") =
self.tryKeycardSync()
self.delegate.destroyAddAccountPopup(switchToAccWithAddress)
method getModuleAsVariant*[T](self: Module[T]): QVariant =
return self.viewVariant
method getSeedPhrase*[T](self: Module[T]): string =
return self.controller.getSeedPhrase()
method onBackActionClicked*[T](self: Module[T]) =
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "waa_cannot resolve current state"
return
debug "waa_back_action", currState=currStateObj.stateType()
currStateObj.executePreBackStateCommand(self.controller)
let backState = currStateObj.getBackState()
if backState.isNil:
return
self.view.setCurrentState(backState)
debug "waa_back_action - set state", newCurrState=backState.stateType()
method onCancelActionClicked*[T](self: Module[T]) =
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "waa_cannot resolve current state"
return
debug "waa_cancel_action", currState=currStateObj.stateType()
currStateObj.executeCancelCommand(self.controller)
method onPrimaryActionClicked*[T](self: Module[T]) =
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "waa_cannot resolve current state"
return
debug "waa_primary_action", currState=currStateObj.stateType()
currStateObj.executePrePrimaryStateCommand(self.controller)
let nextState = currStateObj.getNextPrimaryState(self.controller)
if nextState.isNil:
return
self.view.setCurrentState(nextState)
debug "waa_primary_action - set state", setCurrState=nextState.stateType()
method onSecondaryActionClicked*[T](self: Module[T]) =
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "waa_cannot resolve current state"
return
debug "waa_secondary_action", currState=currStateObj.stateType()
currStateObj.executePreSecondaryStateCommand(self.controller)
let nextState = currStateObj.getNextSecondaryState(self.controller)
if nextState.isNil:
return
self.view.setCurrentState(nextState)
debug "waa_secondary_action - set state", setCurrState=nextState.stateType()
method onTertiaryActionClicked*[T](self: Module[T]) =
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "waa_cannot resolve current state"
return
debug "waa_tertiary_action", currState=currStateObj.stateType()
currStateObj.executePreTertiaryStateCommand(self.controller)
let nextState = currStateObj.getNextTertiaryState(self.controller)
if nextState.isNil:
return
self.view.setCurrentState(nextState)
debug "waa_tertiarry_action - set state", setCurrState=nextState.stateType()
method onQuaternaryActionClicked*[T](self: Module[T]) =
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "waa_cannot resolve current state"
return
debug "waa_quaternary_action", currState=currStateObj.stateType()
currStateObj.executePreQuaternaryStateCommand(self.controller)
let nextState = currStateObj.getNextQuaternaryState(self.controller)
if nextState.isNil:
return
self.view.setCurrentState(nextState)
debug "waa_quaternary_action - set state", setCurrState=nextState.stateType()
proc isKeyPairAlreadyAdded[T](self: Module[T], keyUid: string): bool =
return self.controller.getWalletAccounts().filter(x => x.keyUid == keyUid).len > 0
proc getNumOfAddressesToGenerate[T](self: Module[T]): int =
let selectedOrigin = self.view.getSelectedOrigin()
if selectedOrigin.getMigratedToKeycard():
let walletAccounts = self.controller.getWalletAccounts()
let numOfAlreadyCreated = walletAccounts.filter(x => x.keyUid == selectedOrigin.getKeyUid()).len
let final = NumOfGeneratedAddressesKeycard + numOfAlreadyCreated # In case of a Keycard keypair always offer 10 available addresses
if final < MaxNumOfGeneratedAddresses:
return final
return MaxNumOfGeneratedAddresses
return NumOfGeneratedAddressesRegular
proc fetchAddressForDerivationPath[T](self: Module[T]) =
let derivationPath = self.view.getDerivationPath()
let derivedAddress = self.view.getSelectedDerivedAddress()
if derivationPath == derivedAddress.getPath() and derivedAddress.getAddress().len > 0:
return
self.view.setScanningForActivityIsOngoing(false)
self.view.derivedAddressModel().reset()
self.view.setSelectedDerivedAddress(newDerivedAddressItem())
let selectedOrigin = self.view.getSelectedOrigin()
var paths: seq[string]
if derivationPath.endsWith("/"):
for i in 0 ..< self.getNumOfAddressesToGenerate():
let path = derivationPath & $i
# exclude paths for which user already has created accounts
if selectedOrigin.containsAccountPath(path):
continue
paths.add(path)
else:
paths.add(derivationPath)
if selectedOrigin.getPairType() == KeyPairType.Profile.int or
selectedOrigin.getPairType() == KeyPairType.SeedImport.int:
if not self.isKeyPairAlreadyAdded(selectedOrigin.getKeyUid()):
self.controller.fetchAddressesFromNotImportedSeedPhrase(self.controller.getSeedPhrase(), paths)
return
if selectedOrigin.getMigratedToKeycard():
self.controller.fetchAddressesFromKeycard(paths)
return
self.controller.fetchDerivedAddresses(selectedOrigin.getDerivedFrom(), paths)
return
error "derivation is not supported for other than profile and seed imported keypairs for origin"
proc authenticateSelectedOrigin[T](self: Module[T], reason: AuthenticationReason) =
let selectedOrigin = self.view.getSelectedOrigin()
self.authenticationReason = reason
if selectedOrigin.getMigratedToKeycard():
self.controller.authenticateOrigin(selectedOrigin.getKeyUid())
return
self.controller.authenticateOrigin()
method onUserAuthenticated*[T](self: Module[T], pin: string, password: string, keyUid: string) =
if password.len == 0:
info "unsuccessful authentication"
return
self.controller.setPin(pin)
self.controller.setPassword(password)
self.controller.setAuthenticatedKeyUid(keyUid)
if self.authenticationReason == AuthenticationReason.AddingAccount:
self.view.setDisablePopup(true)
let selectedOrigin = self.view.getSelectedOrigin()
if selectedOrigin.getPairType() == KeyPairType.PrivateKeyImport.int:
self.doAddingAccount() # we're sure that we need to add an account from priv key from here, cause derivation is not possible for imported priv key
return
self.fetchAddressForDerivationPath()
return
if self.authenticationReason == AuthenticationReason.EditingDerivationPath:
self.view.setActionAuthenticated(true)
self.fetchAddressForDerivationPath()
proc isAuthenticationNeededForSelectedOrigin[T](self: Module[T]): bool =
let selectedOrigin = self.view.getSelectedOrigin()
if selectedOrigin.isNil:
error "selected origin is not set"
return true
if selectedOrigin.getPairType() == KeyPairType.Unknown.int and
selectedOrigin.getKeyUid() == Label_OptionAddWatchOnlyAcc:
return false
if selectedOrigin.getKeyUid() == self.controller.getAuthenticatedKeyUid():
return false
if not selectedOrigin.getMigratedToKeycard() and self.controller.getAuthenticatedKeyUid() == singletonInstance.userProfile.getKeyUid():
return false
return true
method changeDerivationPath*[T](self: Module[T], derivationPath: string) =
self.view.setDerivationPath(derivationPath)
if self.isAuthenticationNeededForSelectedOrigin():
return
self.fetchAddressForDerivationPath()
proc resolveSuggestedPathForSelectedOrigin[T](self: Module[T]): tuple[suggestedPath: string, usedIndex: int] =
let selectedOrigin = self.view.getSelectedOrigin()
var nextIndex = 0
if self.isKeyPairAlreadyAdded(selectedOrigin.getKeyUid()):
nextIndex = selectedOrigin.getLastUsedDerivationIndex() + 1
var suggestedPath = account_constants.PATH_WALLET_ROOT & "/" & $nextIndex
let walletAccounts = self.controller.getWalletAccounts()
if walletAccounts.filter(x => x.keyUid == selectedOrigin.getKeyUid() and x.path == suggestedPath).len == 0:
return (suggestedPath, nextIndex)
nextIndex.inc
for i in nextIndex ..< self.getNumOfAddressesToGenerate():
suggestedPath = account_constants.PATH_WALLET_ROOT & "/" & $i
if walletAccounts.filter(x => x.keyUid == selectedOrigin.getKeyUid() and x.path == suggestedPath).len == 0:
return (suggestedPath, i)
error "we couldn't find available path for new account"
method resetDerivationPath*[T](self: Module[T]) =
let selectedOrigin = self.view.getSelectedOrigin()
let (suggestedPath, _) = self.resolveSuggestedPathForSelectedOrigin()
self.changeDerivationPath(suggestedPath)
proc setItemForSelectedOrigin[T](self: Module[T], item: KeyPairItem) =
if item.isNil:
error "provided item cannot be set as selected origin", keyUid=item.getKeyUid()
return
self.view.setSelectedOrigin(item)
if item.getKeyUid() == Label_OptionAddWatchOnlyAcc:
self.view.setWatchOnlyAccAddress(newDerivedAddressItem())
return
let (suggestedPath, _) = self.resolveSuggestedPathForSelectedOrigin()
self.view.setSuggestedDerivationPath(suggestedPath)
self.view.setDerivationPath(suggestedPath)
if self.isAuthenticationNeededForSelectedOrigin():
self.controller.setAuthenticatedKeyUid("")
self.controller.setPin("")
self.controller.setPassword("")
self.view.setActionAuthenticated(false)
else:
self.fetchAddressForDerivationPath()
method changeSelectedOrigin*[T](self: Module[T], keyUid: string) =
let item = self.view.originModel().findItemByKeyUid(keyUid)
self.setItemForSelectedOrigin(item)
method changeSelectedDerivedAddress*[T](self: Module[T], address: string) =
let item = self.view.derivedAddressModel().getItemByAddress(address)
if item.isNil:
error "cannot resolve derived address item by provided address", address=address
return
self.view.setSelectedDerivedAddress(item)
self.view.setDerivationPath(item.getPath())
self.view.setScanningForActivityIsOngoing(true)
self.controller.fetchDetailsForAddresses(@[address])
method changeWatchOnlyAccountAddress*[T](self: Module[T], address: string) =
self.view.setScanningForActivityIsOngoing(false)
self.view.setWatchOnlyAccAddress(newDerivedAddressItem(order = 0, address = address))
if address.len == 0:
return
self.view.setScanningForActivityIsOngoing(true)
self.view.setWatchOnlyAccAddress(newDerivedAddressItem(order = 0, address = address))
self.controller.fetchDetailsForAddresses(@[address])
method changePrivateKey*[T](self: Module[T], privateKey: string) =
self.view.setScanningForActivityIsOngoing(false)
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
self.view.setScanningForActivityIsOngoing(true)
self.view.setPrivateKeyAccAddress(newDerivedAddressItem(order = 0, address = genAccDto.address))
self.controller.fetchDetailsForAddresses(@[genAccDto.address])
method changeSeedPhrase*[T](self: Module[T], seedPhrase: string) =
let genAccDto = self.controller.createAccountFromSeedPhrase(seedPhrase)
self.view.setSelectedDerivedAddress(newDerivedAddressItem())
if 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 keyUid = self.controller.getKeyUidForSeedPhrase(seedPhrase)
return not self.isKeyPairAlreadyAdded(keyUid)
proc setDerivedAddresses[T](self: Module[T], derivedAddresses: seq[DerivedAddressDto]) =
var items: seq[DerivedAddressItem]
let derivationPath = self.view.getDerivationPath()
if derivationPath.endsWith("/"):
for i in 0 ..< self.getNumOfAddressesToGenerate():
let path = derivationPath & $i
for d in derivedAddresses:
if d.path == path:
items.add(newDerivedAddressItem(order = i, address = d.address, path = d.path, alreadyCreated = d.alreadyCreated))
break
self.view.derivedAddressModel().setItems(items)
else:
for d in derivedAddresses:
if d.path == derivationPath:
items.add(newDerivedAddressItem(order = 0, address = d.address, path = d.path, alreadyCreated = d.alreadyCreated))
break
self.view.derivedAddressModel().setItems(items)
self.changeSelectedDerivedAddress(items[0].getAddress())
method onDerivedAddressesFetched*[T](self: Module[T], derivedAddresses: seq[DerivedAddressDto], error: string) =
if error.len > 0:
error "fetching derived addresses error", err=error
return
let selectedOrigin = self.view.getSelectedOrigin()
if selectedOrigin.getPairType() != KeyPairType.Profile.int and
selectedOrigin.getPairType() != KeyPairType.SeedImport.int:
error "received derived addresses reffer to profile or seed imported origin, but that's not the selected origin"
return
self.setDerivedAddresses(derivedAddresses)
if self.authenticationReason == AuthenticationReason.AddingAccount:
self.doAddingAccount()
method onAddressesFromNotImportedMnemonicFetched*[T](self: Module[T], derivations: Table[string, DerivedAccountDetails], error: string) =
if error.len > 0:
error "fetching derived addresses from not imported mnemonic error", err=error
return
let selectedOrigin = self.view.getSelectedOrigin()
if selectedOrigin.getPairType() != KeyPairType.SeedImport.int:
error "received derived addresses from not imported mnemonic reffer to seed imported origin, but that's not the selected origin"
return
var derivedAddresses: seq[DerivedAddressDto]
for path, data in derivations.pairs:
derivedAddresses.add(DerivedAddressDto(
address: data.address,
path: path,
hasActivity: false,
alreadyCreated: false)
)
self.setDerivedAddresses(derivedAddresses)
if self.authenticationReason == AuthenticationReason.AddingAccount:
self.doAddingAccount()
method onDerivedAddressesFromKeycardFetched*[T](self: Module[T], keycardFlowType: string, keycardEvent: KeycardEvent,
paths: seq[string]) =
let selectedOrigin = self.view.getSelectedOrigin()
if not selectedOrigin.getMigratedToKeycard():
error "receiving addresses from a keycard refers to a keycard origin, but selected origin is not a keycard origin"
return
if paths.len != keycardEvent.generatedWalletAccounts.len:
error "unexpected error, keycard didn't generate all addresses we need"
return
var derivedAddresses: seq[DerivedAddressDto]
for i in 0 ..< paths.len:
# we're safe to access `generatedWalletAccounts` by index (read comment in `startExportPublicFlow`)
derivedAddresses.add(DerivedAddressDto(address: keycardEvent.generatedWalletAccounts[i].address,
path: paths[i], hasActivity: false, alreadyCreated: false))
if selectedOrigin.getPairType() != KeyPairType.Profile.int and
selectedOrigin.getPairType() != KeyPairType.SeedImport.int:
error "received derived addresses reffer to profile or seed imported origin, but that's not the selected origin"
return
self.setDerivedAddresses(derivedAddresses)
if self.authenticationReason == AuthenticationReason.AddingAccount:
self.doAddingAccount()
method onAddressDetailsFetched*[T](self: Module[T], derivedAddresses: seq[DerivedAddressDto], error: string) =
if not self.view.getScanningForActivityIsOngoing():
return
if error.len > 0:
error "fetching address details error", err=error
return
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "waa_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,
path = derivedAddresses[0].path,
alreadyCreated = derivedAddresses[0].alreadyCreated,
hasActivity = derivedAddresses[0].hasActivity,
loaded = true)
if currStateObj.stateType() == StateType.EnterPrivateKey:
if cmpIgnoreCase(self.view.getPrivateKeyAccAddress().getAddress(), addressDetailsItem.getAddress()) == 0:
self.view.setPrivateKeyAccAddress(addressDetailsItem)
return
elif currStateObj.stateType() == StateType.Main:
let selectedOrigin = self.view.getSelectedOrigin()
if selectedOrigin.getPairType() == KeyPairType.Unknown.int and
selectedOrigin.getKeyUid() == Label_OptionAddWatchOnlyAcc and
cmpIgnoreCase(self.view.getWatchOnlyAccAddress().getAddress(), addressDetailsItem.getAddress()) == 0:
self.view.setWatchOnlyAccAddress(addressDetailsItem)
return
let selectedAddress = self.view.getSelectedDerivedAddress()
if cmpIgnoreCase(selectedAddress.getAddress(), addressDetailsItem.getAddress()) == 0:
addressDetailsItem.setPath(selectedAddress.getPath())
self.view.setSelectedDerivedAddress(addressDetailsItem)
self.view.derivedAddressModel().updateDetailsForAddressAndBubbleItToTop(addressDetailsItem.getAddress(), addressDetailsItem.getHasActivity())
return
error "derived addresses received in the state in which the app doesn't expect them"
return
error "unknown error, since the length of the response is not expected", length=derivedAddresses.len
method startScanningForActivity*[T](self: Module[T]) =
self.view.setScanningForActivityIsOngoing(true)
let allAddresses = self.view.derivedAddressModel().getAllAddresses()
self.controller.fetchDetailsForAddresses(allAddresses)
method authenticateForEditingDerivationPath*[T](self: Module[T]) =
self.authenticateSelectedOrigin(AuthenticationReason.EditingDerivationPath)
proc doAddingAccount[T](self: Module[T]) =
self.view.setDisablePopup(true)
let
selectedOrigin = self.view.getSelectedOrigin()
selectedAddrItem = self.view.getSelectedDerivedAddress()
(suggestedPath, usedIndex) = self.resolveSuggestedPathForSelectedOrigin()
var
addingNewKeyPair = false
accountType: string
keyPairName = selectedOrigin.getName()
address = selectedAddrItem.getAddress()
path = selectedAddrItem.getPath()
lastUsedDerivationIndex = if selectedAddrItem.getPath() == suggestedPath: usedIndex else: selectedOrigin.getLastUsedDerivationIndex()
rootWalletMasterKey = selectedOrigin.getDerivedFrom()
publicKey = selectedOrigin.getPubKey()
keyUid = selectedOrigin.getKeyUid()
createKeystoreFile = not selectedOrigin.getMigratedToKeycard()
doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser()
if selectedOrigin.getPairType() == KeyPairType.Profile.int:
accountType = account_constants.GENERATED
elif selectedOrigin.getPairType() == KeyPairType.SeedImport.int:
accountType = account_constants.SEED
addingNewKeyPair = not self.isKeyPairAlreadyAdded(keyUid)
elif selectedOrigin.getPairType() == KeyPairType.PrivateKeyImport.int:
let genAcc = self.controller.getGeneratedAccount()
accountType = account_constants.KEY
address = genAcc.address
path = "m" # from private key an address for path `m` is generate (corresponds to the master key address)
lastUsedDerivationIndex = 0
rootWalletMasterKey = ""
publicKey = genAcc.publicKey
keyUid = genAcc.keyUid
addingNewKeyPair = not self.isKeyPairAlreadyAdded(keyUid)
else:
accountType = account_constants.WATCH
createKeystoreFile = false
doPasswordHashing = false
keyPairName = ""
address = self.view.getWatchOnlyAccAddress().getAddress()
path = ""
lastUsedDerivationIndex = 0
rootWalletMasterKey = ""
publicKey = ""
keyUid = ""
var success = false
if addingNewKeyPair:
if selectedOrigin.getPairType() == KeyPairType.PrivateKeyImport.int:
success = self.controller.addNewPrivateKeyAccount(
privateKey = self.controller.getGeneratedAccount().privateKey,
doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser(),
name = self.view.getAccountName(),
keyPairName = keyPairName,
address = address,
path = path,
lastUsedDerivationIndex = lastUsedDerivationIndex,
rootWalletMasterKey = rootWalletMasterKey,
publicKey = publicKey,
keyUid = keyUid,
accountType = accountType,
color = self.view.getSelectedColor(),
emoji = self.view.getSelectedEmoji())
if not success:
error "failed to store new private key account", address=selectedAddrItem.getAddress()
elif selectedOrigin.getPairType() == KeyPairType.SeedImport.int:
success = self.controller.addNewSeedPhraseAccount(
seedPhrase = self.controller.getSeedPhrase(),
doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser(),
name = self.view.getAccountName(),
keyPairName = keyPairName,
address = address,
path = path,
lastUsedDerivationIndex = lastUsedDerivationIndex,
rootWalletMasterKey = rootWalletMasterKey,
publicKey = publicKey,
keyUid = keyUid,
accountType = accountType,
color = self.view.getSelectedColor(),
emoji = self.view.getSelectedEmoji())
if not success:
error "failed to store new seed phrase account", address=selectedAddrItem.getAddress()
else:
success = self.controller.addWalletAccount(
createKeystoreFile = createKeystoreFile,
doPasswordHashing = doPasswordHashing,
name = self.view.getAccountName(),
keyPairName = keyPairName,
address = address,
path = path,
lastUsedDerivationIndex = lastUsedDerivationIndex,
rootWalletMasterKey = rootWalletMasterKey,
publicKey = publicKey,
keyUid = keyUid,
accountType = accountType,
color = self.view.getSelectedColor(),
emoji = self.view.getSelectedEmoji())
if not success:
error "failed to store account", address=selectedAddrItem.getAddress()
if success:
self.closeAddAccountPopup(address)
else:
self.closeAddAccountPopup()
method addAccount*[T](self: Module[T]) =
if self.isAuthenticationNeededForSelectedOrigin():
self.authenticateSelectedOrigin(AuthenticationReason.AddingAccount)
return
self.doAddingAccount()
method buildNewPrivateKeyKeypairAndAddItToOrigin*[T](self: Module[T]) =
let genAcc = self.controller.getGeneratedAccount()
var item = newKeyPairItem(keyUid = genAcc.keyUid,
pubKey = genAcc.publicKey,
locked = false,
name = self.view.getNewKeyPairName(),
image = "",
icon = "key_pair_private_key",
pairType = KeyPairType.PrivateKeyImport)
self.setItemForSelectedOrigin(item)
method buildNewSeedPhraseKeypairAndAddItToOrigin*[T](self: Module[T]) =
let genAcc = self.controller.getGeneratedAccount()
var item = newKeyPairItem(keyUid = genAcc.keyUid,
pubKey = genAcc.publicKey,
locked = false,
name = self.view.getNewKeyPairName(),
image = "",
icon = "key_pair_seed_phrase",
pairType = KeyPairType.SeedImport,
derivedFrom = genAcc.address)
self.setItemForSelectedOrigin(item)

View File

@ -0,0 +1,327 @@
import NimQml
import io_interface
import derived_address_model
import internal/[state, state_wrapper]
import ../../../shared_models/[keypair_model, keypair_item]
QtObject:
type
View* = ref object of QObject
delegate: io_interface.AccessInterface
currentState: StateWrapper
currentStateVariant: QVariant
originModel: KeyPairModel
originModelVariant: QVariant
selectedOrigin: KeyPairItem
selectedOriginVariant: QVariant
derivedAddressModel: DerivedAddressModel
derivedAddressModelVariant: QVariant
selectedDerivedAddress: DerivedAddressItem
selectedDerivedAddressVariant: QVariant
watchOnlyAccAddress: DerivedAddressItem
watchOnlyAccAddressVariant: QVariant
privateKeyAccAddress: DerivedAddressItem
privateKeyAccAddressVariant: QVariant
accountName: string
newKeyPairName: string
selectedEmoji: string
selectedColor: string
derivationPath: string
suggestedDerivationPath: string
actionAuthenticated: bool
scanningForActivityIsOngoing: bool
disablePopup: bool # unables user to interact with the popup (action buttons are disabled as well as close popup button)
proc delete*(self: View) =
self.currentStateVariant.delete
self.currentState.delete
self.originModel.delete
self.originModelVariant.delete
self.selectedOrigin.delete
self.selectedOriginVariant.delete
self.derivedAddressModel.delete
self.derivedAddressModelVariant.delete
self.selectedDerivedAddress.delete
self.selectedDerivedAddressVariant.delete
self.watchOnlyAccAddress.delete
self.watchOnlyAccAddressVariant.delete
self.privateKeyAccAddress.delete
self.privateKeyAccAddressVariant.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.originModel = newKeyPairModel()
result.originModelVariant = newQVariant(result.originModel)
result.selectedOrigin = newKeyPairItem()
result.selectedOriginVariant = newQVariant(result.selectedOrigin)
result.derivedAddressModel = newDerivedAddressModel()
result.derivedAddressModelVariant = newQVariant(result.derivedAddressModel)
result.selectedDerivedAddress = newDerivedAddressItem()
result.selectedDerivedAddressVariant = newQVariant(result.selectedDerivedAddress)
result.watchOnlyAccAddress = newDerivedAddressItem()
result.watchOnlyAccAddressVariant = newQVariant(result.watchOnlyAccAddress)
result.privateKeyAccAddress = newDerivedAddressItem()
result.privateKeyAccAddressVariant = newQVariant(result.privateKeyAccAddress)
result.actionAuthenticated = false
result.scanningForActivityIsOngoing = false
result.disablePopup = false
signalConnect(result.currentState, "backActionClicked()", result, "onBackActionClicked()", 2)
signalConnect(result.currentState, "cancelActionClicked()", result, "onCancelActionClicked()", 2)
signalConnect(result.currentState, "primaryActionClicked()", result, "onPrimaryActionClicked()", 2)
signalConnect(result.currentState, "secondaryActionClicked()", result, "onSecondaryActionClicked()", 2)
signalConnect(result.currentState, "tertiaryActionClicked()", result, "onTertiaryActionClicked()", 2)
signalConnect(result.currentState, "quaternaryActionClicked()", result, "onQuaternaryActionClicked()", 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 disablePopupChanged*(self: View) {.signal.}
proc getDisablePopup*(self: View): bool {.slot.} =
return self.disablePopup
QtProperty[bool] disablePopup:
read = getDisablePopup
notify = disablePopupChanged
proc setDisablePopup*(self: View, value: bool) =
self.disablePopup = value
self.disablePopupChanged()
proc getSeedPhrase*(self: View): string {.slot.} =
return self.delegate.getSeedPhrase()
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 onSecondaryActionClicked*(self: View) {.slot.} =
self.delegate.onSecondaryActionClicked()
proc onTertiaryActionClicked*(self: View) {.slot.} =
self.delegate.onTertiaryActionClicked()
proc onQuaternaryActionClicked*(self: View) {.slot.} =
self.delegate.onQuaternaryActionClicked()
proc originModelChanged*(self: View) {.signal.}
proc getOriginModel*(self: View): QVariant {.slot.} =
if self.originModelVariant.isNil:
return newQVariant()
return self.originModelVariant
QtProperty[QVariant] originModel:
read = getOriginModel
notify = originModelChanged
proc originModel*(self: View): KeyPairModel =
return self.originModel
proc setOriginModelItems*(self: View, items: seq[KeyPairItem]) =
self.originModel.setItems(items)
self.originModelChanged()
proc getSelectedOrigin*(self: View): KeyPairItem =
return self.selectedOrigin
proc getSelectedOriginAsVariant*(self: View): QVariant {.slot.} =
return self.selectedOriginVariant
QtProperty[QVariant] selectedOrigin:
read = getSelectedOriginAsVariant
proc setSelectedOrigin*(self: View, item: KeyPairItem) =
self.selectedOrigin.setItem(item)
proc getDerivedAddressModel(self: View): QVariant {.slot.} =
if self.derivedAddressModelVariant.isNil:
return newQVariant()
return self.derivedAddressModelVariant
QtProperty[QVariant] derivedAddressModel:
read = getDerivedAddressModel
proc derivedAddressModel*(self: View): DerivedAddressModel =
return self.derivedAddressModel
proc getSelectedDerivedAddress*(self: View): DerivedAddressItem =
return self.selectedDerivedAddress
proc selectedDerivedAddressChanged*(self: View) {.signal.}
proc getSelectedDerivedAddressVariant*(self: View): QVariant {.slot.} =
return self.selectedDerivedAddressVariant
QtProperty[QVariant] selectedDerivedAddress:
read = getSelectedDerivedAddressVariant
notify = selectedDerivedAddressChanged
proc setSelectedDerivedAddress*(self: View, item: DerivedAddressItem) =
self.selectedDerivedAddress.setItem(item)
self.selectedDerivedAddressChanged()
proc getWatchOnlyAccAddress*(self: View): DerivedAddressItem =
return self.watchOnlyAccAddress
proc watchOnlyAccAddressChanged*(self: View) {.signal.}
proc getWatchOnlyAccAddressVariant*(self: View): QVariant {.slot.} =
return self.watchOnlyAccAddressVariant
QtProperty[QVariant] watchOnlyAccAddress:
read = getWatchOnlyAccAddressVariant
notify = watchOnlyAccAddressChanged
proc setWatchOnlyAccAddress*(self: View, item: DerivedAddressItem) =
self.watchOnlyAccAddress.setItem(item)
self.watchOnlyAccAddressChanged()
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 actionAuthenticatedChanged*(self: View) {.signal.}
proc getActionAuthenticated*(self: View): bool {.slot.} =
return self.actionAuthenticated
proc setActionAuthenticated*(self: View, value: bool) {.slot.} =
self.actionAuthenticated = value
self.actionAuthenticatedChanged()
QtProperty[bool] actionAuthenticated:
read = getActionAuthenticated
write = setActionAuthenticated
notify = actionAuthenticatedChanged
proc scanningForActivityChanged*(self: View) {.signal.}
proc getScanningForActivityIsOngoing*(self: View): bool {.slot.} =
return self.scanningForActivityIsOngoing
proc setScanningForActivityIsOngoing*(self: View, value: bool) {.slot.} =
self.scanningForActivityIsOngoing = value
self.scanningForActivityChanged()
QtProperty[bool] scanningForActivityIsOngoing:
read = getScanningForActivityIsOngoing
write = setScanningForActivityIsOngoing
notify = scanningForActivityChanged
proc accountNameChanged*(self: View) {.signal.}
proc setAccountName*(self: View, value: string) {.slot.} =
if self.accountName == value:
return
self.accountName = value
self.accountNameChanged()
proc getAccountName*(self: View): string {.slot.} =
return self.accountName
QtProperty[string] accountName:
read = getAccountName
write = setAccountName
notify = accountNameChanged
proc newKeyPairNameChanged*(self: View) {.signal.}
proc setNewKeyPairName*(self: View, value: string) {.slot.} =
if self.newKeyPairName == value:
return
self.newKeyPairName = value
self.newKeyPairNameChanged()
proc getNewKeyPairName*(self: View): string {.slot.} =
return self.newKeyPairName
QtProperty[string] newKeyPairName:
read = getNewKeyPairName
write = setNewKeyPairName
notify = newKeyPairNameChanged
proc selectedEmojiChanged*(self: View) {.signal.}
proc setSelectedEmoji*(self: View, value: string) {.slot.} =
if self.selectedEmoji == value:
return
self.selectedEmoji = value
self.selectedEmojiChanged()
proc getSelectedEmoji*(self: View): string {.slot.} =
return self.selectedEmoji
QtProperty[string] selectedEmoji:
read = getSelectedEmoji
write = setSelectedEmoji
notify = selectedEmojiChanged
proc selectedColorChanged*(self: View) {.signal.}
proc setSelectedColor*(self: View, value: string) {.slot.} =
if self.selectedColor == value:
return
self.selectedColor = value
self.selectedColorChanged()
proc getSelectedColor*(self: View): string {.slot.} =
return self.selectedColor
QtProperty[string] selectedColor:
read = getSelectedColor
write = setSelectedColor
notify = selectedColorChanged
proc derivationPathChanged*(self: View) {.signal.}
proc getDerivationPath*(self: View): string {.slot.} =
return self.derivationPath
proc setDerivationPath*(self: View, value: string) {.slot.} =
if self.derivationPath == value:
return
self.derivationPath = value
self.derivationPathChanged()
QtProperty[string] derivationPath:
read = getDerivationPath
write = setDerivationPath
notify = derivationPathChanged
proc suggestedDerivationPathChanged*(self: View) {.signal.}
proc getSuggestedDerivationPath*(self: View): string {.slot.} =
return self.suggestedDerivationPath
QtProperty[string] suggestedDerivationPath:
read = getSuggestedDerivationPath
notify = suggestedDerivationPathChanged
proc setSuggestedDerivationPath*(self: View, value: string) =
self.suggestedDerivationPath = value
self.suggestedDerivationPathChanged()
proc changeSelectedOrigin*(self: View, keyUid: string) {.slot.} =
self.delegate.changeSelectedOrigin(keyUid)
proc changeDerivationPath*(self: View, path: string) {.slot.} =
self.delegate.changeDerivationPath(path)
proc changeSelectedDerivedAddress*(self: View, address: string) {.slot.} =
self.delegate.changeSelectedDerivedAddress(address)
proc changeWatchOnlyAccountAddress*(self: View, address: string) {.slot.} =
self.delegate.changeWatchOnlyAccountAddress(address)
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 resetDerivationPath*(self: View) {.slot.} =
self.delegate.resetDerivationPath()
proc authenticateForEditingDerivationPath*(self: View) {.slot.} =
self.delegate.authenticateForEditingDerivationPath()
proc startScanningForActivity*(self: View) {.slot.} =
self.delegate.startScanningForActivity()

View File

@ -1,3 +1,4 @@
import NimQml
import ../../shared_models/currency_amount
export CurrencyAmount
@ -56,3 +57,15 @@ method savedAddressesModuleDidLoad*(self: AccessInterface) {.base.} =
method buySellCryptoModuleDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method runAddAccountPopup*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method getAddAccountModule*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
method onAddAccountModuleLoaded*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method destroyAddAccountPopup*(self: AccessInterface, switchToAccWithAddress: string = "") {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -1,4 +1,4 @@
import NimQml
import NimQml, chronicles
import ./controller, ./view
import ./io_interface as io_interface
@ -11,6 +11,7 @@ import ./current_account/module as current_account_module
import ./transactions/module as transactions_module
import ./saved_addresses/module as saved_addresses_module
import ./buy_sell_crypto/module as buy_sell_crypto_module
import ./add_account/module as add_account_module
import ../../../global/global_singleton
import ../../../core/eventemitter
@ -27,6 +28,9 @@ import ../../../../app_service/service/accounts/service as accounts_service
import ../../../../app_service/service/node/service as node_service
import ../../../../app_service/service/network_connection/service as network_connection_service
logScope:
topics = "wallet-section-module"
import io_interface
export io_interface
@ -45,7 +49,10 @@ type
transactionsModule: transactions_module.AccessInterface
savedAddressesModule: saved_addresses_module.AccessInterface
buySellCryptoModule: buy_sell_crypto_module.AccessInterface
addAccountModule: add_account_module.AccessInterface
keycardService: keycard_service.Service
accountsService: accounts_service.Service
walletAccountService: wallet_account_service.Service
proc newModule*(
delegate: delegate_interface.AccessInterface,
@ -66,6 +73,9 @@ proc newModule*(
result = Module()
result.delegate = delegate
result.events = events
result.keycardService = keycardService
result.accountsService = accountsService
result.walletAccountService = walletAccountService
result.moduleLoaded = false
result.controller = newController(result, settingsService, walletAccountService, currencyService)
result.view = newView(result)
@ -88,6 +98,8 @@ method delete*(self: Module) =
self.buySellCryptoModule.delete
self.controller.delete
self.view.delete
if not self.addAccountModule.isNil:
self.addAccountModule.delete
method updateCurrency*(self: Module, currency: string) =
self.controller.updateCurrency(currency)
@ -193,3 +205,25 @@ method savedAddressesModuleDidLoad*(self: Module) =
method buySellCryptoModuleDidLoad*(self: Module) =
self.checkIfModuleDidLoad()
method destroyAddAccountPopup*(self: Module, switchToAccWithAddress: string = "") =
if not self.addAccountModule.isNil:
if switchToAccWithAddress.len > 0:
self.switchAccountByAddress(switchToAccWithAddress)
self.view.emitDestroyAddAccountPopup()
self.addAccountModule.delete
self.addAccountModule = nil
method runAddAccountPopup*(self: Module) =
self.destroyAddAccountPopup()
self.addAccountModule = add_account_module.newModule(self, self.events, self.keycardService, self.accountsService,
self.walletAccountService)
self.addAccountModule.load()
method getAddAccountModule*(self: Module): QVariant =
if self.addAccountModule.isNil:
return newQVariant()
return self.addAccountModule.getModuleAsVariant()
method onAddAccountModuleLoaded*(self: Module) =
self.view.emitDisplayAddAccountPopup()

View File

@ -97,3 +97,19 @@ QtObject:
self.signingPhrase = signingPhrase
self.isMnemonicBackedUp = mnemonicBackedUp
self.currentCurrencyChanged()
proc runAddAccountPopup*(self: View) {.slot.} =
self.delegate.runAddAccountPopup()
proc getAddAccountModule(self: View): QVariant {.slot.} =
return self.delegate.getAddAccountModule()
QtProperty[QVariant] addAccountModule:
read = getAddAccountModule
proc displayAddAccountPopup*(self: View) {.signal.}
proc emitDisplayAddAccountPopup*(self: View) =
self.displayAddAccountPopup()
proc destroyAddAccountPopup*(self: View) {.signal.}
proc emitDestroyAddAccountPopup*(self: View) =
self.destroyAddAccountPopup()

View File

@ -453,18 +453,24 @@ method runFlow*[T](self: Module[T], flowToRun: FlowType, keyUid = "", bip44Path
return
if flowToRun == FlowType.Authentication:
self.controller.connectKeychainSignals()
if keyUid.len > 0:
self.prepareKeyPairItemForAuthentication(keyUid)
self.tmpLocalState = newReadingKeycardState(flowToRun, nil)
self.controller.runAuthenticationFlow(keyUid)
return
if keyUid.len == 0 or keyUid == singletonInstance.userProfile.getKeyUid():
if singletonInstance.userProfile.getUsingBiometricLogin():
self.controller.tryToObtainDataFromKeychain()
return
if singletonInstance.userProfile.getIsKeycardUser():
self.prepareKeyPairItemForAuthentication(singletonInstance.userProfile.getKeyUid())
self.tmpLocalState = newReadingKeycardState(flowToRun, nil)
self.controller.runAuthenticationFlow(singletonInstance.userProfile.getKeyUid())
return
self.view.setCurrentState(newEnterPasswordState(flowToRun, nil))
self.authenticationPopupIsAlreadyRunning = true
self.controller.readyToDisplayPopup()
return
else:
self.prepareKeyPairItemForAuthentication(keyUid)
self.tmpLocalState = newReadingKeycardState(flowToRun, nil)
self.controller.runAuthenticationFlow(keyUid)
return
if flowToRun == FlowType.UnlockKeycard:
## since we can run unlock keycard flow from an already running flow, in order to avoid changing displayed keypair
## (locked keypair) we have to set keycard uid of a keycard used in the flow we're jumping from to `UnlockKeycard` flow.

View File

@ -423,8 +423,8 @@ Item {
anchors.fill: parent
verticalAlignment: parent.verticalAlignment
font.pixelSize: 15
wrapMode: root.multiline ? Text.WordWrap : Text.NoWrap
elide: StatusBaseText.ElideRight
wrapMode: root.multiline ? Text.WrapAnywhere : Text.NoWrap
elide: root.multiline? Text.ElideNone : Text.ElideRight
color: root.enabled ? Theme.palette.baseColor1 : Theme.palette.directColor6
}
}

View File

@ -138,7 +138,7 @@ ThemePalette {
}
statusMenu: QtObject {
property color backgroundColor: baseColor2
property color backgroundColor: baseColor3
property color hoverBackgroundColor: directColor7
property color separatorColor: directColor7
}

View File

@ -321,7 +321,7 @@ Item {
if (model.username === Constants.appTranslatableConstants.loginAccountsListAddNewUser ||
model.username === Constants.appTranslatableConstants.loginAccountsListAddExistingUser ||
model.username === Constants.appTranslatableConstants.loginAccountsListLostKeycard) {
return Constants.appTranslationMap[model.username]
return Utils.appTranslation(model.username)
}
return model.username
}

View File

@ -0,0 +1,260 @@
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 "./states"
StatusModal {
id: root
property AddAccountStore store
width: Constants.addAccountPopup.popupWidth
height: {
let availableSpace = Global.applicationWindow.height - root.margins * 2
return Math.min(implicitHeight, availableSpace)
}
closePolicy: root.store.disablePopup? Popup.NoAutoClose : Popup.CloseOnEscape | Popup.CloseOnPressOutside
hasCloseButton: !root.store.disablePopup
header.title: qsTr("Add a new account")
onOpened: {
root.store.resetStoreValues()
}
onClosed: {
root.store.currentState.doCancelAction()
}
contentItem: StatusScrollView {
id: scrollView
implicitWidth: contentWidth + leftPadding + rightPadding
implicitHeight: contentHeight + topPadding + bottomPadding
padding: 0
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
width: parent.width
height: {
let availableSpace = Global.applicationWindow.height - (root.margins * 2 + root.topPadding + root.bottomPadding)
return Math.min(content.height, availableSpace)
}
Item {
id: content
objectName: "AddAccountPopup-Content"
width: scrollView.availableWidth
Loader {
id: loader
width: parent.width
sourceComponent: {
switch (root.store.currentState.stateType) {
case Constants.addAccountPopup.state.main:
return mainComponent
case Constants.addAccountPopup.state.confirmAddingNewMasterKey:
return confirmAddingNewMasterKeyComponent
case Constants.addAccountPopup.state.confirmSeedPhraseBackup:
return confirmSeedPhraseBackupComponent
case Constants.addAccountPopup.state.displaySeedPhrase:
return displaySeedPhraseComponent
case Constants.addAccountPopup.state.enterKeypairName:
return enterKeypairNameComponent
case Constants.addAccountPopup.state.enterPrivateKey:
return enterPrivateKeyComponent
case Constants.addAccountPopup.state.enterSeedPhraseWord1:
case Constants.addAccountPopup.state.enterSeedPhraseWord2:
return enterSeedPhraseWordComponent
case Constants.addAccountPopup.state.enterSeedPhrase:
return enterSeedPhraseComponent
case Constants.addAccountPopup.state.selectMasterKey:
return selectMasterKeyComponent
}
return undefined
}
onLoaded: {
content.height = Qt.binding(function(){return item.height})
}
}
Component {
id: mainComponent
Main {
store: root.store
}
}
Component {
id: confirmAddingNewMasterKeyComponent
ConfirmAddingNewMasterKey {
height: Constants.addAccountPopup.contentHeight1
store: root.store
}
}
Component {
id: confirmSeedPhraseBackupComponent
ConfirmSeedPhraseBackup {
height: Constants.addAccountPopup.contentHeight1
store: root.store
}
}
Component {
id: displaySeedPhraseComponent
DisplaySeedPhrase {
height: Constants.addAccountPopup.contentHeight1
store: root.store
}
}
Component {
id: enterKeypairNameComponent
EnterKeypairName {
height: Constants.addAccountPopup.contentHeight1
store: root.store
}
}
Component {
id: enterPrivateKeyComponent
EnterPrivateKey {
height: Constants.addAccountPopup.contentHeight1
store: root.store
}
}
Component {
id: enterSeedPhraseComponent
EnterSeedPhrase {
height: Constants.addAccountPopup.contentHeight2
store: root.store
}
}
Component {
id: enterSeedPhraseWordComponent
EnterSeedPhraseWord {
height: Constants.addAccountPopup.contentHeight1
store: root.store
}
}
Component {
id: selectMasterKeyComponent
SelectMasterKey {
height: Constants.addAccountPopup.contentHeight1
store: root.store
}
}
}
}
leftButtons: [
StatusBackButton {
id: backButton
objectName: "AddAccountPopup-BackButton"
visible: root.store.currentState.displayBackButton
enabled: !root.store.disablePopup
height: Constants.addAccountPopup.footerButtonsHeight
width: height
onClicked: {
if (root.store.currentState.stateType === Constants.addAccountPopup.state.confirmAddingNewMasterKey) {
root.store.addingNewMasterKeyConfirmed = false
}
else if (root.store.currentState.stateType === Constants.addAccountPopup.state.displaySeedPhrase) {
root.store.seedPhraseRevealed = false
}
else if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1) {
root.store.seedPhraseWord1Valid = false
root.store.seedPhraseWord1WordNumber = -1
root.store.seedPhraseWord2Valid = false
root.store.seedPhraseWord2WordNumber = -1
}
else if (root.store.currentState.stateType === Constants.addAccountPopup.state.confirmSeedPhraseBackup) {
root.store.seedPhraseBackupConfirmed = false
}
else if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterKeypairName) {
root.store.addAccountModule.newKeyPairName = ""
}
root.store.currentState.doBackAction()
}
}
]
rightButtons: [
StatusButton {
id: primaryButton
objectName: "AddAccountPopup-PrimaryButton"
type: root.store.currentState.stateType === Constants.addAccountPopup.state.main?
StatusBaseButton.Type.Primary :
StatusBaseButton.Type.Normal
height: Constants.addAccountPopup.footerButtonsHeight
text: {
switch (root.store.currentState.stateType) {
case Constants.addAccountPopup.state.main:
return qsTr("Add account")
case Constants.addAccountPopup.state.enterPrivateKey:
case Constants.addAccountPopup.state.enterSeedPhrase:
case Constants.addAccountPopup.state.enterSeedPhraseWord1:
case Constants.addAccountPopup.state.enterSeedPhraseWord2:
case Constants.addAccountPopup.state.confirmSeedPhraseBackup:
case Constants.addAccountPopup.state.enterKeypairName:
return qsTr("Continue")
case Constants.addAccountPopup.state.confirmAddingNewMasterKey:
return qsTr("Reveal seed phrase")
case Constants.addAccountPopup.state.displaySeedPhrase:
return qsTr("Confirm seed phrase")
}
return ""
}
visible: text !== ""
enabled: root.store.primaryPopupButtonEnabled
icon.name: {
if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterPrivateKey ||
root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhrase ||
root.store.currentState.stateType === Constants.addAccountPopup.state.confirmAddingNewMasterKey ||
root.store.currentState.stateType === Constants.addAccountPopup.state.displaySeedPhrase ||
root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1 ||
root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord2 ||
root.store.currentState.stateType === Constants.addAccountPopup.state.confirmSeedPhraseBackup ||
root.store.currentState.stateType === Constants.addAccountPopup.state.enterKeypairName ||
root.store.addAccountModule.actionAuthenticated ||
root.store.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.unknown &&
root.store.selectedOrigin.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc) {
return ""
}
if (root.store.selectedOrigin.keyUid === root.store.userProfileKeyUid &&
root.store.userProfileUsingBiometricLogin) {
return "touch-id"
}
if (root.store.selectedOrigin.migratedToKeycard || root.store.userProfileIsKeycardUser) {
return "keycard"
}
return "password"
}
onClicked: {
root.store.submitAddAccount(null)
}
}
]
}

View File

@ -0,0 +1,229 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import utils 1.0
import SortFilterProxyModel 0.2
import "../stores"
StatusMenu {
id: root
property AddAccountStore store
property int itemsPerPage: 5
signal selected(string address)
QtObject {
id: d
property int currentPage: 0
readonly property int totalPages: Math.ceil(root.store.derivedAddressModel.count / root.itemsPerPage)
}
SortFilterProxyModel {
id: proxyModel
sourceModel: root.store.derivedAddressModel
filters: ExpressionFilter {
expression: {
let lowerBound = root.itemsPerPage * d.currentPage
return model.index >= lowerBound && model.index < lowerBound + root.itemsPerPage
}
}
}
contentItem: Column {
width: root.width
Item {
anchors.left: parent.left
anchors.right: parent.right
height: Constants.addAccountPopup.itemHeight - root.topPadding
Row{
anchors.centerIn: parent
spacing: Style.current.halfPadding
StatusIcon {
visible: !root.store.addAccountModule.scanningForActivityIsOngoing ||
root.store.derivedAddressModel.loadedCount > 0
width: 20
height: 20
icon: "flash"
color: root.store.derivedAddressModel.loadedCount === 0?
Theme.palette.primaryColor1 : Theme.palette.successColor1
}
StatusLinkText {
visible: !root.store.addAccountModule.scanningForActivityIsOngoing
font.pixelSize: Constants.addAccountPopup.labelFontSize1
text: qsTr("Scan addresses for activity")
color: Theme.palette.primaryColor1
onClicked: {
root.store.startScanningForActivity()
}
}
StatusLoadingIndicator {
visible: root.store.addAccountModule.scanningForActivityIsOngoing &&
root.store.derivedAddressModel.loadedCount === 0
}
StatusBaseText {
visible: root.store.addAccountModule.scanningForActivityIsOngoing
color: root.store.derivedAddressModel.loadedCount === 0?
Theme.palette.baseColor1 : Theme.palette.successColor1
font.pixelSize: Constants.addAccountPopup.labelFontSize1
text: root.store.derivedAddressModel.loadedCount === 0?
qsTr("Scanning for activity...")
: qsTr("Activity fetched for %1 / %2 addresses").arg(root.store.derivedAddressModel.loadedCount).arg(root.store.derivedAddressModel.count)
}
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
color: Theme.palette.baseColor2
}
}
Repeater {
model: proxyModel
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
height: Constants.addAccountPopup.itemHeight
enabled: !model.addressDetails.alreadyCreated
radius: Style.current.halfPadding
color: {
if (sensor.containsMouse) {
return Theme.palette.baseColor2
}
return "transparent"
}
GridLayout {
anchors.fill: parent
anchors.leftMargin: Style.current.padding
anchors.rightMargin: Style.current.padding
columnSpacing: Style.current.padding
rowSpacing: 0
StatusBaseText {
Layout.preferredWidth: 208
color: model.addressDetails.alreadyCreated? Theme.palette.baseColor1 : Theme.palette.directColor1
font.pixelSize: Constants.addAccountPopup.labelFontSize1
text: StatusQUtils.Utils.elideText(model.addressDetails.address, 15, 4)
}
Row {
Layout.preferredWidth: 108
spacing: Style.current.halfPadding * 0.5
StatusIcon {
visible: model.addressDetails.loaded && model.addressDetails.hasActivity
width: 20
height: 20
icon: "flash"
color: Theme.palette.successColor1
}
StatusTextWithLoadingState {
width: 84
font.pixelSize: Constants.addAccountPopup.labelFontSize1
text: {
if (!root.store.addAccountModule.scanningForActivityIsOngoing) {
return ""
}
if (!model.addressDetails.loaded) {
return qsTr("loading...")
}
if (model.addressDetails.hasActivity) {
return qsTr("Has activity")
}
return qsTr("No activity")
}
color: {
if (!root.store.addAccountModule.scanningForActivityIsOngoing || !model.addressDetails.loaded) {
return "transparent"
}
if (model.addressDetails.hasActivity) {
return Theme.palette.successColor1
}
return Theme.palette.warningColor1
}
loading: root.store.addAccountModule.scanningForActivityIsOngoing && !model.addressDetails.loaded
}
}
StatusBaseText {
Layout.preferredWidth: 20
color: Theme.palette.baseColor1
font.pixelSize: Constants.addAccountPopup.labelFontSize1
text: model.addressDetails.order
}
StatusFlatRoundButton {
Layout.preferredWidth: 20
Layout.preferredHeight: 20
type: StatusFlatRoundButton.Type.Tertiary
icon.name: "external"
icon.width: 16
icon.height: 16
onClicked: {
Qt.openUrlExternally("https://etherscan.io/address/%1".arg(model.addressDetails.address))
}
}
}
MouseArea {
id: sensor
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selected(model.addressDetails.address)
}
}
}
}
Item {
anchors.left: parent.left
anchors.right: parent.right
height: Constants.addAccountPopup.itemHeight - root.bottomPadding
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: 1
color: Theme.palette.baseColor2
}
StatusPageIndicator {
anchors.top: parent.top
anchors.topMargin: (Constants.addAccountPopup.itemHeight - height) * 0.5
anchors.horizontalCenter: parent.horizontalCenter
totalPages: d.totalPages
onCurrentIndexChanged: {
d.currentPage = currentIndex
}
}
}
}
}

View File

@ -0,0 +1,60 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
Row {
id: root
property var addressDetailsItem
property bool defaultMessageCondition: true
property string defaultMessage: ""
StatusIcon {
id: icon
visible: root.addressDetailsItem &&
root.addressDetailsItem.loaded &&
root.addressDetailsItem.address !== "" &&
root.addressDetailsItem.hasActivity
width: 20
height: 20
icon: "flash"
color: Theme.palette.successColor1
}
StatusBaseText {
width: icon.visible? parent.width - icon.width : parent.width
font.pixelSize: Constants.addAccountPopup.labelFontSize2
wrapMode: Text.WordWrap
text: {
if (root.defaultMessageCondition) {
return root.defaultMessage
}
if (!root.addressDetailsItem || !root.addressDetailsItem.loaded) {
return qsTr("Scanning for activity...")
}
if (root.addressDetailsItem.alreadyCreated) {
return qsTr("Already added")
}
if (root.addressDetailsItem.hasActivity) {
return qsTr("Has activity")
}
return qsTr("No activity")
}
color: {
if (root.defaultMessageCondition || !root.addressDetailsItem || !root.addressDetailsItem.loaded) {
return Theme.palette.baseColor1
}
if (root.addressDetailsItem.alreadyCreated) {
return Theme.palette.dangerColor1
}
if (root.addressDetailsItem.hasActivity) {
return Theme.palette.successColor1
}
return Theme.palette.warningColor1
}
}
}

View File

@ -0,0 +1,174 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import utils 1.0
import "../stores"
GridLayout {
id: root
property AddAccountStore store
columns: 3
columnSpacing: Style.current.padding
rowSpacing: Style.current.halfPadding
QtObject {
id: d
readonly property int oneHalfWidth: (root.width - root.columnSpacing) * 0.5
}
StatusBaseText {
Layout.fillWidth: true
font.pixelSize: Constants.addAccountPopup.labelFontSize1
text: qsTr("Derivation Path")
}
StatusLinkText {
enabled: root.store.addAccountModule.suggestedDerivationPath !== root.store.addAccountModule.derivationPath
font.pixelSize: Constants.addAccountPopup.labelFontSize1
text: qsTr("Reset")
color: enabled? Theme.palette.primaryColor1 : Theme.palette.baseColor1
onClicked: {
root.store.resetDerivationPath()
}
}
StatusBaseText {
Layout.preferredWidth: d.oneHalfWidth
font.pixelSize: Constants.addAccountPopup.labelFontSize1
text: qsTr("Account")
}
StatusInput {
id: derivationPath
Layout.preferredWidth: d.oneHalfWidth
Layout.columnSpan: 2
text: root.store.addAccountModule.derivationPath
onTextChanged: {
let t = text
if (t.endsWith("\n")) {
t = t.replace("\n", "")
}
if(root.store.derivationPathRegEx.test(t)) {
root.store.changeDerivationPathPostponed(t)
}
else {
root.store.addAccountModule.derivationPath = t
}
}
multiline: false
input.rightComponent: StatusIcon {
icon: "chevron-down"
color: Theme.palette.baseColor1
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
derivationPathSelection.popup(derivationPath.x, derivationPath.y + derivationPath.height + Style.current.halfPadding)
}
}
}
onKeyPressed: {
root.store.submitAddAccount(event)
}
DerivationPathSelection {
id: derivationPathSelection
roots: root.store.roots
translation: root.store.translation
selectedRootPath: root.store.selectedRootPath
onSelected: {
root.store.changeRootDerivationPath(rootPath)
}
}
}
StatusListItem {
id: generatedAddress
Layout.preferredWidth: d.oneHalfWidth
Layout.preferredHeight: derivationPath.height
color: "transparent"
border.width: 1
border.color: Theme.palette.baseColor2
enabled: root.store.derivedAddressModel.count > 1
statusListItemTitle.elide: Qt.ElideMiddle
loading: root.store.derivedAddressModel.count === 0
title: {
if (!!root.store.selectedDerivedAddress && root.store.selectedDerivedAddress.address !== "") {
return root.store.selectedDerivedAddress.address
}
else if (root.store.derivedAddressModel.count > 1) {
return qsTr("Select address")
}
return "0x0000000000000000000000000000000000000000"
}
components: [
StatusIcon {
visible: root.store.derivedAddressModel.count > 1
icon: "chevron-down"
color: Theme.palette.baseColor1
}
]
onClicked: {
accountAddressSelection.popup(-generatedAddress.x, generatedAddress.y + generatedAddress.height + Style.current.halfPadding)
}
AccountAddressSelection {
id: accountAddressSelection
width: root.width
store: root.store
onSelected: {
accountAddressSelection.close()
root.store.changeSelectedDerivedAddress(address)
}
}
}
StatusBaseText {
Layout.preferredWidth: d.oneHalfWidth
Layout.columnSpan: 2
font.pixelSize: Constants.addAccountPopup.labelFontSize2
color: Theme.palette.baseColor1
text: root.store.translation(root.store.selectedRootPath, true)
}
AddressDetails {
Layout.preferredWidth: d.oneHalfWidth
addressDetailsItem: root.store.selectedDerivedAddress
defaultMessage: ""
defaultMessageCondition: !root.store.selectedDerivedAddress || root.store.selectedDerivedAddress.address === ""
}
StatusCheckBox {
visible: root.store.derivationPathOutOfTheDefaultStatusDerivationTree
Layout.fillWidth: true
Layout.columnSpan: 3
font.pixelSize: Constants.addAccountPopup.labelFontSize1
text: qsTr("I understand that this non-Ethereum derivation path is incompatible with Keycard")
onToggled: {
root.store.derivationPathOutOfTheDefaultStatusDerivationTreeConfirmed = checked
}
}
}

View File

@ -0,0 +1,123 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import utils 1.0
import "../stores"
Column {
id: root
property AddAccountStore store
padding: Style.current.padding
state: root.store.addAccountModule.actionAuthenticated? d.expandedState : d.collapsedState
QtObject {
id: d
readonly property string expandedState: "expanded"
readonly property string collapsedState: "collapsed"
}
RowLayout {
width: parent.width - 2 * root.padding
height: 64
StatusBaseText {
font.pixelSize: Constants.addAccountPopup.labelFontSize1
textFormat: Text.RichText
text: {
let t = qsTr("Derivation path")
let t1 = qsTr("(advanced)")
return `%1 <font color="${Theme.palette.baseColor1}">%2</font>`.arg(t).arg(t1)
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 1
}
StatusButton {
visible: !root.store.addAccountModule.actionAuthenticated
text: qsTr("Edit")
icon.name: {
if (root.store.selectedOrigin.keyUid === root.store.userProfileKeyUid &&
root.store.userProfileUsingBiometricLogin) {
return "touch-id"
}
if (root.store.selectedOrigin.migratedToKeycard || root.store.userProfileIsKeycardUser) {
return "keycard"
}
return "password"
}
onClicked: {
root.store.authenticateForEditingDerivationPath()
}
}
StatusIcon {
id: expandImage
visible: root.store.addAccountModule.actionAuthenticated
color: Theme.palette.baseColor1
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.state === d.expandedState) {
root.state = d.collapsedState
return
}
root.state = d.expandedState
root.state = Qt.binding(function(){ return root.store.addAccountModule.actionAuthenticated? d.expandedState : d.collapsedState })
}
}
}
}
DerivationPath {
id: derivationPathContent
width: parent.width - 2 * root.padding
store: root.store
}
states: [
State {
name: d.expandedState
PropertyChanges {target: expandImage; icon: "chevron-up"}
PropertyChanges {target: derivationPathContent; visible: true}
},
State {
name: d.collapsedState
PropertyChanges {target: expandImage; icon: "chevron-down"}
PropertyChanges {target: derivationPathContent; visible: false}
}
]
transitions: [
Transition {
from: d.collapsedState
to: d.expandedState
NumberAnimation { properties: "height"; duration: 200;}
},
Transition {
from: d.expandedState
to: d.collapsedState
NumberAnimation { properties: "height"; duration: 200;}
}
]
}

View File

@ -0,0 +1,48 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import utils 1.0
StatusMenu {
id: root
implicitWidth: 285
property string selectedRootPath
property var roots: []
property var translation: function (key, isTitle) {}
signal selected(string rootPath)
contentItem: Column {
width: root.width
Repeater {
model: root.roots.length
StatusListItem {
width: parent.width
title: root.translation(root.roots[index], true)
subTitle: root.translation(root.roots[index], false)
components: [
StatusIcon {
visible: root.selectedRootPath === root.roots[index]
icon: "checkmark"
color: Theme.palette.primaryColor1
}
]
onClicked: {
root.selected(root.roots[index])
root.close()
}
}
}
}
}

View File

@ -0,0 +1,155 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import utils 1.0
StatusSelect {
id: root
property string userProfilePublicKey
property var originModel
property var selectedOrigin
signal originSelected(string keyUid)
label: qsTr("Origin")
model: root.originModel
selectedItemComponent: StatusListItem {
title: Utils.appTranslation(root.selectedOrigin.name)
border.width: 1
border.color: Theme.palette.baseColor2
asset {
width: root.selectedOrigin.icon? 24 : 40
height: root.selectedOrigin.icon? 24 : 40
name: root.selectedOrigin.image? root.selectedOrigin.image : root.selectedOrigin.icon
isImage: !!root.selectedOrigin.image
color: root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.profile?
Utils.colorForPubkey(root.userProfilePublicKey) : Theme.palette.primaryColor1
letterSize: Math.max(4, asset.width / 2.4)
charactersLen: 2
isLetterIdenticon: !root.selectedOrigin.icon && !asset.name.toString()
bgColor: "transparent"
}
ringSettings {
ringSpecModel: root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.profile?
Utils.getColorHashAsJson(root.userProfilePublicKey) : []
ringPxSize: Math.max(asset.width / 24.0)
}
tagsModel : root.selectedOrigin.accounts
tagsDelegate: StatusListItemTag {
bgColor: model.account.color
height: Style.current.bigPadding
bgRadius: 6
tagClickable: false
closeButtonVisible: false
asset {
emoji: model.account.emoji
emojiSize: Emoji.size.verySmall
isLetterIdenticon: !!model.account.emoji
name: model.account.icon
color: Theme.palette.indirectColor1
width: 16
height: 16
}
title: model.account.name
titleText.font.pixelSize: 12
titleText.color: Theme.palette.indirectColor1
}
}
menuDelegate: StatusListItem {
id: menu
property bool isProfileKeypair: model.keyPair.pairType === Constants.addAccountPopup.keyPairType.profile
property bool isOption: model.keyPair.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey ||
model.keyPair.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc
property bool isHeader: model.keyPair.pairType === Constants.addAccountPopup.keyPairType.unknown && !menu.isOption
title: model.keyPair.pairType === Constants.addAccountPopup.keyPairType.unknown?
Utils.appTranslation(model.keyPair.keyUid) :
model.keyPair.name
subTitle: {
if (menu.isOption) {
if (model.keyPair.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey)
return qsTr("From Keycard, private key or seed phrase")
if (model.keyPair.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc)
return qsTr("Any ETH address")
}
return ""
}
enabled: !menu.isHeader && model.keyPair.pairType !== Constants.addAccountPopup.keyPairType.privateKeyImport
asset {
width: model.keyPair.icon? 24 : 40
height: model.keyPair.icon? 24 : 40
name: model.keyPair.image? model.keyPair.image : model.keyPair.icon
isImage: !!model.keyPair.image
color: menu.isProfileKeypair? Utils.colorForPubkey(root.userProfilePublicKey) : Theme.palette.baseColor1
letterSize: Math.max(4, asset.width / 2.4)
charactersLen: 2
isLetterIdenticon: !menu.isHeader && !model.keyPair.icon && !asset.name.toString()
bgColor: "transparent"
}
ringSettings {
ringSpecModel: menu.isProfileKeypair? Utils.getColorHashAsJson(root.userProfilePublicKey) : []
ringPxSize: Math.max(asset.width / 24.0)
}
tagsModel: menu.isHeader || menu.isOption? [] : model.keyPair.accounts
tagsDelegate: StatusListItemTag {
bgColor: model.account.color
height: Style.current.bigPadding
bgRadius: 6
tagClickable: false
closeButtonVisible: false
asset {
emoji: model.account.emoji
emojiSize: Emoji.size.verySmall
isLetterIdenticon: !!model.account.emoji
name: model.account.icon
color: Theme.palette.indirectColor1
width: 16
height: 16
}
title: model.account.name
titleText.font.pixelSize: 12
titleText.color: Theme.palette.indirectColor1
}
components: [
StatusIcon {
visible: icon != ""
icon: {
if (menu.isOption) {
return "tiny/chevron-right"
}
if (!menu.isHeader && model.keyPair.name === root.selectedOrigin.name) {
return "checkmark"
}
return ""
}
color: menu.isOption? Theme.palette.baseColor1 : Theme.palette.primaryColor1
}
]
onClicked: {
root.originSelected(model.keyPair.keyUid)
root.selectMenu.close()
}
}
}

View File

@ -0,0 +1,65 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import utils 1.0
import "../stores"
Column {
id: root
property AddAccountStore store
function reset() {
addressInput.reset()
}
StatusInput {
id: addressInput
width: parent.width
maximumHeight: Constants.addAccountPopup.itemHeight
minimumHeight: Constants.addAccountPopup.itemHeight
label: qsTr("Ethereum address or ENS name")
placeholderText: "e.g.0x95222293DD7278Aa3Cdd389Cc1D1d165CCBAfe5"
input.multiline: true
input.rightComponent: StatusButton {
anchors.verticalCenter: parent.verticalCenter
borderColor: Theme.palette.primaryColor1
size: StatusBaseButton.Size.Tiny
text: qsTr("Paste")
onClicked: {
addressInput.text = ""
addressInput.input.edit.paste()
}
}
validators: [
StatusAddressOrEnsValidator {
errorMessage: qsTr("Please enter a valid Ethereum address or ENS name")
}
]
onTextChanged: {
if (addressInput.valid) {
root.store.changeWatchOnlyAccountAddressPostponed(text.trim())
return
}
root.store.cleanWatchOnlyAccountAddress()
}
onKeyPressed: {
root.store.submitAddAccount(event)
}
}
AddressDetails {
width: parent.width
addressDetailsItem: root.store.watchOnlyAccAddress
defaultMessage: qsTr("You will need to import your seed phrase or use your Keycard to transact with this account")
defaultMessageCondition: addressInput.text === "" || !addressInput.valid
}
}

View File

@ -0,0 +1,129 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
import utils 1.0
import "../stores"
Item {
id: root
property AddAccountStore store
implicitHeight: layout.implicitHeight
Component.onCompleted: {
if (root.store.addingNewMasterKeyConfirmed) {
havePen.checked = true
writeDown.checked = true
storeIt.checked = true
}
}
QtObject {
id: d
readonly property int width1: layout.width - 2 * Style.current.padding
readonly property int width2: d.width1 - 2 * Style.current.padding
readonly property int checkboxHeight: 24
readonly property real lineHeight: 1.2
readonly property bool allAccepted: havePen.checked && writeDown.checked && storeIt.checked
onAllAcceptedChanged: {
root.store.addingNewMasterKeyConfirmed = allAccepted
}
}
ColumnLayout {
id: layout
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 2 * Style.current.padding
spacing: Style.current.padding
Image {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.current.padding
Layout.preferredWidth: 120
Layout.preferredHeight: 120
fillMode: Image.PreserveAspectFit
source: Style.png("onboarding/keys")
mipmap: true
}
StatusBaseText {
Layout.preferredWidth: d.width1
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
font.bold: true
font.pixelSize: 22
text: qsTr("Secure Your Assets and Funds")
}
StatusBaseText {
Layout.preferredWidth: d.width1
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
textFormat: Text.RichText
font.pixelSize: Style.current.primaryTextFontSize
lineHeight: d.lineHeight
text: qsTr("Your seed phrase is a 12-word passcode to your funds.<br/><br/>Your seed phrase cannot be recovered if lost. Therefore, you <b>must</b> back it up. The simplest way is to <b>write it down offline and store it somewhere secure.</b>")
}
StatusCheckBox {
id: havePen
Layout.preferredWidth: d.width2
Layout.preferredHeight: d.checkboxHeight
Layout.topMargin: Style.current.padding
Layout.alignment: Qt.AlignHCenter
spacing: Style.current.padding
font.pixelSize: Style.current.primaryTextFontSize
text: qsTr("I have a pen and paper")
}
StatusCheckBox {
id: writeDown
Layout.preferredWidth: d.width2
Layout.preferredHeight: d.checkboxHeight
Layout.alignment: Qt.AlignHCenter
spacing: Style.current.padding
font.pixelSize: Style.current.primaryTextFontSize
text: qsTr("I am ready to write down my seed phrase")
}
StatusCheckBox {
id: storeIt
Layout.preferredWidth: d.width2
Layout.preferredHeight: d.checkboxHeight
Layout.alignment: Qt.AlignHCenter
spacing: Style.current.padding
font.pixelSize: Style.current.primaryTextFontSize
text: qsTr("I know where Ill store it")
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 60
Layout.topMargin: Style.current.padding
radius: Style.current.radius
color: Theme.palette.dangerColor3
StatusBaseText {
anchors.fill: parent
anchors.margins: Style.current.halfPadding
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: Style.current.primaryTextFontSize
wrapMode: Text.WordWrap
color: Theme.palette.dangerColor1
lineHeight: d.lineHeight
text: qsTr("You can only complete this process once. Status will not store your seed phrase and can never help you recover it.")
}
}
}
}

View File

@ -0,0 +1,92 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import utils 1.0
import "../stores"
Item {
id: root
property AddAccountStore store
implicitHeight: layout.implicitHeight
Component.onCompleted: {
if (root.store.seedPhraseBackupConfirmed) {
aknowledge.checked = true
}
}
ColumnLayout {
id: layout
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 2 * 3 * Style.current.padding
spacing: Style.current.halfPadding
StatusStepper {
Layout.preferredWidth: Constants.addAccountPopup.stepperWidth
Layout.preferredHeight: Constants.addAccountPopup.stepperHeight
Layout.topMargin: Style.current.padding
Layout.alignment: Qt.AlignCenter
title: qsTr("Step 4 of 4")
titleFontSize: Constants.addAccountPopup.labelFontSize1
totalSteps: 4
completedSteps: 4
leftPadding: 0
rightPadding: 0
}
StatusBaseText {
Layout.preferredWidth: parent.width
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Constants.addAccountPopup.labelFontSize1
color: Theme.palette.directColor1
text: qsTr("Complete back up")
}
StatusBaseText {
Layout.preferredWidth: parent.width
Layout.topMargin: 2 * Style.current.xlPadding
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
font.bold: true
font.pixelSize: 18
color: Theme.palette.directColor1
text: qsTr("Store Your Phrase Offline and Complete Your Back Up")
}
StatusBaseText {
Layout.preferredWidth: parent.width
Layout.topMargin: Style.current.halfPadding
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
font.pixelSize: Style.current.primaryTextFontSize
lineHeight: 1.2
color: Theme.palette.directColor1
text: qsTr("By completing this process, you will remove your seed phrase from this applications storage. This makes your funds more secure.\n\nYou will remain logged in, and your seed phrase will be entirely in your hands.")
}
StatusCheckBox {
id: aknowledge
Layout.preferredWidth: parent.width
Layout.topMargin: 2 * Style.current.xlPadding
Layout.alignment: Qt.AlignHCenter
spacing: Style.current.padding
font.pixelSize: Style.current.primaryTextFontSize
text: qsTr("I aknowledge that Status will not be able to show me my seed phrase again.")
onToggled: {
root.store.seedPhraseBackupConfirmed = checked
}
}
}
}

View File

@ -0,0 +1,75 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import utils 1.0
import shared.panels 1.0
import "../stores"
Item {
id: root
property AddAccountStore store
implicitHeight: layout.implicitHeight
ColumnLayout {
id: layout
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 2 * Style.current.padding
spacing: Style.current.halfPadding
StatusStepper {
Layout.preferredWidth: Constants.addAccountPopup.stepperWidth
Layout.preferredHeight: Constants.addAccountPopup.stepperHeight
Layout.topMargin: Style.current.padding
Layout.alignment: Qt.AlignCenter
title: qsTr("Step 1 of 4")
titleFontSize: Constants.addAccountPopup.labelFontSize1
totalSteps: 4
completedSteps: 1
leftPadding: 0
rightPadding: 0
}
StatusBaseText {
Layout.preferredWidth: parent.width
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Constants.addAccountPopup.labelFontSize1
color: Theme.palette.directColor1
text: qsTr("Write down your 12-word seed phrase to keep offline")
}
SeedPhrase {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 304
Layout.topMargin: 3 * Style.current.padding
seedPhraseRevealed: root.store.seedPhraseRevealed
seedPhrase: root.store.getSeedPhrase().split(" ")
onSeedPhraseRevealedChanged: {
root.store.seedPhraseRevealed = seedPhraseRevealed
}
}
StatusBaseText {
Layout.preferredWidth: parent.width
Layout.topMargin: 2 * Style.current.padding
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
visible: !root.store.seedPhraseRevealed
font.pixelSize: Constants.addAccountPopup.labelFontSize1
textFormat: Text.RichText
wrapMode: Text.WordWrap
color: Theme.palette.dangerColor1
text: qsTr("The next screen contains your seed phrase.<br/><b>Anyone</b> who sees it can use it to access to your funds.")
}
}
}

View File

@ -0,0 +1,53 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import utils 1.0
import "../stores"
Item {
id: root
property AddAccountStore store
implicitHeight: layout.implicitHeight
ColumnLayout {
id: layout
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 2 * Style.current.padding
spacing: Style.current.halfPadding
StatusInput {
Layout.preferredWidth: parent.width
Layout.topMargin: Style.current.padding
label: qsTr("Key name")
charLimit: Constants.addAccountPopup.keyPairNameMaxLength
placeholderText: qsTr("Enter a name")
text: root.store.addAccountModule.newKeyPairName
onTextChanged: {
if (text.trim() == "") {
root.store.addAccountModule.newKeyPairName = ""
return
}
root.store.addAccountModule.newKeyPairName = text
}
onKeyPressed: {
root.store.submitAddAccount(event)
}
}
StatusBaseText {
text: qsTr("For your future reference. This is only visible to you.")
font.pixelSize: Constants.addAccountPopup.labelFontSize2
color: Theme.palette.baseColor1
}
}
}

View File

@ -0,0 +1,171 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import utils 1.0
import "../stores"
import "../panels"
Item {
id: root
property AddAccountStore store
QtObject {
id: d
property bool showPassword: false
property bool addressResolved: root.store.privateKeyAccAddress.address !== ""
}
Column {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Style.current.padding
spacing: Style.current.padding
Column {
width: parent.width
spacing: Style.current.halfPadding
StatusBaseText {
text: qsTr("Private key")
font.pixelSize: Constants.addAccountPopup.labelFontSize1
}
GridLayout {
width: parent.width
columns: 2
columnSpacing: Style.current.padding
rowSpacing: Style.current.halfPadding
StatusPasswordInput {
id: privKeyInput
Layout.preferredHeight: Constants.addAccountPopup.itemHeight
Layout.preferredWidth: parent.width - parent.columnSpacing - showHideButton.width
rightPadding: pasteButton.width + pasteButton.anchors.rightMargin + Style.current.halfPadding
placeholderText: qsTr("Type or paste your private key")
echoMode: d.showPassword ? TextInput.Normal : TextInput.Password
onTextChanged: {
root.store.enteredPrivateKeyIsValid = Utils.isPrivateKey(text)
if (root.store.enteredPrivateKeyIsValid) {
root.store.changePrivateKeyPostponed(text)
return
}
root.store.cleanPrivateKey()
}
onPressed: {
root.store.submitAddAccount(event)
}
StatusButton {
id: pasteButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
borderColor: Theme.palette.primaryColor1
size: StatusBaseButton.Size.Tiny
text: qsTr("Paste")
onClicked: {
privKeyInput.text = root.store.getFromClipboard()
}
}
}
StatusFlatRoundButton {
id: showHideButton
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 24
Layout.preferredHeight: 24
icon.name: d.showPassword ? "hide" : "show"
icon.color: Theme.palette.baseColor1
onClicked: d.showPassword = !d.showPassword
}
StatusBaseText {
Layout.alignment: Qt.AlignRight
visible: privKeyInput.text !== "" && !root.store.enteredPrivateKeyIsValid
wrapMode: Text.WordWrap
font.pixelSize: 12
color: Theme.palette.dangerColor1
text: qsTr("Private key invalid")
}
}
}
Column {
width: parent.width
spacing: Style.current.halfPadding
visible: d.addressResolved
StatusBaseText {
text: qsTr("Public address of private key")
font.pixelSize: Constants.addAccountPopup.labelFontSize1
}
StatusInput {
width: parent.width
input.edit.enabled: false
text: root.store.privateKeyAccAddress.address
input.enabled: false
input.background.color: "transparent"
input.background.border.color: Theme.palette.baseColor2
}
AddressDetails {
width: parent.width
addressDetailsItem: root.store.privateKeyAccAddress
defaultMessage: ""
defaultMessageCondition: !d.addressResolved
}
}
StatusModalDivider {
width: parent.width
visible: d.addressResolved
}
Column {
width: parent.width
spacing: Style.current.halfPadding
visible: d.addressResolved
StatusInput {
width: parent.width
label: qsTr("Key name")
charLimit: Constants.addAccountPopup.keyPairNameMaxLength
placeholderText: qsTr("Enter a name")
text: root.store.addAccountModule.newKeyPairName
onTextChanged: {
if (text.trim() == "") {
root.store.addAccountModule.newKeyPairName = ""
return
}
root.store.addAccountModule.newKeyPairName = text
}
onKeyPressed: {
root.store.submitAddAccount(event)
}
}
StatusBaseText {
text: qsTr("For your future reference. This is only visible to you.")
font.pixelSize: Constants.addAccountPopup.labelFontSize2
color: Theme.palette.baseColor1
}
}
}
}

View File

@ -0,0 +1,96 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Components 0.1
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
Column {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Style.current.padding
spacing: Style.current.padding
StatusBaseText {
text: qsTr("Enter seed phrase")
font.pixelSize: Constants.addAccountPopup.labelFontSize1
}
SharedPanels.EnterSeedPhrase {
id: enterSeedPhrase
width: parent.width
isSeedPhraseValid: function(mnemonic) {
return root.store.validSeedPhrase(mnemonic)
}
onSeedPhraseUpdated: {
if (valid) {
root.store.changeSeedPhrase(seedPhrase)
}
root.store.enteredSeedPhraseIsValid = valid
if (!enterSeedPhrase.isSeedPhraseValid(seedPhrase)) {
enterSeedPhrase.setWrongSeedPhraseMessage(qsTr("The entered seed phrase is already added"))
}
}
onSubmitSeedPhrase: {
root.store.submitAddAccount()
}
}
StatusModalDivider {
width: parent.width
visible: root.store.enteredSeedPhraseIsValid
}
Column {
width: parent.width
spacing: Style.current.halfPadding
visible: root.store.enteredSeedPhraseIsValid
StatusInput {
width: parent.width
label: qsTr("Key name")
charLimit: Constants.addAccountPopup.keyPairNameMaxLength
placeholderText: qsTr("Enter a name")
text: root.store.addAccountModule.newKeyPairName
onTextChanged: {
if (text.trim() == "") {
root.store.addAccountModule.newKeyPairName = ""
return
}
root.store.addAccountModule.newKeyPairName = text
}
onKeyPressed: {
root.store.submitAddAccount(event)
}
}
StatusBaseText {
text: qsTr("For your future reference. This is only visible to you.")
font.pixelSize: Constants.addAccountPopup.labelFontSize2
color: Theme.palette.baseColor1
}
}
}
}

View File

@ -0,0 +1,157 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Components 0.1
import utils 1.0
import "../stores"
Item {
id: root
property AddAccountStore store
implicitHeight: layout.implicitHeight
state: root.store.currentState.stateType
Component.onCompleted: {
if (root.store.seedPhraseWord1WordNumber === -1) {
let randomNo1 = Math.floor(Math.random() * 12)
let randomNo2 = randomNo1
while (randomNo2 == randomNo1) {
randomNo2 = Math.floor(Math.random() * 12)
}
if (randomNo1 < randomNo2) {
root.store.seedPhraseWord1WordNumber = randomNo1
root.store.seedPhraseWord2WordNumber = randomNo2
}
else {
root.store.seedPhraseWord1WordNumber = randomNo2
root.store.seedPhraseWord2WordNumber = randomNo1
}
}
}
onStateChanged: {
d.updateEntry()
}
QtObject {
id: d
readonly property int step: root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1? 2 : 3
readonly property var seedPhrase: root.store.getSeedPhrase().split(" ")
function updateEntry() {
if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1) {
if (!root.store.seedPhraseWord1Valid) {
if (word.text.trim() !== "") {
word.reset()
}
}
else if (root.store.seedPhraseWord1WordNumber >= -1 && root.store.seedPhraseWord1WordNumber < d.seedPhrase.length) {
word.text = d.seedPhrase[root.store.seedPhraseWord1WordNumber]
}
}
else {
if (!root.store.seedPhraseWord2Valid) {
if (word.text.trim() !== "") {
word.reset()
}
}
else if (root.store.seedPhraseWord2WordNumber >= -1 && root.store.seedPhraseWord2WordNumber < d.seedPhrase.length) {
word.text = d.seedPhrase[root.store.seedPhraseWord2WordNumber]
}
}
word.validate()
}
function processText(text) {
if(text.length === 0)
return ""
if(/(^\s|^\r|^\n)|(\s$|^\r$|^\n$)/.test(text)) {
return text.trim()
}
else if(/\s|\r|\n/.test(text)) {
return ""
}
return text
}
}
ColumnLayout {
id: layout
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 2 * Style.current.padding
spacing: Style.current.halfPadding
StatusStepper {
Layout.preferredWidth: Constants.addAccountPopup.stepperWidth
Layout.preferredHeight: Constants.addAccountPopup.stepperHeight
Layout.topMargin: Style.current.padding
Layout.alignment: Qt.AlignCenter
title: qsTr("Step %1 of 4").arg(d.step)
titleFontSize: Constants.addAccountPopup.labelFontSize1
totalSteps: 4
completedSteps: d.step
leftPadding: 0
rightPadding: 0
}
StatusBaseText {
Layout.preferredWidth: parent.width
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Constants.addAccountPopup.labelFontSize1
color: Theme.palette.directColor1
text: qsTr("Confirm word #%1 of your seed phrase").arg(root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1?
root.store.seedPhraseWord1WordNumber + 1 :
root.store.seedPhraseWord2WordNumber + 1)
}
StatusInput {
id: word
Layout.fillWidth: true
Layout.topMargin: Style.current.xlPadding
validationMode: StatusInput.ValidationMode.Always
label: qsTr("Word #%1").arg(root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1?
root.store.seedPhraseWord1WordNumber + 1 :
root.store.seedPhraseWord2WordNumber + 1)
placeholderText: qsTr("Enter word")
validators: [
StatusValidator {
validate: function (t) {
if (!d.seedPhrase || d.seedPhrase.length === 0 || word.text.length === 0)
return false
if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1) {
return (d.seedPhrase[root.store.seedPhraseWord1WordNumber] === word.text)
}
return (d.seedPhrase[root.store.seedPhraseWord2WordNumber] === word.text)
}
errorMessage: (word.text.length) > 0 ? qsTr("Incorrect word") : ""
}
]
input.tabNavItem: word.input.edit
onTextChanged: {
text = d.processText(text)
if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1) {
root.store.seedPhraseWord1Valid = word.valid
return
}
root.store.seedPhraseWord2Valid = word.valid
}
onKeyPressed: {
root.store.submitAddAccount(event)
}
}
}
}

View File

@ -0,0 +1,181 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import utils 1.0
import "../stores"
import "../panels"
Item {
id: root
property AddAccountStore store
implicitHeight: layout.implicitHeight
Component.onCompleted: {
if (root.store.addAccountModule.selectedColor === "") {
colorSelection.selectedColorIndex = Math.floor(Math.random() * colorSelection.model.length)
}
else {
let ind = d.evaluateColorIndex(root.store.addAccountModule.selectedColor)
colorSelection.selectedColorIndex = ind
}
if (root.store.addAccountModule.selectedEmoji === "") {
root.store.addAccountModule.selectedEmoji = StatusQUtils.Emoji.getRandomEmoji(StatusQUtils.Emoji.size.verySmall)
}
accountName.text = root.store.addAccountModule.accountName
accountName.input.edit.forceActiveFocus()
}
QtObject {
id: d
function evaluateColorIndex(color) {
for (let i = 0; i < Constants.preDefinedWalletAccountColors.length; i++) {
if(Constants.preDefinedWalletAccountColors[i] === color) {
return i
}
}
return 0
}
}
Connections {
target: root.store.emojiPopup
function onEmojiSelected (emojiText, atCursor) {
root.store.addAccountModule.selectedEmoji = emojiText
}
}
Component {
id: spacer
Rectangle {
color: Theme.palette.baseColor4
}
}
ColumnLayout {
id: layout
width: parent.width
spacing: 0
Loader {
Layout.preferredHeight: Style.current.padding
Layout.fillWidth: true
sourceComponent: spacer
}
Column {
Layout.fillWidth: true
spacing: Style.current.padding
topPadding: Style.current.padding
bottomPadding: Style.current.padding
StatusInput {
id: accountName
anchors.horizontalCenter: parent.horizontalCenter
placeholderText: qsTr("Enter an account name...")
label: qsTr("Name")
text: root.store.addAccountModule.accountName
input.isIconSelectable: true
input.leftPadding: Style.current.padding
input.asset.color: root.store.addAccountModule.selectedColor
input.asset.emoji: root.store.addAccountModule.selectedEmoji
onIconClicked: {
if (!root.store.emojiPopup) {
return
}
let inputCoords = accountName.mapToItem(appMain, 0, 0)
root.store.emojiPopup.open()
root.store.emojiPopup.emojiSize = StatusQUtils.Emoji.size.verySmall
root.store.emojiPopup.x = inputCoords.x
root.store.emojiPopup.y = inputCoords.y + accountName.height + Style.current.halfPadding
}
onTextChanged: {
root.store.addAccountModule.accountName = text
}
onKeyPressed: {
root.store.submitAddAccount(event)
}
}
StatusColorSelectorGrid {
id: colorSelection
anchors.horizontalCenter: parent.horizontalCenter
model: Constants.preDefinedWalletAccountColors
title.color: Theme.palette.directColor1
title.font.pixelSize: Constants.addAccountPopup.labelFontSize1
title.text: qsTr("Colour")
selectedColorIndex: -1
onSelectedColorChanged: {
root.store.addAccountModule.selectedColor = selectedColor
}
}
SelectOrigin {
anchors.horizontalCenter: parent.horizontalCenter
userProfilePublicKey: root.store.userProfilePublicKey
originModel: root.store.originModel
selectedOrigin: root.store.selectedOrigin
onOriginSelected: {
if (keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey) {
root.store.currentState.doSecondaryAction()
return
}
root.store.changeSelectedOrigin(keyUid)
}
}
WatchOnlyAddressSection {
width: parent.width - 2 * Style.current.padding
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.current.padding
visible: root.store.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.unknown &&
root.store.selectedOrigin.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc
store: root.store
onVisibleChanged: {
reset()
}
}
}
Loader {
Layout.preferredHeight: Style.current.padding
Layout.fillWidth: true
sourceComponent: spacer
}
DerivationPathSection {
id: derivationPathSection
Layout.fillWidth: true
visible: root.store.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.profile ||
root.store.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.seedImport
store: root.store
}
Loader {
Layout.preferredHeight: Style.current.padding
Layout.fillWidth: true
visible: derivationPathSection.visible
sourceComponent: spacer
}
}
}

View File

@ -0,0 +1,114 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import utils 1.0
import "../stores"
Item {
id: root
property AddAccountStore store
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Style.current.padding
StatusListItem {
title: qsTr("Add new master key")
enabled: false
}
StatusListItem {
title: qsTr("Import using seed phrase")
asset {
name: "key_pair_seed_phrase"
color: Theme.palette.primaryColor1
bgColor: Theme.palette.primaryColor3
}
components: [
StatusIcon {
icon: "tiny/chevron-right"
color: Theme.palette.baseColor1
}
]
onClicked: {
root.store.cleanSeedPhrase()
root.store.currentState.doPrimaryAction()
}
}
StatusListItem {
title: qsTr("Import private key")
asset {
name: "objects"
color: Theme.palette.primaryColor1
bgColor: Theme.palette.primaryColor3
}
components: [
StatusIcon {
icon: "tiny/chevron-right"
color: Theme.palette.baseColor1
}
]
onClicked: {
root.store.cleanPrivateKey()
root.store.currentState.doSecondaryAction()
}
}
StatusListItem {
title: qsTr("Generate new master key")
asset {
name: "objects"
color: Theme.palette.primaryColor1
bgColor: Theme.palette.primaryColor3
}
components: [
StatusIcon {
icon: "tiny/chevron-right"
color: Theme.palette.baseColor1
}
]
onClicked: {
root.store.resetStoreValues()
root.store.currentState.doTertiaryAction()
}
}
StatusModalDivider {
width: parent.width
}
StatusListItem {
title: qsTr("Use Keycard")
sensor.enabled: false
sensor.hoverEnabled: false
statusListItemIcon.enabled: false
statusListItemIcon.hoverEnabled: false
asset {
name: "keycard"
color: Theme.palette.primaryColor1
bgColor: Theme.palette.primaryColor3
}
components: [
StatusButton {
text: qsTr("Continue in Keycard settings")
onClicked: {
Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.keycard)
}
}
]
}
}
}

View File

@ -0,0 +1,308 @@
import QtQuick 2.13
import utils 1.0
QtObject {
id: root
property var addAccountModule
property var emojiPopup: null
property string userProfilePublicKey: userProfile.pubKey
property string userProfileKeyUid: userProfile.keyUid
property bool userProfileIsKeycardUser: userProfile.isKeycardUser
property bool userProfileUsingBiometricLogin: userProfile.usingBiometricLogin
// Module Properties
property var currentState: root.addAccountModule? root.addAccountModule.currentState : null
property var originModel: root.addAccountModule? root.addAccountModule.originModel : []
property var selectedOrigin: root.addAccountModule? root.addAccountModule.selectedOrigin : null
property var derivedAddressModel: root.addAccountModule? root.addAccountModule.derivedAddressModel : []
property var selectedDerivedAddress: root.addAccountModule? root.addAccountModule.selectedDerivedAddress : null
property var watchOnlyAccAddress: root.addAccountModule? root.addAccountModule.watchOnlyAccAddress : null
property var privateKeyAccAddress: root.addAccountModule? root.addAccountModule.privateKeyAccAddress : null
property bool disablePopup: root.addAccountModule? root.addAccountModule.disablePopup : false
property bool enteredSeedPhraseIsValid: false
property bool enteredPrivateKeyIsValid: false
property bool addingNewMasterKeyConfirmed: false
property bool seedPhraseRevealed: false
property bool seedPhraseWord1Valid: false
property int seedPhraseWord1WordNumber: -1
property bool seedPhraseWord2Valid: false
property int seedPhraseWord2WordNumber: -1
property bool seedPhraseBackupConfirmed: false
property bool derivationPathOutOfTheDefaultStatusDerivationTreeConfirmed: false
property bool derivationPathOutOfTheDefaultStatusDerivationTree: root.addAccountModule?
!root.addAccountModule.derivationPath.startsWith(Constants.addAccountPopup.predefinedPaths.ethereum) ||
(root.addAccountModule.derivationPath.match(/'/g) || []).length !== 3 ||
(root.addAccountModule.derivationPath.match(/\//g) || []).length !== 5
: false
readonly property var derivationPathRegEx: /^(m\/44'\/)([0-9|'|\/](?!\/'))*$/
property string selectedRootPath: Constants.addAccountPopup.predefinedPaths.ethereum
readonly property var roots: [Constants.addAccountPopup.predefinedPaths.custom,
Constants.addAccountPopup.predefinedPaths.ethereum,
Constants.addAccountPopup.predefinedPaths.ethereumRopsten,
Constants.addAccountPopup.predefinedPaths.ethereumLedger,
Constants.addAccountPopup.predefinedPaths.ethereumLedgerLive
]
function resetStoreValues() {
root.enteredSeedPhraseIsValid = false
root.enteredPrivateKeyIsValid = false
root.addingNewMasterKeyConfirmed = false
root.seedPhraseRevealed = false
root.seedPhraseWord1Valid = false
root.seedPhraseWord1WordNumber = -1
root.seedPhraseWord2Valid = false
root.seedPhraseWord2WordNumber = -1
root.seedPhraseBackupConfirmed = false
root.derivationPathOutOfTheDefaultStatusDerivationTreeConfirmed = false
root.selectedRootPath = Constants.addAccountPopup.predefinedPaths.ethereum
root.cleanPrivateKey()
root.cleanSeedPhrase()
}
function moduleInitialized() {
if (!root.addAccountModule) {
console.warn("addAccountModule not initialized")
return false
}
return true
}
function submitAddAccount(event) {
if (!root.moduleInitialized() || !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()
}
}
function getSeedPhrase() {
if (!root.moduleInitialized()) {
return
}
return root.addAccountModule.getSeedPhrase()
}
function changeSelectedOrigin(keyUid) {
if (!root.moduleInitialized()) {
return
}
root.addAccountModule.changeSelectedOrigin(keyUid)
}
readonly property var changeDerivationPathPostponed: Backpressure.debounce(root, 400, function (path) {
if (!root.moduleInitialized()) {
return
}
root.changeDerivationPath(path)
})
readonly property var changeWatchOnlyAccountAddressPostponed: Backpressure.debounce(root, 400, function (address) {
if (!root.moduleInitialized()) {
return
}
root.addAccountModule.changeWatchOnlyAccountAddress(address)
})
function cleanWatchOnlyAccountAddress() {
if (!root.moduleInitialized()) {
return
}
root.addAccountModule.changeWatchOnlyAccountAddress("")
}
readonly property var changePrivateKeyPostponed: Backpressure.debounce(root, 400, function (privateKey) {
if (!root.moduleInitialized()) {
return
}
root.addAccountModule.changePrivateKey(privateKey)
})
function cleanPrivateKey() {
if (!root.moduleInitialized()) {
return
}
root.enteredPrivateKeyIsValid = false
root.addAccountModule.newKeyPairName = ""
root.addAccountModule.changePrivateKey("")
}
function changeDerivationPath(path) {
if (!root.moduleInitialized()) {
return
}
root.addAccountModule.changeDerivationPath(path)
}
function changeRootDerivationPath(rootPath) {
if (!root.moduleInitialized()) {
return
}
root.selectedRootPath = rootPath
root.addAccountModule.derivationPath = "%1/".arg(rootPath)
}
function changeSelectedDerivedAddress(address) {
if (!root.moduleInitialized()) {
return
}
root.addAccountModule.changeSelectedDerivedAddress(address)
}
function resetDerivationPath() {
if (!root.moduleInitialized()) {
return
}
root.selectedRootPath = Constants.addAccountPopup.predefinedPaths.ethereum
root.addAccountModule.resetDerivationPath()
}
function authenticateForEditingDerivationPath() {
if (!root.moduleInitialized()) {
return
}
root.addAccountModule.authenticateForEditingDerivationPath()
}
function startScanningForActivity() {
if (!root.moduleInitialized()) {
return
}
root.addAccountModule.startScanningForActivity()
}
function validSeedPhrase(seedPhrase) {
if (!root.moduleInitialized()) {
return
}
return root.addAccountModule.validSeedPhrase(seedPhrase)
}
function changeSeedPhrase(seedPhrase) {
if (!root.moduleInitialized()) {
return
}
root.addAccountModule.changeSeedPhrase(seedPhrase)
}
function cleanSeedPhrase() {
if (!root.moduleInitialized()) {
return
}
root.enteredSeedPhraseIsValid = false
root.addAccountModule.newKeyPairName = ""
root.changeSeedPhrase("")
}
function translation(key, isTitle) {
if (!isTitle) {
if (key === Constants.addAccountPopup.predefinedPaths.custom)
return qsTr("Type your own derivation path")
return key
}
switch(key) {
case Constants.addAccountPopup.predefinedPaths.custom:
return qsTr("Custom")
case Constants.addAccountPopup.predefinedPaths.ethereum:
return qsTr("Ethereum")
case Constants.addAccountPopup.predefinedPaths.ethereumRopsten:
return qsTr("Ethereum Testnet (Ropsten)")
case Constants.addAccountPopup.predefinedPaths.ethereumLedger:
return qsTr("Ethereum (Ledger)")
case Constants.addAccountPopup.predefinedPaths.ethereumLedgerLive:
return qsTr("Ethereum (Ledger Live/KeepKey)")
}
}
function getFromClipboard() {
return globalUtils.getFromClipboard()
}
readonly property bool primaryPopupButtonEnabled: {
if (!root.addAccountModule || !root.currentState || root.disablePopup) {
return false
}
let valid = root.addAccountModule.accountName !== "" &&
root.addAccountModule.selectedColor !== "" &&
root.addAccountModule.selectedEmoji !== ""
if (root.currentState.stateType === Constants.addAccountPopup.state.main) {
if (root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.profile ||
root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.seedImport) {
return valid &&
root.derivationPathRegEx.test(root.addAccountModule.derivationPath) &&
(!root.derivationPathOutOfTheDefaultStatusDerivationTree ||
root.derivationPathOutOfTheDefaultStatusDerivationTreeConfirmed)
}
if (root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.unknown &&
root.selectedOrigin.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc) {
return valid &&
!!root.watchOnlyAccAddress &&
root.watchOnlyAccAddress.loaded &&
!root.watchOnlyAccAddress.alreadyCreated &&
root.watchOnlyAccAddress.address !== ""
}
if (root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.privateKeyImport) {
return valid &&
root.enteredPrivateKeyIsValid &&
!!root.privateKeyAccAddress &&
root.privateKeyAccAddress.loaded &&
!root.privateKeyAccAddress.alreadyCreated &&
root.privateKeyAccAddress.address !== "" &&
root.addAccountModule.newKeyPairName !== ""
}
}
if (root.currentState.stateType === Constants.addAccountPopup.state.enterPrivateKey) {
return root.enteredPrivateKeyIsValid &&
!!root.privateKeyAccAddress &&
root.privateKeyAccAddress.loaded &&
!root.privateKeyAccAddress.alreadyCreated &&
root.privateKeyAccAddress.address !== "" &&
root.addAccountModule.newKeyPairName !== ""
}
if (root.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhrase) {
return root.enteredSeedPhraseIsValid &&
root.addAccountModule.newKeyPairName !== ""
}
if (root.currentState.stateType === Constants.addAccountPopup.state.confirmAddingNewMasterKey) {
return root.addingNewMasterKeyConfirmed
}
if (root.currentState.stateType === Constants.addAccountPopup.state.displaySeedPhrase) {
return root.seedPhraseRevealed
}
if (root.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1) {
return root.seedPhraseWord1Valid
}
if (root.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord2) {
return root.seedPhraseWord2Valid
}
if (root.currentState.stateType === Constants.addAccountPopup.state.confirmSeedPhraseBackup) {
return root.seedPhraseBackupConfirmed
}
if (root.currentState.stateType === Constants.addAccountPopup.state.enterKeypairName) {
return root.addAccountModule.newKeyPairName !== ""
}
return true
}
}

View File

@ -6,6 +6,7 @@ import utils 1.0
import SortFilterProxyModel 0.2
import StatusQ.Core.Theme 0.1
import "../addaccount/stores"
QtObject {
id: root
@ -187,4 +188,11 @@ QtObject {
function copyToClipboard(text) {
globalUtils.copyToClipboard(text)
}
property AddAccountStore addAccountStore: AddAccountStore {
}
function runAddAccountPopup() {
walletSection.runAddAccountPopup()
}
}

View File

@ -19,6 +19,7 @@ import shared.stores 1.0
import "../controls"
import "../popups"
import "../stores"
import "../addaccount"
Rectangle {
id: root
@ -34,6 +35,34 @@ Rectangle {
color: Style.current.secondaryMenuBackground
Loader {
id: addAccount
active: false
asynchronous: true
sourceComponent: AddAccountPopup {
store: RootStore.addAccountStore
anchors.centerIn: parent
}
onLoaded: {
addAccount.item.open()
}
}
Connections {
target: walletSection
function onDisplayAddAccountPopup() {
RootStore.addAccountStore.emojiPopup = root.emojiPopup
RootStore.addAccountStore.addAccountModule = walletSection.addAccountModule
addAccount.active = true
}
function onDestroyAddAccountPopup() {
addAccount.active = false
}
}
ColumnLayout {
anchors.fill: parent
spacing: Style.current.padding
@ -63,6 +92,7 @@ Rectangle {
height: parent.height * 2
color: hovered || highlighted ? Theme.palette.primaryColor3
: "transparent"
onClicked: RootStore.runAddAccountPopup()
}
}

View File

@ -0,0 +1,294 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import utils 1.0
import shared.stores 1.0
import shared.controls 1.0
ColumnLayout {
id: root
//**************************************************************************
//* This component is not refactored, just pulled out to a shared location *
//**************************************************************************
spacing: Style.current.padding
clip: true
property var isSeedPhraseValid: function (mnemonic) { return false }
signal submitSeedPhrase()
signal seedPhraseUpdated(bool valid, string seedPhrase)
function setWrongSeedPhraseMessage(message) {
invalidSeedTxt.text = message
}
QtObject {
id: d
property bool allEntriesValid: false
property var mnemonicInput: []
readonly property var tabs: [12, 18, 24]
readonly property ListModel seedPhrases_en: BIP39_en {}
onAllEntriesValidChanged: {
let mnemonicString = ""
if (d.allEntriesValid) {
const sortTable = mnemonicInput.sort((a, b) => a.pos - b.pos)
for (let i = 0; i < mnemonicInput.length; i++) {
d.checkWordExistence(sortTable[i].seed)
mnemonicString += sortTable[i].seed + ((i === (grid.count-1)) ? "" : " ")
}
if (!Utils.isMnemonic(mnemonicString) || !root.isSeedPhraseValid(mnemonicString)) {
root.setWrongSeedPhraseMessage(qsTr("Invalid seed phrase"))
d.allEntriesValid = false
}
}
root.seedPhraseUpdated(d.allEntriesValid, mnemonicString)
}
function checkMnemonicLength() {
d.allEntriesValid = d.mnemonicInput.length === d.tabs[switchTabBar.currentIndex]
}
function checkWordExistence(word) {
d.allEntriesValid = d.allEntriesValid && d.seedPhrases_en.words.includes(word)
if (d.allEntriesValid) {
root.setWrongSeedPhraseMessage("")
}
else {
root.setWrongSeedPhraseMessage(qsTr("The phrase youve entered is invalid"))
}
}
function pasteWords () {
const clipboardText = globalUtils.getFromClipboard()
// Split words separated by commas and or blank spaces (spaces, enters, tabs)
const words = clipboardText.split(/[, \s]+/)
let index = d.tabs.indexOf(words.length)
if (index === -1) {
return false
}
let timeout = 0
if (switchTabBar.currentIndex !== index) {
switchTabBar.currentIndex = index
// Set the teimeout to 100 so the grid has time to generate the new items
timeout = 100
}
d.mnemonicInput = []
timer.setTimeout(() => {
// Populate mnemonicInput
for (let i = 0; i < words.length; i++) {
grid.addWord(i + 1, words[i], true)
}
// Populate grid
for (let j = 0; j < grid.count; j++) {
const item = grid.itemAtIndex(j)
if (!item || !item.leftComponentText) {
// The grid has gaps in it and also sometimes doesn't return the item correctly when offscreen
// in those cases, we just add the word in the array but not in the grid.
// The button will still work and import correctly. The Grid itself will be partly empty, but offscreen
// With the re-design of the grid, this should be fixed
continue
}
const pos = item.mnemonicIndex
item.setWord(words[pos - 1])
}
d.checkMnemonicLength()
}, timeout)
return true
}
}
Timer {
id: timer
}
StatusSwitchTabBar {
id: switchTabBar
Layout.preferredWidth: parent.width
Repeater {
model: d.tabs
StatusSwitchTabButton {
text: qsTr("%n word(s)", "", modelData)
id: seedPhraseWords
objectName: `${modelData}SeedButton`
}
}
onCurrentIndexChanged: {
d.mnemonicInput = d.mnemonicInput.filter(function(value) {
return value.pos <= d.tabs[switchTabBar.currentIndex]
})
d.checkMnemonicLength()
}
}
StatusGridView {
id: grid
readonly property var wordIndex: [
["1", "3", "5", "7", "9", "11", "2", "4", "6", "8", "10", "12"]
,["1", "4", "7", "10", "13", "16", "2", "5", "8",
"11", "14", "17", "3", "6", "9", "12", "15", "18"]
,["1", "5", "9", "13", "17", "21", "2", "6", "10", "14", "18", "22",
"3", "7", "11", "15", "19", "23", "4", "8", "12", "16", "20", "24"]
]
Layout.preferredWidth: parent.width
Layout.preferredHeight: 312
clip: false
flow: GridView.FlowTopToBottom
cellWidth: (parent.width/(count/6))
cellHeight: 52
interactive: false
z: 100000
cacheBuffer: 9999
model: switchTabBar.currentItem.text.substring(0,2)
function addWord(pos, word, ignoreGoingNext = false) {
d.mnemonicInput.push({pos: pos, seed: word.replace(/\s/g, '')})
for (let j = 0; j < d.mnemonicInput.length; j++) {
if (d.mnemonicInput[j].pos === pos && d.mnemonicInput[j].seed !== word) {
d.mnemonicInput[j].seed = word
break
}
}
//remove duplicates
const valueArr = d.mnemonicInput.map(item => item.pos)
const isDuplicate = valueArr.some((item, idx) => {
if (valueArr.indexOf(item) !== idx) {
d.mnemonicInput.splice(idx, 1)
}
return valueArr.indexOf(item) !== idx
})
if (!ignoreGoingNext) {
for (let i = 0; i < grid.count; i++) {
if (grid.itemAtIndex(i).mnemonicIndex !== (pos + 1)) {
continue
}
grid.currentIndex = grid.itemAtIndex(i).itemIndex
grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus()
if (grid.currentIndex !== 12) {
continue
}
grid.positionViewAtEnd()
if (grid.count === 20) {
grid.contentX = 1500
}
}
}
d.checkMnemonicLength()
}
delegate: StatusSeedPhraseInput {
id: seedWordInput
textEdit.input.edit.objectName: `statusSeedPhraseInputField${seedWordInput.leftComponentText}`
width: (grid.cellWidth - 8)
height: (grid.cellHeight - 8)
Behavior on width { NumberAnimation { duration: 180 } }
textEdit.text: {
const pos = seedWordInput.mnemonicIndex
for (let i in d.mnemonicInput) {
const p = d.mnemonicInput[i]
if (p.pos === pos) {
return p.seed
}
}
return ""
}
readonly property int mnemonicIndex: grid.wordIndex[(grid.count / 6) - 2][index]
leftComponentText: mnemonicIndex
inputList: d.seedPhrases_en
property int itemIndex: index
z: (grid.currentIndex === index) ? 150000000 : 0
onTextChanged: {
d.checkWordExistence(text)
}
onDoneInsertingWord: {
grid.addWord(mnemonicIndex, word)
}
onEditClicked: {
grid.currentIndex = index
grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus()
}
onKeyPressed: {
grid.currentIndex = index
if (event.key === Qt.Key_Backtab) {
for (let i = 0; i < grid.count; i++) {
if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex - 1) >= 0 ? (mnemonicIndex - 1) : 0)) {
grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.BacktabFocusReason)
textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit
event.accepted = true
break
}
}
} else if (event.key === Qt.Key_Tab) {
for (let i = 0; i < grid.count; i++) {
if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex + 1) <= grid.count ? (mnemonicIndex + 1) : grid.count)) {
grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.TabFocusReason)
textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit
event.accepted = true
break
}
}
}
if (event.matches(StandardKey.Paste)) {
if (d.pasteWords()) {
// Paste was done by splitting the words
event.accepted = true
}
return
}
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
event.accepted = true
if (d.allEntriesValid) {
root.submitSeedPhrase()
return
}
}
if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) {
const wordIndex = d.mnemonicInput.findIndex(x => x.pos === mnemonicIndex)
if (wordIndex > -1) {
d.mnemonicInput.splice(wordIndex, 1)
d.checkMnemonicLength()
}
}
}
Component.onCompleted: {
const item = grid.itemAtIndex(0)
if (item) {
item.textEdit.input.edit.forceActiveFocus()
}
}
}
}
StatusBaseText {
id: invalidSeedTxt
visible: text !== ""
Layout.alignment: Qt.AlignHCenter
color: Theme.palette.dangerColor1
}
}

View File

@ -0,0 +1,59 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import QtQuick.Controls 2.14
import QtGraphicalEffects 1.14
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
Item {
id: root
property var seedPhrase: []
property bool seedPhraseRevealed: false
StatusGridView {
id: grid
anchors.fill: parent
visible: root.seedPhraseRevealed
cellWidth: parent.width * 0.5
cellHeight: 48
interactive: false
model: 12
readonly property var wordIndex: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
readonly property int spacing: 4
delegate: StatusSeedPhraseInput {
width: (grid.cellWidth - grid.spacing)
height: (grid.cellHeight - grid.spacing)
textEdit.input.edit.enabled: false
text: {
const idx = parseInt(leftComponentText) - 1;
if (!root.seedPhrase || idx < 0 || idx > root.seedPhrase.length - 1)
return "";
return root.seedPhrase[idx];
}
leftComponentText: grid.wordIndex[index]
}
}
GaussianBlur {
id: blur
anchors.fill: grid
visible: !root.seedPhraseRevealed
source: grid
radius: 16
samples: 16
transparentBorder: true
}
StatusButton {
anchors.centerIn: parent
visible: !root.seedPhraseRevealed
type: StatusBaseButton.Type.Primary
icon.name: "view"
text: qsTr("Reveal seed phrase")
onClicked: {
root.seedPhraseRevealed = true
}
}
}

View File

@ -24,3 +24,5 @@ StatusAssetSelector 1.0 StatusAssetSelector.qml
AcceptRejectOptionsButtonsPanel 1.0 AcceptRejectOptionsButtonsPanel.qml
DidYouKnowSplashScreen 1.0 DidYouKnowSplashScreen.qml
ConnectionWarnings 1.0 ConnectionWarnings.qml
EnterSeedPhrase 1.0 EnterSeedPhrase.qml
SeedPhrase 1.0 SeedPhrase.qml

View File

@ -16,7 +16,6 @@ Item {
QtObject {
id: d
property bool primaryButtonEnabled: false
property bool seedPhraseRevealed: false
}
Loader {
@ -226,12 +225,11 @@ Item {
sharedKeycardModule: root.sharedKeycardModule
Component.onCompleted: {
hideSeed = !d.seedPhraseRevealed
d.primaryButtonEnabled = Qt.binding(function(){ return d.seedPhraseRevealed })
seedPhraseRevealed = false
}
onSeedPhraseRevealed: {
d.seedPhraseRevealed = true
onSeedPhraseRevealedChanged: {
d.primaryButtonEnabled = seedPhraseRevealed
}
}
}

View File

@ -1,14 +1,7 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import utils 1.0
import shared.stores 1.0
import shared.controls 1.0
import shared.panels 1.0 as SharedPanels
Item {
id: root
@ -20,292 +13,34 @@ Item {
QtObject {
id: d
property bool allEntriesValid: false
property var mnemonicInput: []
readonly property var tabs: [12, 18, 24]
readonly property ListModel seedPhrases_en: BIP39_en {}
property bool wrongSeedPhrase: root.sharedKeycardModule.keycardData & Constants.predefinedKeycardData.wrongSeedPhrase
onWrongSeedPhraseChanged: {
if (wrongSeedPhrase) {
invalidSeedTxt.text = qsTr("The phrase youve entered does not match this Keycards seed phrase")
}
else {
invalidSeedTxt.text = ""
}
seedPhrase.setWrongSeedPhraseMessage(wrongSeedPhrase? qsTr("The phrase youve entered does not match this Keycards seed phrase") : "")
}
onAllEntriesValidChanged: {
if (d.allEntriesValid) {
let mnemonicString = ""
const sortTable = mnemonicInput.sort((a, b) => a.pos - b.pos)
for (let i = 0; i < mnemonicInput.length; i++) {
d.checkWordExistence(sortTable[i].seed)
mnemonicString += sortTable[i].seed + ((i === (grid.count-1)) ? "" : " ")
}
if (Utils.isMnemonic(mnemonicString) && root.sharedKeycardModule.validSeedPhrase(mnemonicString)) {
root.sharedKeycardModule.setSeedPhrase(mnemonicString)
} else {
invalidSeedTxt.text = qsTr("Invalid seed phrase")
d.allEntriesValid = false
}
}
root.validation(d.allEntriesValid)
}
function checkMnemonicLength() {
d.allEntriesValid = d.mnemonicInput.length === d.tabs[switchTabBar.currentIndex]
}
function checkWordExistence(word) {
d.allEntriesValid = d.allEntriesValid && d.seedPhrases_en.words.includes(word)
if (d.allEntriesValid) {
invalidSeedTxt.text = ""
}
else {
invalidSeedTxt.text = qsTr("The phrase youve entered is invalid")
}
}
function pasteWords () {
const clipboardText = globalUtils.getFromClipboard()
// Split words separated by commas and or blank spaces (spaces, enters, tabs)
const words = clipboardText.split(/[, \s]+/)
let index = d.tabs.indexOf(words.length)
if (index === -1) {
return false
}
let timeout = 0
if (switchTabBar.currentIndex !== index) {
switchTabBar.currentIndex = index
// Set the teimeout to 100 so the grid has time to generate the new items
timeout = 100
}
d.mnemonicInput = []
timer.setTimeout(() => {
// Populate mnemonicInput
for (let i = 0; i < words.length; i++) {
grid.addWord(i + 1, words[i], true)
}
// Populate grid
for (let j = 0; j < grid.count; j++) {
const item = grid.itemAtIndex(j)
if (!item || !item.leftComponentText) {
// The grid has gaps in it and also sometimes doesn't return the item correctly when offscreen
// in those cases, we just add the word in the array but not in the grid.
// The button will still work and import correctly. The Grid itself will be partly empty, but offscreen
// With the re-design of the grid, this should be fixed
continue
}
const pos = item.mnemonicIndex
item.setWord(words[pos - 1])
}
d.checkMnemonicLength()
}, timeout)
return true
}
}
ColumnLayout {
SharedPanels.EnterSeedPhrase {
id: seedPhrase
anchors.fill: parent
anchors.topMargin: Style.current.xlPadding
anchors.bottomMargin: Style.current.halfPadding
anchors.leftMargin: Style.current.xlPadding
anchors.rightMargin: Style.current.xlPadding
spacing: Style.current.padding
clip: true
StatusBaseText {
id: title
Layout.preferredHeight: Constants.keycard.general.titleHeight
Layout.alignment: Qt.AlignHCenter
text: qsTr("Enter key pair seed phrase")
font.pixelSize: Constants.keycard.general.fontSize1
font.weight: Font.Bold
color: Theme.palette.directColor1
isSeedPhraseValid: function(mnemonic) {
return root.sharedKeycardModule.validSeedPhrase(mnemonic)
}
Timer {
id: timer
onSeedPhraseUpdated: {
if (valid) {
root.sharedKeycardModule.setSeedPhrase(seedPhrase)
}
root.validation(valid)
}
StatusSwitchTabBar {
id: switchTabBar
Layout.alignment: Qt.AlignHCenter
Repeater {
model: d.tabs
StatusSwitchTabButton {
text: qsTr("%n word(s)", "", modelData)
id: seedPhraseWords
objectName: `${modelData}SeedButton`
}
}
onCurrentIndexChanged: {
d.mnemonicInput = d.mnemonicInput.filter(function(value) {
return value.pos <= d.tabs[switchTabBar.currentIndex]
})
d.checkMnemonicLength()
}
}
StatusGridView {
id: grid
readonly property var wordIndex: [
["1", "3", "5", "7", "9", "11", "2", "4", "6", "8", "10", "12"]
,["1", "4", "7", "10", "13", "16", "2", "5", "8",
"11", "14", "17", "3", "6", "9", "12", "15", "18"]
,["1", "5", "9", "13", "17", "21", "2", "6", "10", "14", "18", "22",
"3", "7", "11", "15", "19", "23", "4", "8", "12", "16", "20", "24"]
]
Layout.preferredWidth: parent.width
Layout.preferredHeight: 312
clip: false
flow: GridView.FlowTopToBottom
cellWidth: (parent.width/(count/6))
cellHeight: 52
interactive: false
z: 100000
cacheBuffer: 9999
model: switchTabBar.currentItem.text.substring(0,2)
function addWord(pos, word, ignoreGoingNext = false) {
d.mnemonicInput.push({pos: pos, seed: word.replace(/\s/g, '')})
for (let j = 0; j < d.mnemonicInput.length; j++) {
if (d.mnemonicInput[j].pos === pos && d.mnemonicInput[j].seed !== word) {
d.mnemonicInput[j].seed = word
break
}
}
//remove duplicates
const valueArr = d.mnemonicInput.map(item => item.pos)
const isDuplicate = valueArr.some((item, idx) => {
if (valueArr.indexOf(item) !== idx) {
d.mnemonicInput.splice(idx, 1)
}
return valueArr.indexOf(item) !== idx
})
if (!ignoreGoingNext) {
for (let i = 0; i < grid.count; i++) {
if (grid.itemAtIndex(i).mnemonicIndex !== (pos + 1)) {
continue
}
grid.currentIndex = grid.itemAtIndex(i).itemIndex
grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus()
if (grid.currentIndex !== 12) {
continue
}
grid.positionViewAtEnd()
if (grid.count === 20) {
grid.contentX = 1500
}
}
}
d.checkMnemonicLength()
}
delegate: StatusSeedPhraseInput {
id: seedWordInput
textEdit.input.edit.objectName: `statusSeedPhraseInputField${seedWordInput.leftComponentText}`
width: (grid.cellWidth - 8)
height: (grid.cellHeight - 8)
Behavior on width { NumberAnimation { duration: 180 } }
textEdit.text: {
const pos = seedWordInput.mnemonicIndex
for (let i in d.mnemonicInput) {
const p = d.mnemonicInput[i]
if (p.pos === pos) {
return p.seed
}
}
return ""
}
readonly property int mnemonicIndex: grid.wordIndex[(grid.count / 6) - 2][index]
leftComponentText: mnemonicIndex
inputList: d.seedPhrases_en
property int itemIndex: index
z: (grid.currentIndex === index) ? 150000000 : 0
onTextChanged: {
d.checkWordExistence(text)
}
onDoneInsertingWord: {
grid.addWord(mnemonicIndex, word)
}
onEditClicked: {
grid.currentIndex = index
grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus()
}
onKeyPressed: {
grid.currentIndex = index
if (event.key === Qt.Key_Backtab) {
for (let i = 0; i < grid.count; i++) {
if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex - 1) >= 0 ? (mnemonicIndex - 1) : 0)) {
grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.BacktabFocusReason)
textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit
event.accepted = true
break
}
}
} else if (event.key === Qt.Key_Tab) {
for (let i = 0; i < grid.count; i++) {
if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex + 1) <= grid.count ? (mnemonicIndex + 1) : grid.count)) {
grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.TabFocusReason)
textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit
event.accepted = true
break
}
}
}
if (event.matches(StandardKey.Paste)) {
if (d.pasteWords()) {
// Paste was done by splitting the words
event.accepted = true
}
return
}
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
event.accepted = true
if (d.allEntriesValid) {
onSubmitSeedPhrase: {
root.sharedKeycardModule.currentState.doPrimaryAction()
return
}
}
if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) {
const wordIndex = d.mnemonicInput.findIndex(x => x.pos === mnemonicIndex)
if (wordIndex > -1) {
d.mnemonicInput.splice(wordIndex, 1)
d.checkMnemonicLength()
}
}
}
Component.onCompleted: {
const item = grid.itemAtIndex(0)
if (item) {
item.textEdit.input.edit.forceActiveFocus()
}
}
}
}
StatusBaseText {
id: invalidSeedTxt
Layout.alignment: Qt.AlignHCenter
color: Theme.palette.dangerColor1
}
}

View File

@ -9,20 +9,13 @@ import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import utils 1.0
import shared.panels 1.0 as SharedPanels
Item {
id: root
property var sharedKeycardModule
property bool hideSeed: true
signal seedPhraseRevealed()
QtObject {
id: d
readonly property var seedPhrase: root.sharedKeycardModule.getMnemonic().split(" ")
}
property alias seedPhraseRevealed: displaySeed.seedPhraseRevealed
ColumnLayout {
anchors.fill: parent
@ -48,55 +41,12 @@ Item {
wrapMode: Text.WordWrap
}
Item {
SharedPanels.SeedPhrase {
id: displaySeed
Layout.preferredWidth: parent.width
Layout.fillHeight: true
StatusGridView {
id: grid
anchors.fill: parent
visible: !root.hideSeed
cellWidth: parent.width * 0.5
cellHeight: 48
interactive: false
model: 12
readonly property var wordIndex: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
readonly property int spacing: 4
delegate: StatusSeedPhraseInput {
width: (grid.cellWidth - grid.spacing)
height: (grid.cellHeight - grid.spacing)
textEdit.input.edit.enabled: false
text: {
const idx = parseInt(leftComponentText) - 1;
if (!d.seedPhrase || idx < 0 || idx > d.seedPhrase.length - 1)
return "";
return d.seedPhrase[idx];
}
leftComponentText: grid.wordIndex[index]
}
}
GaussianBlur {
id: blur
anchors.fill: grid
visible: root.hideSeed
source: grid
radius: 16
samples: 16
transparentBorder: true
}
StatusButton {
anchors.centerIn: parent
visible: root.hideSeed
type: StatusBaseButton.Type.Primary
icon.name: "view"
text: qsTr("Reveal seed phrase")
onClicked: {
root.hideSeed = false;
root.seedPhraseRevealed()
}
}
property var seedPhrase: root.sharedKeycardModule.getMnemonic().split(" ")
}
}

View File

@ -608,6 +608,48 @@ QtObject {
readonly property int eventProcessError: 6
}
readonly property QtObject addAccountPopup: QtObject {
readonly property int popupWidth: 480
readonly property int contentHeight1: 554
readonly property int contentHeight2: 642
readonly property int itemHeight: 64
readonly property int labelFontSize1: 15
readonly property int labelFontSize2: 13
readonly property int footerButtonsHeight: 44
readonly property int keyPairNameMaxLength: 20
readonly property int stepperWidth: 242
readonly property int stepperHeight: 30
readonly property QtObject keyPairType: QtObject {
readonly property int unknown: -1
readonly property int profile: 0
readonly property int seedImport: 1
readonly property int privateKeyImport: 2
}
readonly property QtObject predefinedPaths: QtObject {
readonly property string custom: "m/44'"
readonly property string ethereum: "m/44'/60'/0'/0"
readonly property string ethereumRopsten: "m/44'/1'/0'/0"
readonly property string ethereumLedger: "m/44'/60'/0'"
readonly property string ethereumLedgerLive: "m/44'/60'"
}
readonly property QtObject state: QtObject {
readonly property string noState: "NoState"
readonly property string main: "Main"
readonly property string selectMasterKey: "SelectMasterKey"
readonly property string enterSeedPhrase: "EnterSeedPhrase"
readonly property string enterSeedPhraseWord1: "EnterSeedPhraseWord1"
readonly property string enterSeedPhraseWord2: "EnterSeedPhraseWord2"
readonly property string enterPrivateKey: "EnterPrivateKey"
readonly property string enterKeypairName: "EnterKeypairName"
readonly property string displaySeedPhrase: "DisplaySeedPhrase"
readonly property string confirmAddingNewMasterKey: "ConfirmAddingNewMasterKey"
readonly property string confirmSeedPhraseBackup: "ConfirmSeedPhraseBackup"
}
}
readonly property QtObject localPairingAction: QtObject {
readonly property int actionUnknown: 0
readonly property int actionConnect: 1
@ -842,12 +884,10 @@ QtObject {
readonly property string loginAccountsListAddNewUser: "LOGIN-ACCOUNTS-LIST-ADD-NEW-USER"
readonly property string loginAccountsListAddExistingUser: "LOGIN-ACCOUNTS-LIST-ADD-EXISTING-USER"
readonly property string loginAccountsListLostKeycard: "LOGIN-ACCOUNTS-LIST-LOST-KEYCARD"
}
readonly property var appTranslationMap: ({})
Component.onCompleted: {
appTranslationMap[appTranslatableConstants.loginAccountsListAddNewUser] = qsTr("Add new user")
appTranslationMap[appTranslatableConstants.loginAccountsListAddExistingUser] = qsTr("Add existing Status user")
appTranslationMap[appTranslatableConstants.loginAccountsListLostKeycard] = qsTr("Lost Keycard")
readonly property string addAccountLabelNewWatchOnlyAccount: "LABEL-NEW-WATCH-ONLY-ACCOUNT"
readonly property string addAccountLabelExisting: "LABEL-EXISTING"
readonly property string addAccountLabelImportNew: "LABEL-IMPORT-NEW"
readonly property string addAccountLabelOptionAddNewMasterKey: "LABEL-OPTION-ADD-NEW-MASTER-KEY"
readonly property string addAccountLabelOptionAddWatchOnlyAcc: "LABEL-OPTION-ADD-WATCH-ONLY-ACC"
}
}

View File

@ -617,6 +617,29 @@ QtObject {
return result
}
function appTranslation(key) {
switch(key) {
case Constants.appTranslatableConstants.loginAccountsListAddNewUser:
return qsTr("Add new user")
case Constants.appTranslatableConstants.loginAccountsListAddExistingUser:
return qsTr("Add existing Status user")
case Constants.appTranslatableConstants.loginAccountsListLostKeycard:
return qsTr("Lost Keycard")
case Constants.appTranslatableConstants.addAccountLabelNewWatchOnlyAccount:
return qsTr("New watch-only account")
case Constants.appTranslatableConstants.addAccountLabelExisting:
return qsTr("Existing")
case Constants.appTranslatableConstants.addAccountLabelImportNew:
return qsTr("Import new")
case Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey:
return qsTr("Add new master key")
case Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc:
return qsTr("Add watch-only account")
}
return key
}
// Leave this function at the bottom of the file as QT Creator messes up the code color after this
function isPunct(c) {
return /(!|\@|#|\$|%|\^|&|\*|\(|\)|\+|\||-|=|\\|{|}|[|]|"|;|'|<|>|\?|,|\.|\/)/.test(c)