refactor(@desktop/wallet): wallet service improvements

Closes: #11752
This commit is contained in:
Sale Djenic 2023-08-03 12:48:31 +02:00 committed by saledjenic
parent 23426f184b
commit 570f312617
7 changed files with 1142 additions and 1069 deletions

View File

@ -54,16 +54,13 @@ proc getCurrencyAmount*(self: Controller, amount: float64, symbol: string): Curr
proc updateCurrency*(self: Controller, currency: string) =
self.walletAccountService.updateCurrency(currency)
# proc getIndex*(self: Controller, address: string): int =
# return self.walletAccountService.getIndex(address)
proc getNetworks*(self: Controller): seq[NetworkDto] =
return self.networkService.getNetworks()
proc getWalletAccounts*(self: Controller): seq[wallet_account_service.WalletAccountDto] =
return self.walletAccountService.getWalletAccounts()
proc getEnabledChainIds*(self: Controller): seq[int] =
proc getEnabledChainIds*(self: Controller): seq[int] =
return self.networkService.getNetworks().filter(n => n.enabled).map(n => n.chainId)
proc toggleIncludeWatchOnlyAccount*(self: Controller) =

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,588 @@
proc storeWatchOnlyAccount(self: Service, account: WalletAccountDto) =
if self.watchOnlyAccounts.hasKey(account.address):
error "trying to store an already existing watch only account"
return
self.watchOnlyAccounts[account.address] = account
proc storeKeypair(self: Service, keypair: KeypairDto) =
if keypair.keyUid.len == 0:
error "trying to store a keypair with empty keyUid"
return
if self.keypairs.hasKey(keypair.keyUid):
error "trying to store an already existing keypair"
return
self.keypairs[keypair.keyUid] = keypair
proc storeAccountToKeypair(self: Service, account: WalletAccountDto) =
if account.keyUid.len == 0:
error "trying to store a keypair related account with empty keyUid"
return
if self.keypairs[account.keyUid].accounts.filter(acc => cmpIgnoreCase(acc.address, account.address) == 0).len != 0:
error "trying to store an already existing keytpair related account"
return
self.keypairs[account.keyUid].accounts.add(account)
proc getKeypairs*(self: Service): seq[KeypairDto] =
return toSeq(self.keypairs.values)
proc getKeypairByKeyUid*(self: Service, keyUid: string): KeypairDto =
if not self.keypairs.hasKey(keyUid):
return
return self.keypairs[keyUid]
proc getWatchOnlyAccounts*(self: Service): seq[WalletAccountDto] =
return toSeq(self.watchOnlyAccounts.values)
proc getWatchOnlyAccountByAddress*(self: Service, address: string): WalletAccountDto =
if not self.watchOnlyAccounts.hasKey(address):
return
return self.watchOnlyAccounts[address]
proc getAccountByAddress*(self: Service, address: string): WalletAccountDto =
result = self.getWatchOnlyAccountByAddress(address)
if not result.isNil:
return
for _, kp in self.keypairs:
let accounts = kp.accounts.filter(acc => cmpIgnoreCase(acc.address, address) == 0)
if accounts.len == 1:
return accounts[0]
proc getAccountsByAddresses*(self: Service, addresses: seq[string]): seq[WalletAccountDto] =
for address in addresses:
let acc = self.getAccountByAddress(address)
if acc.isNil:
continue
result.add(acc)
proc getWalletAccounts*(self: Service): seq[WalletAccountDto] =
for _, kp in self.keypairs:
if kp.keypairType == KeypairTypeProfile:
for acc in kp.accounts:
if acc.isChat:
continue
result.add(acc)
continue
result.add(kp.accounts)
result.add(toSeq(self.watchOnlyAccounts.values))
result.sort(walletAccountsCmp)
proc getWalletAddresses*(self: Service): seq[string] =
return self.getWalletAccounts().map(a => a.address)
proc updateAssetsLoadingState(self: Service, address: string, loading: bool) =
var acc = self.getAccountByAddress(address)
if acc.isNil:
return
acc.assetsLoading = loading
#################################################
# TODO: remove functions below
#
# The only need for a function `getIndex` below is for switching selected account.
# Switching an account in UI by the index on which an account is stored in wallet settings service cache
# is completely wrong approach we need to handle that properly, at least by using position.
proc getIndex*(self: Service, address: string): int =
let accounts = self.getWalletAccounts()
for i in 0 ..< accounts.len:
if cmpIgnoreCase(accounts[i].address, address) == 0:
return i
# The same for `getWalletAccount`, both of them need to be removed. Parts of the app which are using them
# need refactor for sure.
proc getWalletAccount*(self: Service, accountIndex: int): WalletAccountDto =
let accounts = self.getWalletAccounts()
if accountIndex < 0 or accountIndex >= accounts.len:
return
return accounts[accountIndex]
#################################################
proc startWallet(self: Service) =
if(not main_constants.WALLET_ENABLED):
return
discard backend.startWallet()
proc init*(self: Service) =
try:
let chainId = self.networkService.getNetworkForEns().chainId
let woAccounts = getWatchOnlyAccountsFromDb()
for acc in woAccounts:
acc.ens = getEnsName(acc.address, chainId)
self.storeWatchOnlyAccount(acc)
let keypairs = getKeypairsFromDb()
for kp in keypairs:
for acc in kp.accounts:
acc.ens = getEnsName(acc.address, chainId)
self.storeKeypair(kp)
let addresses = self.getWalletAddresses()
self.buildAllTokens(addresses, store = true)
self.checkRecentHistory(addresses)
self.startWallet()
except Exception as e:
let errDesription = e.msg
error "error: ", errDesription
return
self.events.on(SignalType.Message.event) do(e: Args):
var receivedData = MessageSignal(e)
if receivedData.watchOnlyAccounts.len > 0:
for acc in receivedData.watchOnlyAccounts:
self.handleWalletAccount(acc)
if receivedData.keypairs.len > 0:
for kp in receivedData.keypairs:
self.handleKeypair(kp)
if receivedData.accountsPositions.len > 0:
self.updateAccountsPositions()
self.events.emit(SIGNAL_WALLET_ACCOUNT_POSITION_UPDATED, Args())
self.events.on(SignalType.Wallet.event) do(e:Args):
var data = WalletSignal(e)
case data.eventType:
of "wallet-tick-reload":
let addresses = self.getWalletAddresses()
self.buildAllTokens(addresses, store = true)
self.checkRecentHistory(addresses)
self.events.on(SIGNAL_CURRENCY_UPDATED) do(e:Args):
self.buildAllTokens(self.getWalletAddresses(), store = true)
proc addNewKeypairsAccountsToLocalStoreAndNotify(self: Service, notify: bool = true) =
let chainId = self.networkService.getNetworkForEns().chainId
let allLocalAaccounts = self.getWalletAccounts()
# check if there is new watch only account
let woAccountsDb = getWatchOnlyAccountsFromDb()
for woAccDb in woAccountsDb:
var found = false
for localAcc in allLocalAaccounts:
if cmpIgnoreCase(localAcc.address, woAccDb.address) == 0:
found = true
break
if found:
continue
woAccDb.ens = getEnsName(woAccDb.address, chainId)
self.storeWatchOnlyAccount(woAccDb)
self.buildAllTokens(@[woAccDb.address], store = true)
if notify:
self.events.emit(SIGNAL_WALLET_ACCOUNT_SAVED, AccountArgs(account: woAccDb))
# check if there is new keypair or any account added to an existing keypair
let keypairsDb = getKeypairsFromDb()
for kpDb in keypairsDb:
var localKp = self.getKeypairByKeyUid(kpDb.keyUid)
if localKp.isNil:
self.storeKeypair(kpDb)
let addresses = kpDb.accounts.map(a => a.address)
self.buildAllTokens(addresses, store = true)
for acc in kpDb.accounts:
acc.ens = getEnsName(acc.address, chainId)
if notify:
self.events.emit(SIGNAL_WALLET_ACCOUNT_SAVED, AccountArgs(account: acc))
else:
for accDb in kpDb.accounts:
var found = false
for localAcc in allLocalAaccounts:
if cmpIgnoreCase(localAcc.address, accDb.address) == 0:
found = true
break
if found:
continue
accDb.ens = getEnsName(accDb.address, chainId)
self.storeAccountToKeypair(accDb)
self.buildAllTokens(@[accDb.address], store = true)
if notify:
self.events.emit(SIGNAL_WALLET_ACCOUNT_SAVED, AccountArgs(account: accDb))
proc removeAccountFromLocalStoreAndNotify(self: Service, address: string, notify: bool = true) =
var acc = self.getAccountByAddress(address)
if acc.isNil:
return
if acc.keyUid.len == 0:
self.watchOnlyAccounts.del(acc.address)
else:
var index = -1
for i in 0 ..< self.keypairs[acc.keyUid].accounts.len:
if cmpIgnoreCase(self.keypairs[acc.keyUid].accounts[i].address, acc.address) == 0:
index = i
break
if index == -1:
error "cannot find account with the address to remove", address=address
return
self.keypairs[acc.keyUid].accounts.del(index)
if self.keypairs[acc.keyUid].accounts.len == 0:
self.keypairs.del(acc.keyUid)
if notify:
self.events.emit(SIGNAL_WALLET_ACCOUNT_DELETED, AccountArgs(account: acc))
proc updateAccountsPositions(self: Service) =
let dbAccounts = getAccountsFromDb()
for dbAcc in dbAccounts:
var localAcc = self.getAccountByAddress(dbAcc.address)
if localAcc.isNil:
continue
localAcc.position = dbAcc.position
proc updateAccountInLocalStoreAndNotify(self: Service, address, name, colorId, emoji: string,
positionUpdated: Option[bool] = none(bool), notify: bool = true) =
if address.len > 0:
var account = self.getAccountByAddress(address)
if account.isNil:
return
if name.len > 0 or colorId.len > 0 or emoji.len > 0:
if name.len > 0 and name != account.name:
account.name = name
if colorId.len > 0 and colorId != account.colorId:
account.colorId = colorId
if emoji.len > 0 and emoji != account.emoji:
account.emoji = emoji
if notify:
self.events.emit(SIGNAL_WALLET_ACCOUNT_UPDATED, AccountArgs(account: account))
else:
if not positionUpdated.isSome:
return
if positionUpdated.get:
## if reordering was successfully stored, we need to update local storage
self.updateAccountsPositions()
if notify:
self.events.emit(SIGNAL_WALLET_ACCOUNT_POSITION_UPDATED, Args())
proc updatePreferredSharingChainsAndNotify(self: Service, address, prodPreferredChains, testPreferredChains: string) =
var account = self.getAccountByAddress(address)
if account.isNil:
error "account's address is not among known addresses: ", address=address, procName="updatePreferredSharingChainsAndNotify"
return
if testPreferredChains.len > 0 and testPreferredChains != account.testPreferredChainIds:
account.testPreferredChainIds = testPreferredChains
if prodPreferredChains.len > 0 and prodPreferredChains != account.prodPreferredChainIds:
account.prodPreferredChainIds = prodPreferredChains
self.events.emit(SIGNAL_WALLET_ACCOUNT_PREFERRED_SHARING_CHAINS_UPDATED, AccountArgs(account: account))
## if password is not provided local keystore file won't be created
proc addWalletAccount*(self: Service, password: string, doPasswordHashing: bool, name, address, path, publicKey,
keyUid, accountType, colorId, emoji: string): string =
try:
var response: RpcResponse[JsonNode]
if password.len == 0:
response = status_go_accounts.addAccountWithoutKeystoreFileCreation(name, address, path, publicKey, keyUid,
accountType, colorId, emoji)
else:
var finalPassword = password
if doPasswordHashing:
finalPassword = utils.hashPassword(password)
response = status_go_accounts.addAccount(finalPassword, name, address, path, publicKey, keyUid, accountType,
colorId, emoji)
if not response.error.isNil:
error "status-go error", procName="addWalletAccount", errCode=response.error.code, errDesription=response.error.message
return response.error.message
self.addNewKeypairsAccountsToLocalStoreAndNotify()
return ""
except Exception as e:
error "error: ", procName="addWalletAccount", errName=e.name, errDesription=e.msg
return e.msg
## Mandatory fields for account: `address`, `keyUid`, `walletType`, `path`, `publicKey`, `name`, `emoji`, `colorId`
proc addNewPrivateKeyKeypair*(self: Service, privateKey, password: string, doPasswordHashing: bool,
keyUid, keypairName, rootWalletMasterKey: string, account: WalletAccountDto): string =
if password.len == 0:
error "for adding new private key account, password must be provided"
return
var finalPassword = password
if doPasswordHashing:
finalPassword = utils.hashPassword(password)
try:
var response = status_go_accounts.importPrivateKey(privateKey, finalPassword)
if not response.error.isNil:
error "status-go error importing private key", procName="addNewPrivateKeyKeypair", errCode=response.error.code, errDesription=response.error.message
return response.error.message
response = status_go_accounts.addKeypair(finalPassword, keyUid, keypairName, KeypairTypeKey, rootWalletMasterKey, @[account])
if not response.error.isNil:
error "status-go error adding keypair", procName="addNewPrivateKeyKeypair", errCode=response.error.code, errDesription=response.error.message
return response.error.message
self.addNewKeypairsAccountsToLocalStoreAndNotify()
return ""
except Exception as e:
error "error: ", procName="addNewPrivateKeyKeypair", errName=e.name, errDesription=e.msg
return e.msg
## Mandatory fields for all accounts: `address`, `keyUid`, `walletType`, `path`, `publicKey`, `name`, `emoji`, `colorId`
proc addNewSeedPhraseKeypair*(self: Service, seedPhrase, password: string, doPasswordHashing: bool,
keyUid, keypairName, rootWalletMasterKey: string, accounts: seq[WalletAccountDto]): string =
var finalPassword = password
if password.len > 0 and doPasswordHashing:
finalPassword = utils.hashPassword(password)
try:
if seedPhrase.len > 0 and password.len > 0:
let response = status_go_accounts.importMnemonic(seedPhrase, finalPassword)
if not response.error.isNil:
error "status-go error importing private key", procName="addNewSeedPhraseKeypair", errCode=response.error.code, errDesription=response.error.message
return response.error.message
let response = status_go_accounts.addKeypair(finalPassword, keyUid, keypairName, KeypairTypeSeed, rootWalletMasterKey, accounts)
if not response.error.isNil:
error "status-go error adding keypair", procName="addNewSeedPhraseKeypair", errCode=response.error.code, errDesription=response.error.message
return response.error.message
for i in 0 ..< accounts.len:
self.addNewKeypairsAccountsToLocalStoreAndNotify()
return ""
except Exception as e:
error "error: ", procName="addNewSeedPhraseKeypair", errName=e.name, errDesription=e.msg
return e.msg
proc getRandomMnemonic*(self: Service): string =
try:
let response = status_go_accounts.getRandomMnemonic()
if not response.error.isNil:
error "status-go error", procName="getRandomMnemonic", errCode=response.error.code, errDesription=response.error.message
return ""
return response.result.getStr
except Exception as e:
error "error: ", procName="getRandomMnemonic", errName=e.name, errDesription=e.msg
return ""
proc deleteAccount*(self: Service, address: string) =
try:
let response = status_go_accounts.deleteAccount(address)
if not response.error.isNil:
error "status-go error", procName="deleteAccount", errCode=response.error.code, errDesription=response.error.message
return
self.removeAccountFromLocalStoreAndNotify(address)
except Exception as e:
error "error: ", procName="deleteAccount", errName = e.name, errDesription = e.msg
proc deleteKeypair*(self: Service, keyUid: string) =
try:
let kp = self.getKeypairByKeyUid(keyUid)
if kp.isNil:
error "there is no known keypair", keyUid=keyUid, procName="deleteKeypair"
return
let response = status_go_accounts.deleteKeypair(keyUid)
if not response.error.isNil:
error "status-go error", procName="deleteKeypair", errCode=response.error.code, errDesription=response.error.message
return
self.updateAccountsPositions()
for acc in kp.accounts:
self.removeAccountFromLocalStoreAndNotify(acc.address)
except Exception as e:
error "error: ", procName="deleteKeypair", errName = e.name, errDesription = e.msg
proc updateCurrency*(self: Service, newCurrency: string) =
discard self.settingsService.saveCurrency(newCurrency)
proc setNetworksState*(self: Service, chainIds: seq[int], enabled: bool) =
self.networkService.setNetworksState(chainIds, enabled)
self.events.emit(SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED, Args())
proc toggleTestNetworksEnabled*(self: Service) =
discard self.settingsService.toggleTestNetworksEnabled()
let addresses = self.getWalletAddresses()
self.buildAllTokens(addresses, store = true)
self.tokenService.loadData()
self.checkRecentHistory(addresses)
self.events.emit(SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED, Args())
proc updateWalletAccount*(self: Service, address: string, accountName: string, colorId: string, emoji: string): bool =
try:
var account = self.getAccountByAddress(address)
if account.isNil:
error "account's address is not among known addresses: ", address=address, procName="updateWalletAccount"
return false
let response = status_go_accounts.updateAccount(accountName, account.address, account.path, account.publicKey,
account.keyUid, account.walletType, colorId, emoji, account.isWallet, account.isChat, account.prodPreferredChainIds, account.testPreferredChainIds)
if not response.error.isNil:
error "status-go error", procName="updateWalletAccount", errCode=response.error.code, errDesription=response.error.message
return false
self.updateAccountInLocalStoreAndNotify(address, accountName, colorId, emoji)
return true
except Exception as e:
error "error: ", procName="updateWalletAccount", errName=e.name, errDesription=e.msg
return false
proc updateWalletAccountProdPreferredChains*(self: Service, address, preferredChainIds: string): bool =
try:
var account = self.getAccountByAddress(address)
if account.isNil:
error "account's address is not among known addresses: ", address=address, procName="updateWalletAccountProdPreferredChains"
return false
let response = status_go_accounts.updateAccount(account.name, account.address, account.path, account.publicKey,
account.keyUid, account.walletType, account.colorId, account.emoji, account.isWallet, account.isChat, preferredChainIds, account.testPreferredChainIds)
if not response.error.isNil:
error "status-go error", procName="updateWalletAccountProdPreferredChains", errCode=response.error.code, errDesription=response.error.message
return false
self.updatePreferredSharingChainsAndNotify(address, prodPreferredChains = preferredChainIds, testPreferredChains = "")
return true
except Exception as e:
error "error: ", procName="updateWalletAccountProdPreferredChains", errName=e.name, errDesription=e.msg
return false
proc updateWalletAccountTestPreferredChains*(self: Service, address, preferredChainIds: string): bool =
try:
var account = self.getAccountByAddress(address)
if account.isNil:
error "account's address is not among known addresses: ", address=address, procName="updateWalletAccountTestPreferredChains"
return false
let response = status_go_accounts.updateAccount(account.name, account.address, account.path, account.publicKey,
account.keyUid, account.walletType, account.colorId, account.emoji, account.isWallet, account.isChat, account.prodPreferredChainIds, preferredChainIds)
if not response.error.isNil:
error "status-go error", procName="updateWalletAccountTestPreferredChains", errCode=response.error.code, errDesription=response.error.message
return false
self.updatePreferredSharingChainsAndNotify(address, prodPreferredChains = "", testPreferredChains = preferredChainIds)
return true
except Exception as e:
error "error: ", procName="updateWalletAccountTestPreferredChains", errName=e.name, errDesription=e.msg
return false
proc moveAccountFinally*(self: Service, fromPosition: int, toPosition: int) =
var updated = false
try:
let response = backend.moveWalletAccount(fromPosition, toPosition)
if not response.error.isNil:
error "status-go error", procName="moveAccountFinally", errCode=response.error.code, errDesription=response.error.message
updated = true
except Exception as e:
error "error: ", procName="moveAccountFinally", errName=e.name, errDesription=e.msg
self.updateAccountInLocalStoreAndNotify(address = "", name = "", colorId = "", emoji = "", some(updated))
proc updateKeypairName*(self: Service, keyUid: string, name: string) =
try:
let kp = self.getKeypairByKeyUid(keyUid)
if kp.isNil:
error "there is no known keypair", keyUid=keyUid, procName="updateKeypairName"
return
let response = backend.updateKeypairName(keyUid, name)
if not response.error.isNil:
error "status-go error", procName="updateKeypairName", errCode=response.error.code, errDesription=response.error.message
return
var data = KeypairArgs(
keypair: KeypairDto(
keyUid: keyUid,
name: name
),
oldKeypairName: kp.name
)
kp.name = name
self.events.emit(SIGNAL_KEYPAIR_NAME_CHANGED, data)
except Exception as e:
error "error: ", procName="updateKeypairName", errName=e.name, errDesription=e.msg
proc fetchDerivedAddresses*(self: Service, password: string, derivedFrom: string, paths: seq[string], hashPassword: bool) =
let arg = FetchDerivedAddressesTaskArg(
password: if hashPassword: utils.hashPassword(password) else: password,
derivedFrom: derivedFrom,
paths: paths,
tptr: cast[ByteAddress](fetchDerivedAddressesTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onDerivedAddressesFetched",
)
self.threadpool.start(arg)
proc onDerivedAddressesFetched*(self: Service, jsonString: string) {.slot.} =
let response = parseJson(jsonString)
var derivedAddress: seq[DerivedAddressDto] = @[]
derivedAddress = response["derivedAddresses"].getElems().map(x => x.toDerivedAddressDto())
let error = response["error"].getStr()
self.events.emit(SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESSES_FETCHED, DerivedAddressesArgs(
derivedAddresses: derivedAddress,
error: error
))
proc fetchDerivedAddressesForMnemonic*(self: Service, mnemonic: string, paths: seq[string])=
let arg = FetchDerivedAddressesForMnemonicTaskArg(
mnemonic: mnemonic,
paths: paths,
tptr: cast[ByteAddress](fetchDerivedAddressesForMnemonicTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onDerivedAddressesForMnemonicFetched",
)
self.threadpool.start(arg)
proc onDerivedAddressesForMnemonicFetched*(self: Service, jsonString: string) {.slot.} =
let response = parseJson(jsonString)
var derivedAddress: seq[DerivedAddressDto] = @[]
derivedAddress = response["derivedAddresses"].getElems().map(x => x.toDerivedAddressDto())
let error = response["error"].getStr()
self.events.emit(SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESSES_FROM_MNEMONIC_FETCHED, DerivedAddressesArgs(
derivedAddresses: derivedAddress,
error: error
))
proc fetchDetailsForAddresses*(self: Service, uniqueId: string, addresses: seq[string]) =
let network = self.networkService.getNetworkForActivityCheck()
let arg = FetchDetailsForAddressesTaskArg(
uniqueId: uniqueId,
chainId: network.chainId,
addresses: addresses,
tptr: cast[ByteAddress](fetchDetailsForAddressesTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAddressDetailsFetched",
)
self.threadpool.start(arg)
proc onAddressDetailsFetched*(self: Service, jsonString: string) {.slot.} =
var data = DerivedAddressesArgs()
try:
let response = parseJson(jsonString)
data.uniqueId = response["uniqueId"].getStr()
let addrDto = response{"details"}.toDerivedAddressDto()
data.derivedAddresses.add(addrDto)
data.error = response["error"].getStr()
except Exception as e:
error "error: ", procName="fetchAddressDetails", errName = e.name, errDesription = e.msg
data.error = e.msg
self.events.emit(SIGNAL_WALLET_ACCOUNT_ADDRESS_DETAILS_FETCHED, data)
proc handleWalletAccount(self: Service, account: WalletAccountDto, notify: bool = true) =
if account.removed:
self.updateAccountsPositions()
self.removeAccountFromLocalStoreAndNotify(account.address, notify)
else:
var localAcc = self.getAccountByAddress(account.address)
if not localAcc.isNil:
self.updateAccountInLocalStoreAndNotify(account.address, account.name, account.colorId, account.emoji,
none(bool), notify)
else:
self.addNewKeypairsAccountsToLocalStoreAndNotify(notify)
proc handleKeypair(self: Service, keypair: KeypairDto) =
let localKp = self.getKeypairByKeyUid(keypair.keyUid)
if not localKp.isNil:
# - first remove removed accounts from the UI
for localAcc in localKp.accounts:
let accAddress = localAcc.address
if keypair.accounts.filter(a => cmpIgnoreCase(a.address, accAddress) == 0).len == 0:
self.handleWalletAccount(WalletAccountDto(address: accAddress, removed: true), notify = false)
# - second add/update new/existing accounts
for acc in keypair.accounts:
self.handleWalletAccount(acc, notify = false)
else:
self.addNewKeypairsAccountsToLocalStoreAndNotify(notify = false)
# notify all interested parts about the keypair change
self.events.emit(SIGNAL_KEYPAIR_SYNCED, KeypairArgs(keypair: keypair))
proc isIncludeWatchOnlyAccount*(self: Service): bool =
return self.settingsService.isIncludeWatchOnlyAccount()
proc toggleIncludeWatchOnlyAccount*(self: Service) =
self.settingsService.toggleIncludeWatchOnlyAccount()
proc onFetchChainIdForUrl*(self: Service, jsonString: string) {.slot.} =
let response = parseJson(jsonString)
self.events.emit(SIGNAL_WALLET_ACCOUNT_CHAIN_ID_FOR_URL_FETCHED, ChainIdForUrlArgs(
chainId: response{"chainId"}.getInt,
success: response{"success"}.getBool,
url: response{"url"}.getStr,
))
proc fetchChainIdForUrl*(self: Service, url: string) =
let arg = FetchChainIdForUrlTaskArg(
tptr: cast[ByteAddress](fetchChainIdForUrlTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onFetchChainIdForUrl",
url: url
)
self.threadpool.start(arg)
proc getEnabledChainIds*(self: Service): seq[int] =
return self.networkService.getNetworks().filter(n => n.enabled).map(n => n.chainId)
proc getCurrencyFormat*(self: Service, symbol: string): CurrencyFormatDto =
return self.currencyService.getCurrencyFormat(symbol)
proc areTestNetworksEnabled*(self: Service): bool =
return self.settingsService.areTestNetworksEnabled()

View File

@ -0,0 +1,184 @@
proc addKeycardOrAccountsAsync*(self: Service, keycard: KeycardDto, accountsComingFromKeycard: bool = false) =
let arg = SaveOrUpdateKeycardTaskArg(
tptr: cast[ByteAddress](saveOrUpdateKeycardTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onKeycardAdded",
keycard: keycard,
accountsComingFromKeycard: accountsComingFromKeycard
)
self.threadpool.start(arg)
proc emitAddKeycardAddAccountsChange(self: Service, success: bool, keycard: KeycardDto) =
let data = KeycardArgs(
success: success,
keycard: keycard
)
self.events.emit(SIGNAL_NEW_KEYCARD_SET, data)
proc onKeycardAdded*(self: Service, response: string) {.slot.} =
var keycard = KeycardDto()
var success = false
try:
let responseObj = response.parseJson
discard responseObj.getProp("success", success)
var kpJson: JsonNode
if responseObj.getProp("keycard", kpJson):
keycard = kpJson.toKeycardDto()
except Exception as e:
error "error handilng migrated keycard response", errDesription=e.msg
self.emitAddKeycardAddAccountsChange(success, keycard)
proc addKeycardOrAccounts*(self: Service, keycard: KeycardDto, accountsComingFromKeycard: bool = false): bool =
var success = false
try:
let response = backend.saveOrUpdateKeycard(
%* {
"keycard-uid": keycard.keycardUid,
"keycard-name": keycard.keycardName,
# "keycard-locked" - no need to set it here, cause it will be set to false by the status-go
"key-uid": keycard.keyUid,
"accounts-addresses": keycard.accountsAddresses,
# "position": - no need to set it here, cause it is fully maintained by the status-go
},
accountsComingFromKeycard
)
success = responseHasNoErrors("addKeycardOrAccounts", response)
except Exception as e:
error "error: ", procName="addKeycardOrAccounts", errName = e.name, errDesription = e.msg
self.emitAddKeycardAddAccountsChange(success = success, keycard)
return success
proc removeMigratedAccountsForKeycard*(self: Service, keyUid: string, keycardUid: string, accountsToRemove: seq[string]) =
let arg = DeleteKeycardAccountsTaskArg(
tptr: cast[ByteAddress](deleteKeycardAccountsTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onMigratedAccountsForKeycardRemoved",
keycard: KeycardDto(keyUid: keyUid, keycardUid: keycardUid, accountsAddresses: accountsToRemove)
)
self.threadpool.start(arg)
proc onMigratedAccountsForKeycardRemoved*(self: Service, response: string) {.slot.} =
var data = KeycardArgs(
success: false,
)
try:
let responseObj = response.parseJson
discard responseObj.getProp("success", data.success)
var kpJson: JsonNode
if responseObj.getProp("keycard", kpJson):
data.keycard = kpJson.toKeycardDto()
except Exception as e:
error "error handilng migrated keycard response", errDesription=e.msg
self.events.emit(SIGNAL_KEYCARD_ACCOUNTS_REMOVED, data)
proc getAllKnownKeycards*(self: Service): seq[KeycardDto] =
try:
let response = backend.getAllKnownKeycards()
if responseHasNoErrors("getAllKnownKeycards", response):
return map(response.result.getElems(), proc(x: JsonNode): KeycardDto = toKeycardDto(x))
except Exception as e:
error "error: ", procName="getAllKnownKeycards", errName = e.name, errDesription = e.msg
proc getKeycardByKeycardUid*(self: Service, keycardUid: string): KeycardDto =
try:
let response = backend.getKeycardByKeycardUID(keycardUid)
if responseHasNoErrors("getKeycardByKeycardUid", response):
return response.result.toKeycardDto()
except Exception as e:
error "error: ", procName="getKeycardByKeycardUid", errName = e.name, errDesription = e.msg
proc getKeycardsWithSameKeyUid*(self: Service, keyUid: string): seq[KeycardDto] =
try:
let response = backend.getKeycardsWithSameKeyUID(keyUid)
if responseHasNoErrors("getKeycardsWithSameKeyUid", response):
return map(response.result.getElems(), proc(x: JsonNode): KeycardDto = toKeycardDto(x))
except Exception as e:
error "error: ", procName="getKeycardsWithSameKeyUid", errName = e.name, errDesription = e.msg
proc isKeycardAccount*(self: Service, account: WalletAccountDto): bool =
if account.isNil or
account.keyUid.len == 0 or
account.path.len == 0 or
utils.isPathOutOfTheDefaultStatusDerivationTree(account.path):
return false
let keycards = self.getKeycardsWithSameKeyUid(account.keyUid)
return keycards.len > 0
proc updateKeycardName*(self: Service, keycardUid: string, name: string): bool =
var data = KeycardArgs(
success: false,
keycard: KeycardDto(keycardUid: keycardUid, keycardName: name)
)
try:
let response = backend.setKeycardName(keycardUid, name)
data.success = responseHasNoErrors("updateKeycardName", response)
except Exception as e:
error "error: ", procName="updateKeycardName", errName = e.name, errDesription = e.msg
self.events.emit(SIGNAL_KEYCARD_NAME_CHANGED, data)
return data.success
proc setKeycardLocked*(self: Service, keyUid: string, keycardUid: string): bool =
var data = KeycardArgs(
success: false,
keycard: KeycardDto(keyUid: keyUid, keycardUid: keycardUid)
)
try:
let response = backend.keycardLocked(keycardUid)
data.success = responseHasNoErrors("setKeycardLocked", response)
except Exception as e:
error "error: ", procName="setKeycardLocked", errName = e.name, errDesription = e.msg
self.events.emit(SIGNAL_KEYCARD_LOCKED, data)
return data.success
proc setKeycardUnlocked*(self: Service, keyUid: string, keycardUid: string): bool =
var data = KeycardArgs(
success: false,
keycard: KeycardDto(keyUid: keyUid, keycardUid: keycardUid)
)
try:
let response = backend.keycardUnlocked(keycardUid)
data.success = responseHasNoErrors("setKeycardUnlocked", response)
except Exception as e:
error "error: ", procName="setKeycardUnlocked", errName = e.name, errDesription = e.msg
self.events.emit(SIGNAL_KEYCARD_UNLOCKED, data)
return data.success
proc updateKeycardUid*(self: Service, oldKeycardUid: string, newKeycardUid: string): bool =
var data = KeycardArgs(
success: false,
oldKeycardUid: oldKeycardUid,
keycard: KeycardDto(keycardUid: newKeycardUid)
)
try:
let response = backend.updateKeycardUID(oldKeycardUid, newKeycardUid)
data.success = responseHasNoErrors("updateKeycardUid", response)
except Exception as e:
error "error: ", procName="updateKeycardUid", errName = e.name, errDesription = e.msg
self.events.emit(SIGNAL_KEYCARD_UID_UPDATED, data)
return data.success
proc deleteKeycard*(self: Service, keycardUid: string): bool =
var data = KeycardArgs(
success: false,
keycard: KeycardDto(keycardUid: keycardUid)
)
try:
let response = backend.deleteKeycard(keycardUid)
data.success = responseHasNoErrors("deleteKeycard", response)
except Exception as e:
error "error: ", procName="deleteKeycard", errName = e.name, errDesription = e.msg
self.events.emit(SIGNAL_KEYCARD_DELETED, data)
return data.success
proc deleteAllKeycardsWithKeyUid*(self: Service, keyUid: string): bool =
var data = KeycardArgs(
success: false,
keycard: KeycardDto(keyUid: keyUid)
)
try:
let response = backend.deleteAllKeycardsWithKeyUID(keyUid)
data.success = responseHasNoErrors("deleteAllKeycardsWithKeyUid", response)
except Exception as e:
error "error: ", procName="deleteAllKeycardsWithKeyUid", errName = e.name, errDesription = e.msg
self.events.emit(SIGNAL_ALL_KEYCARDS_DELETED, data)
return data.success

View File

@ -0,0 +1,178 @@
proc storeTokensForAccount*(self: Service, address: string, tokens: seq[WalletTokenDto], areBalancesCached: bool, areMarketValuesCached: bool) =
let acc = self.getAccountByAddress(address)
if acc.isNil:
return
self.accountsTokens[address] = tokens
acc.hasBalanceCache = areBalancesCached
acc.hasMarketValuesCache = areMarketValuesCached
proc updateReceivedTokens*(self: Service, address: string, tokens: var seq[WalletTokenDto]) =
let acc = self.getAccountByAddress(address)
if acc.isNil or not self.accountsTokens.hasKey(address):
return
let allBalancesForAllTokensHaveError = allBalancesForAllTokensHaveError(tokens)
let allMarketValuesForAllTokensHaveError = allMarketValuesForAllTokensHaveError(tokens)
for storedToken in self.accountsTokens[address]:
for token in tokens.mitems:
if storedToken.name == token.name:
if allBalancesForAllTokensHaveError:
token.balancesPerChain = storedToken.balancesPerChain
if allMarketValuesForAllTokensHaveError:
token.marketValuesPerCurrency = storedToken.marketValuesPerCurrency
proc getTokensByAddress*(self: Service, address: string): seq[WalletTokenDto] =
if not self.accountsTokens.hasKey(address):
return
return self.accountsTokens[address]
proc getTokensByAddresses*(self: Service, addresses: seq[string]): seq[WalletTokenDto] =
var tokens = initTable[string, WalletTokenDto]()
for address in addresses:
if not self.accountsTokens.hasKey(address):
continue
for token in self.accountsTokens[address]:
if not tokens.hasKey(token.symbol):
let newToken = token.copyToken()
tokens[token.symbol] = newToken
continue
for chainId, balanceDto in token.balancesPerChain:
if not tokens[token.symbol].balancesPerChain.hasKey(chainId):
tokens[token.symbol].balancesPerChain[chainId] = balanceDto
continue
tokens[token.symbol].balancesPerChain[chainId].balance += balanceDto.balance
result = toSeq(tokens.values)
result.sort(priorityTokenCmp)
proc onAllTokensBuilt*(self: Service, response: string) {.slot.} =
try:
var visibleSymbols: seq[string]
let chainIds = self.networkService.getNetworks().map(n => n.chainId)
let responseObj = response.parseJson
var storeResult: bool
var resultObj: JsonNode
discard responseObj.getProp("storeResult", storeResult)
discard responseObj.getProp("result", resultObj)
var data = TokensPerAccountArgs()
data.accountsTokens = initOrderedTable[string, seq[WalletTokenDto]]()
data.hasBalanceCache = false
data.hasMarketValuesCache = false
if resultObj.kind == JObject:
for wAddress, tokensDetailsObj in resultObj:
if tokensDetailsObj.kind == JArray:
var tokens: seq[WalletTokenDto]
tokens = map(tokensDetailsObj.getElems(), proc(x: JsonNode): WalletTokenDto = x.toWalletTokenDto())
tokens.sort(priorityTokenCmp)
self.updateReceivedTokens(wAddress, tokens)
let hasBalanceCache = anyTokenHasBalanceForAnyChain(tokens)
let hasMarketValuesCache = anyTokenHasMarketValuesForAnyChain(tokens)
data.accountsTokens[wAddress] = @[]
deepCopy(data.accountsTokens[wAddress], tokens)
data.hasBalanceCache = data.hasBalanceCache or hasBalanceCache
data.hasMarketValuesCache = data.hasMarketValuesCache or hasMarketValuesCache
# set assetsLoading to false once the tokens are loaded
self.updateAssetsLoadingState(wAddress, false)
if storeResult:
self.storeTokensForAccount(wAddress, tokens, hasBalanceCache, hasMarketValuesCache)
self.tokenService.updateTokenPrices(tokens) # For efficiency. Will be removed when token info fetching gets moved to the tokenService
# Gather symbol for visible tokens
for token in tokens:
if token.getVisibleForNetworkWithPositiveBalance(chainIds) and find(visibleSymbols, token.symbol) == -1:
visibleSymbols.add(token.symbol)
self.events.emit(SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT, data)
if visibleSymbols.len > 0:
discard backend.updateVisibleTokens(visibleSymbols)
except Exception as e:
error "error: ", procName="onAllTokensBuilt", errName = e.name, errDesription = e.msg
proc buildAllTokens(self: Service, accounts: seq[string], store: bool) =
if not main_constants.WALLET_ENABLED or
accounts.len == 0:
return
# set assetsLoading to true as the tokens are being loaded
for waddress in accounts:
self.updateAssetsLoadingState(waddress, true)
let arg = BuildTokensTaskArg(
tptr: cast[ByteAddress](prepareTokensTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAllTokensBuilt",
accounts: accounts,
storeResult: store
)
self.threadpool.start(arg)
proc checkRecentHistory*(self: Service, addresses: seq[string]) =
if(not main_constants.WALLET_ENABLED):
return
try:
let chainIds = self.networkService.getNetworks().map(a => a.chainId)
status_go_transactions.checkRecentHistory(chainIds, addresses)
except Exception as e:
let errDescription = e.msg
error "error: ", errDescription
proc reloadAccountTokens*(self: Service) =
let addresses = self.getWalletAddresses()
self.buildAllTokens(addresses, store = true)
self.checkRecentHistory(addresses)
proc getCurrency*(self: Service): string =
return self.settingsService.getCurrency()
proc getCurrentCurrencyIfEmpty(self: Service, currency = ""): string =
if currency != "":
return currency
else:
return self.getCurrency()
proc getCurrencyBalance*(self: Service, address: string, chainIds: seq[int], currency: string): float64 =
if not self.accountsTokens.hasKey(address):
return
return self.accountsTokens[address].map(t => t.getCurrencyBalance(chainIds, currency)).foldl(a + b, 0.0)
proc getTotalCurrencyBalance*(self: Service, addresses: seq[string], currency: string = ""): float64 =
let chainIds = self.networkService.getNetworks().filter(a => a.enabled).map(a => a.chainId)
let accounts = self.getWalletAccounts().filter(w => addresses.contains(w.address))
return accounts.map(a => self.getCurrencyBalance(a.address, chainIds, self.getCurrentCurrencyIfEmpty(currency))).foldl(a + b, 0.0)
proc findTokenSymbolByAddress*(self: Service, address: string): string =
return self.tokenService.findTokenSymbolByAddress(address)
proc getOrFetchBalanceForAddressInPreferredCurrency*(self: Service, address: string): tuple[balance: float64, fetched: bool] =
let acc = self.getAccountByAddress(address)
if acc.isNil:
self.buildAllTokens(@[address], store = false)
result.balance = 0.0
result.fetched = false
return
let chainIds = self.networkService.getNetworks().map(n => n.chainId)
result.balance = self.getCurrencyBalance(acc.address, chainIds, self.getCurrentCurrencyIfEmpty())
result.fetched = true
proc getTokenBalanceOnChain*(self: Service, address: string, chainId: int, symbol: string): float64 =
if not self.accountsTokens.hasKey(address):
return 0.0
for token in self.accountsTokens[address]:
if token.symbol == symbol and token.balancesPerChain.hasKey(chainId):
return token.balancesPerChain[chainId].balance
return 0.0
proc allAccountsTokenBalance*(self: Service, symbol: string): float64 =
var totalTokenBalance = 0.0
for walletAccount in self.getWalletAccounts():
if walletAccount.walletType == WalletTypeWatch or
not self.accountsTokens.hasKey(walletAccount.address):
continue
for token in self.accountsTokens[walletAccount.address]:
if token.symbol == symbol:
totalTokenBalance += token.getTotalBalanceOfSupportedChains()
return totalTokenBalance

View File

@ -0,0 +1,66 @@
#################################################
# Signals emitted by wallet account service
#################################################
const SIGNAL_WALLET_ACCOUNT_SAVED* = "walletAccount/accountSaved"
const SIGNAL_WALLET_ACCOUNT_DELETED* = "walletAccount/accountDeleted"
const SIGNAL_WALLET_ACCOUNT_UPDATED* = "walletAccount/walletAccountUpdated"
const SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED* = "walletAccount/networkEnabledUpdated"
const SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT* = "walletAccount/tokensRebuilt"
const SIGNAL_WALLET_ACCOUNT_TOKENS_BEING_FETCHED* = "walletAccount/tokenFetching"
const SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESSES_FETCHED* = "walletAccount/derivedAddressesFetched"
const SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESSES_FROM_MNEMONIC_FETCHED* = "walletAccount/derivedAddressesFromMnemonicFetched"
const SIGNAL_WALLET_ACCOUNT_ADDRESS_DETAILS_FETCHED* = "walletAccount/addressDetailsFetched"
const SIGNAL_WALLET_ACCOUNT_POSITION_UPDATED* = "walletAccount/positionUpdated"
const SIGNAL_WALLET_ACCOUNT_OPERABILITY_UPDATED* = "walletAccount/operabilityUpdated"
const SIGNAL_WALLET_ACCOUNT_CHAIN_ID_FOR_URL_FETCHED* = "walletAccount/chainIdForUrlFetched"
const SIGNAL_WALLET_ACCOUNT_PREFERRED_SHARING_CHAINS_UPDATED* = "walletAccount/preferredSharingChainsUpdated"
const SIGNAL_KEYPAIR_SYNCED* = "keypairSynced"
const SIGNAL_KEYPAIR_NAME_CHANGED* = "keypairNameChanged"
const SIGNAL_NEW_KEYCARD_SET* = "newKeycardSet"
const SIGNAL_KEYCARD_REBUILD* = "keycardRebuild"
const SIGNAL_KEYCARD_DELETED* = "keycardDeleted"
const SIGNAL_ALL_KEYCARDS_DELETED* = "allKeycardsDeleted"
const SIGNAL_KEYCARD_ACCOUNTS_REMOVED* = "keycardAccountsRemoved"
const SIGNAL_KEYCARD_LOCKED* = "keycardLocked"
const SIGNAL_KEYCARD_UNLOCKED* = "keycardUnlocked"
const SIGNAL_KEYCARD_UID_UPDATED* = "keycardUidUpdated"
const SIGNAL_KEYCARD_NAME_CHANGED* = "keycardNameChanged"
#################################################
# Payload sent via above defined signals
#################################################
type AccountArgs* = ref object of Args
account*: WalletAccountDto
type KeypairArgs* = ref object of Args
keypair*: KeypairDto
oldKeypairName*: string
type KeycardArgs* = ref object of Args
success*: bool
oldKeycardUid*: string
keycard*: KeycardDto
type DerivedAddressesArgs* = ref object of Args
uniqueId*: string
derivedAddresses*: seq[DerivedAddressDto]
error*: string
type TokensPerAccountArgs* = ref object of Args
accountsTokens*: OrderedTable[string, seq[WalletTokenDto]] # [wallet address, list of tokens]
hasBalanceCache*: bool
hasMarketValuesCache*: bool
type KeycardActivityArgs* = ref object of Args
success*: bool
oldKeycardUid*: string
keycard*: KeycardDto
type ChainIdForUrlArgs* = ref object of Args
chainId*: int
success*: bool
url*: string

View File

@ -0,0 +1,109 @@
#################################################
# Common functions
#################################################
proc priorityTokenCmp(a, b: WalletTokenDto): int =
for symbol in @["ETH", "SNT", "DAI", "STT"]:
if a.symbol == symbol:
return -1
if b.symbol == symbol:
return 1
cmp(a.name, b.name)
proc walletAccountsCmp(x, y: WalletAccountDto): int =
cmp(x.position, y.position)
proc hex2Balance*(input: string, decimals: int): string =
var value = fromHex(Stuint[256], input)
if decimals == 0:
return fmt"{value}"
var p = u256(10).pow(decimals)
var i = value.div(p)
var r = value.mod(p)
var leading_zeros = "0".repeat(decimals - ($r).len)
var d = fmt"{leading_zeros}{$r}"
result = $i
if(r > 0): result = fmt"{result}.{d}"
proc responseHasNoErrors(procName: string, response: RpcResponse[JsonNode]): bool =
var errMsg = ""
if not response.error.isNil:
errMsg = "(" & $response.error.code & ") " & response.error.message
elif response.result.kind == JObject and response.result.contains("error"):
errMsg = response.result["error"].getStr
if(errMsg.len == 0):
return true
error "error: ", procName=procName, errDesription = errMsg
return false
proc allBalancesForAllTokensHaveError(tokens: seq[WalletTokenDto]): bool =
for token in tokens:
for chainId, balanceDto in token.balancesPerChain:
if not balanceDto.hasError:
return false
return true
proc anyTokenHasBalanceForAnyChain(tokens: seq[WalletTokenDto]): bool =
for token in tokens:
if len(token.balancesPerChain) > 0:
return true
return false
proc allMarketValuesForAllTokensHaveError(tokens: seq[WalletTokenDto]): bool =
for token in tokens:
for currency, marketDto in token.marketValuesPerCurrency:
if not marketDto.hasError:
return false
return true
proc anyTokenHasMarketValuesForAnyChain(tokens: seq[WalletTokenDto]): bool =
for token in tokens:
if len(token.marketValuesPerCurrency) > 0:
return true
return false
#################################################
# Remote functions
#################################################
proc getAccountsFromDb(): seq[WalletAccountDto] =
try:
let response = status_go_accounts.getAccounts()
return response.result.getElems().map(
x => x.toWalletAccountDto()
).filter(a => not a.isChat)
except Exception as e:
error "error: ", procName="getAccounts", errName = e.name, errDesription = e.msg
proc getWatchOnlyAccountsFromDb(): seq[WalletAccountDto] =
try:
let response = status_go_accounts.getWatchOnlyAccounts()
return response.result.getElems().map(x => x.toWalletAccountDto())
except Exception as e:
error "error: ", procName="getWatchOnlyAccounts", errName = e.name, errDesription = e.msg
proc getKeypairsFromDb(): seq[KeypairDto] =
try:
let response = status_go_accounts.getKeypairs()
return response.result.getElems().map(x => x.toKeypairDto())
except Exception as e:
error "error: ", procName="getKeypairs", errName = e.name, errDesription = e.msg
proc getKeypairByKeyUidFromDb(keyUid: string): KeypairDto =
if keyUid.len == 0:
return
try:
let response = status_go_accounts.getKeypairByKeyUid(keyUid)
if not response.error.isNil:
return
return response.result.toKeypairDto()
except Exception as e:
info "no known keypair", keyUid=keyUid, procName="getKeypairByKeyUid", errName = e.name, errDesription = e.msg
proc getEnsName(address: string, chainId: int): string =
try:
let response = backend.getName(chainId, address)
return response.result.getStr
except Exception as e:
let errDesription = e.msg
error "error: ", errDesription