fix: loading of wallet history, display of tx datetime

Fixes: #2539.

Transaction history is now correctly being fetched from status-go as per mobile. Firstly, when accounts are added (ie as watch accounts), `wallet_checkRecentHistory` must be called first so that the status-go db is populated. After that, `wallet_getTransfersByAddress` can be called. On app load, when we run the `initBalance` logic, we are calling `wallet_getTransfersByAddress`, asking for the last 20 transactions with the `loadMore` parameter set to false. When the user navigates to the Wallet > History tab, they can then click “Load More” to fetch more transactions from status-go. Once the number of transactions returns false below the expected amount, the remaining transactions to fetch have been exhausted and the “Load More” button is disabled.

feat: add non-archival node warning to the UI to indicate to the user that they may not have complete results

feat: set active account to the added account
Once an account is added to the wallet, that newly added account is selected as the active account.

1. The “load more” button is active when new transactions that aren’t already displayed are returned from `wallet_getTransfersByAddress`. This is the only way to enable or disable the “Load more” button as status-go is not able to return information regarding whether or not there are more transactions to be fetched. The downside to this is that lets say the last page of transactions is returned, but there are no more pages left. These returned txs are not currently displayed, so the “load more” button will still be enabled. However, the next click of the button will return 0 results, thus disabling it. It’s effectively an extra click to get to the disabled state.
2. For more information on how the `toBlock` parameter operates for the `wallet_getTransfersForAddress` RPC call, see https://notes.status.im/XmENTrVRRaqhwE4gK0m8Mg?view.
This commit is contained in:
Eric Mastro 2021-06-01 00:03:41 +10:00 committed by Eric Mastro
parent 21e6affa98
commit 0b0a542828
23 changed files with 311 additions and 174 deletions

View File

@ -288,12 +288,6 @@ returns `true` if the specified address is in the list of default or custom (use
Returns stringified JSON result of the decoding. The JSON will contain only an `error` field if there was an error during decoding. Otherwise, it will contain a `symbol` (the token symbol) and an `amount` (amount approved to spend) field. Returns stringified JSON result of the decoding. The JSON will contain only an `error` field if there was an error during decoding. Otherwise, it will contain a `symbol` (the token symbol) and an `amount` (amount approved to spend) field.
#### `isHistoryFetched(address: string)` : `bool`
* `address` (`string`): address of the account to check
returns `true` if `status-go` has returned transfer history for the specified account (result of `wallet_getTransfersByAddress`)
#### `loadTransactionsForAccount(address: string)` : `void` #### `loadTransactionsForAccount(address: string)` : `void`
* `address` (`string`): address of the account to load transactions for * `address` (`string`): address of the account to load transactions for
@ -343,7 +337,6 @@ The `walletModel` exposes the following signals, which can be consumed in QML us
| `transactionWasSent` | `txResult` (`string`): JSON stringified result of sending a transaction | fired when accounts on the node have chagned | | `transactionWasSent` | `txResult` (`string`): JSON stringified result of sending a transaction | fired when accounts on the node have chagned |
| `defaultCurrencyChanged` | none | fired when the user's default currency has chagned | | `defaultCurrencyChanged` | none | fired when the user's default currency has chagned |
| `gasPricePredictionsChanged` | none | fired when the gas price predictions have changed, typically after getting a gas price prediction response | | `gasPricePredictionsChanged` | none | fired when the gas price predictions have changed, typically after getting a gas price prediction response |
| `historyWasFetched` | none | fired when `status-go` completes fetching of transaction history |
| `loadingTrxHistoryChanged` | `isLoading` (`bool`): `true` if the transaction history is loading | fired when the loading of transfer history starts and completes | | `loadingTrxHistoryChanged` | `isLoading` (`bool`): `true` if the transaction history is loading | fired when the loading of transfer history starts and completes |
| `ensWasResolved` | `resolvedAddress` (`string`): address resolved from the ENS name<br>`uuid` (`string`): unique identifier that was used to identify the request in QML so that only specific components can respond when needed | fired when an ENS name was resolved | | `ensWasResolved` | `resolvedAddress` (`string`): address resolved from the ENS name<br>`uuid` (`string`): unique identifier that was used to identify the request in QML so that only specific components can respond when needed | fired when an ENS name was resolved |
| `transactionCompleted` | `success` (`bool`): `true` if the transaction was successful<br>`txHash` (`string`): has of the transaction<br>`revertReason` (`string`): reason transaction was reverted (if provided and if the transaction was reverted) | fired when a tracked transction (from the wallet or ENS) was completed | | `transactionCompleted` | `success` (`bool`): `true` if the transaction was successful<br>`txHash` (`string`): has of the transaction<br>`revertReason` (`string`): reason transaction was reverted (if provided and if the transaction was reverted) | fired when a tracked transction (from the wallet or ENS) was completed |

View File

@ -19,7 +19,6 @@ The `walletModel` exposes the following signals, which can be consumed in QML us
| `transactionWasSent` | `txResult` (`string`): JSON stringified result of sending a transaction | fired when accounts on the node have chagned | | `transactionWasSent` | `txResult` (`string`): JSON stringified result of sending a transaction | fired when accounts on the node have chagned |
| `defaultCurrencyChanged` | none | fired when the user's default currency has chagned | | `defaultCurrencyChanged` | none | fired when the user's default currency has chagned |
| `gasPricePredictionsChanged` | none | fired when the gas price predictions have changed, typically after getting a gas price prediction response | | `gasPricePredictionsChanged` | none | fired when the gas price predictions have changed, typically after getting a gas price prediction response |
| `historyWasFetched` | none | fired when `status-go` completes fetching of transaction history |
| `loadingTrxHistoryChanged` | `isLoading` (`bool`): `true` if the transaction history is loading | fired when the loading of transfer history starts and completes | | `loadingTrxHistoryChanged` | `isLoading` (`bool`): `true` if the transaction history is loading | fired when the loading of transfer history starts and completes |
| `ensWasResolved` | `resolvedAddress` (`string`): address resolved from the ENS name<br>`uuid` (`string`): unique identifier that was used to identify the request in QML so that only specific components can respond when needed | fired when an ENS name was resolved | | `ensWasResolved` | `resolvedAddress` (`string`): address resolved from the ENS name<br>`uuid` (`string`): unique identifier that was used to identify the request in QML so that only specific components can respond when needed | fired when an ENS name was resolved |
| `transactionCompleted` | `success` (`bool`): `true` if the transaction was successful<br>`txHash` (`string`): has of the transaction<br>`revertReason` (`string`): reason transaction was reverted (if provided and if the transaction was reverted) | fired when a tracked transction (from the wallet or ENS) was completed | | `transactionCompleted` | `success` (`bool`): `true` if the transaction was successful<br>`txHash` (`string`): has of the transaction<br>`revertReason` (`string`): reason transaction was reverted (if provided and if the transaction was reverted) | fired when a tracked transction (from the wallet or ENS) was completed |
@ -100,7 +99,6 @@ Methods can be invoked by calling them directly on the `walletModel`, ie `wallet
| `isFetchingHistory` | `address` (`string`): address of the account to check | `bool` | returns `true` if `status-go` is currently fetching the transaction history for the specified account | | `isFetchingHistory` | `address` (`string`): address of the account to check | `bool` | returns `true` if `status-go` is currently fetching the transaction history for the specified account |
| `isKnownTokenContract` | `address` (`string`): contract address | `bool` | returns `true` if the specified address is in the list of default or custom (user-added) contracts | | `isKnownTokenContract` | `address` (`string`): contract address | `bool` | returns `true` if the specified address is in the list of default or custom (user-added) contracts |
| `decodeTokenApproval` | `tokenAddress` (`string`): contract address<br>`data` (`string`): response received from the ERC-20 token `Approve` function call | `string` | Returns stringified JSON result of the decoding. The JSON will contain only an `error` field if there was an error during decoding. Otherwise, it will contain a `symbol` (the token symbol) and an `amount` (amount approved to spend) field. | | `decodeTokenApproval` | `tokenAddress` (`string`): contract address<br>`data` (`string`): response received from the ERC-20 token `Approve` function call | `string` | Returns stringified JSON result of the decoding. The JSON will contain only an `error` field if there was an error during decoding. Otherwise, it will contain a `symbol` (the token symbol) and an `amount` (amount approved to spend) field. |
| `isHistoryFetched` | `address` (`string`): address of the account to check | `bool` | returns `true` if `status-go` has returned transfer history for the specified account (result of `wallet_getTransfersByAddress`) |
| `loadTransactionsForAccount` | `address` (`string`): address of the account to load transactions for | `void` | loads the transfer history for the specified account (result of `wallet_getTransfersByAddress`) in a separate thread | | `loadTransactionsForAccount` | `address` (`string`): address of the account to load transactions for | `void` | loads the transfer history for the specified account (result of `wallet_getTransfersByAddress`) in a separate thread |
| `setTrxHistoryResult` | `historyJSON` (`string`): stringified JSON result from `status-go`'s response to `wallet_getTransfersByAddress` | `void` | sets the transaction history for the account requested. If the requested account was tracked by the `walletModel`, it will have its transactions updated (including `currentAccount`). The `loadingTrxHistoryChanged` signal is also fired with `false` as a parameter. | | `setTrxHistoryResult` | `historyJSON` (`string`): stringified JSON result from `status-go`'s response to `wallet_getTransfersByAddress` | `void` | sets the transaction history for the account requested. If the requested account was tracked by the `walletModel`, it will have its transactions updated (including `currentAccount`). The `loadingTrxHistoryChanged` signal is also fired with `false` as a parameter. |
| `resolveENS` | `ens` (`string`): the ENS name to resolve | `void` | resolves an ENS name in a separate thread | | `resolveENS` | `ens` (`string`): the ENS name to resolve | `void` | resolves an ENS name in a separate thread |

View File

@ -87,7 +87,7 @@ QtObject:
# somehow this value crashes the app # somehow this value crashes the app
if value == "0x0": if value == "0x0":
return "0" return "0"
return stripTrailingZeroes(stint.toString(stint.fromHex(StUint[256], value))) return $stint.fromHex(StUint[256], value)
proc urlFromUserInput*(self: UtilsView, input: string): string {.slot.} = proc urlFromUserInput*(self: UtilsView, input: string): string {.slot.} =
result = url_fromUserInput(input) result = url_fromUserInput(input)

View File

@ -1,4 +1,4 @@
import NimQml, strformat, strutils, chronicles import NimQml, strformat, strutils, chronicles, sugar, sequtils
import view import view
import views/[asset_list, account_list, account_item] import views/[asset_list, account_list, account_item]
@ -34,7 +34,7 @@ proc init*(self: WalletController) =
for account in accounts: for account in accounts:
self.view.addAccountToList(account) self.view.addAccountToList(account)
self.view.initBalances() self.view.checkRecentHistory()
self.view.setDappBrowserAddress() self.view.setDappBrowserAddress()
self.status.events.on("accountsUpdated") do(e: Args): self.status.events.on("accountsUpdated") do(e: Args):
@ -63,14 +63,16 @@ proc init*(self: WalletController) =
# TODO: show notification # TODO: show notification
of "new-transfers": of "new-transfers":
for acc in data.accounts: self.view.initBalances(data.accounts)
self.view.loadTransactionsForAccount(acc)
self.view.initBalances(false)
of "recent-history-fetching": of "recent-history-fetching":
self.view.setHistoryFetchState(data.accounts, true) self.view.setHistoryFetchState(data.accounts, true)
of "recent-history-ready": of "recent-history-ready":
self.view.initBalances(data.accounts)
self.view.setHistoryFetchState(data.accounts, false) self.view.setHistoryFetchState(data.accounts, false)
self.view.initBalances(false) of "non-archival-node-detected":
self.view.setHistoryFetchState(self.status.wallet.accounts.map(account => account.address), false)
self.view.setNonArchivalNode()
error "Non-archival node detected, please check your Infura key or your connected node"
else: else:
error "Unhandled wallet signal", eventType=data.eventType error "Unhandled wallet signal", eventType=data.eventType

View File

@ -1,5 +1,6 @@
import # std libs import # std libs
atomics, strformat, strutils, sequtils, json, std/wrapnils, parseUtils, tables algorithm, atomics, sequtils, strformat, strutils, sugar, sequtils, json,
parseUtils, std/wrapnils, tables
import # vendor libs import # vendor libs
NimQml, chronicles, stint NimQml, chronicles, stint
@ -36,7 +37,9 @@ type
GasPredictionsTaskArg = ref object of QObjectTaskArg GasPredictionsTaskArg = ref object of QObjectTaskArg
LoadTransactionsTaskArg = ref object of QObjectTaskArg LoadTransactionsTaskArg = ref object of QObjectTaskArg
address: string address: string
blockNumber: string toBlock: Uint256
limit: int
loadMore: bool
ResolveEnsTaskArg = ref object of QObjectTaskArg ResolveEnsTaskArg = ref object of QObjectTaskArg
ens: string ens: string
uuid: string uuid: string
@ -144,16 +147,20 @@ const loadTransactionsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.}
arg = decode[LoadTransactionsTaskArg](argEncoded) arg = decode[LoadTransactionsTaskArg](argEncoded)
output = %*{ output = %*{
"address": arg.address, "address": arg.address,
"history": getTransfersByAddress(arg.address) "history": getTransfersByAddress(arg.address, arg.toBlock, arg.limit, arg.loadMore),
"loadMore": arg.loadMore
} }
arg.finish(output) arg.finish(output)
proc loadTransactions[T](self: T, slot: string, address: string) = proc loadTransactions[T](self: T, slot: string, address: string, toBlock: Uint256, limit: int, loadMore: bool) =
let arg = LoadTransactionsTaskArg( let arg = LoadTransactionsTaskArg(
tptr: cast[ByteAddress](loadTransactionsTask), tptr: cast[ByteAddress](loadTransactionsTask),
vptr: cast[ByteAddress](self.vptr), vptr: cast[ByteAddress](self.vptr),
slot: slot, slot: slot,
address: address address: address,
toBlock: toBlock,
limit: limit,
loadMore: loadMore
) )
self.status.tasks.threadpool.start(arg) self.status.tasks.threadpool.start(arg)
@ -211,6 +218,7 @@ QtObject:
defaultGasLimit: string defaultGasLimit: string
signingPhrase: string signingPhrase: string
fetchingHistoryState: Table[string, bool] fetchingHistoryState: Table[string, bool]
isNonArchivalNode: bool
proc delete(self: WalletView) = proc delete(self: WalletView) =
self.accounts.delete self.accounts.delete
@ -249,6 +257,7 @@ QtObject:
result.defaultGasLimit = "21000" result.defaultGasLimit = "21000"
result.signingPhrase = "" result.signingPhrase = ""
result.fetchingHistoryState = initTable[string, bool]() result.fetchingHistoryState = initTable[string, bool]()
result.isNonArchivalNode = false
result.setup result.setup
proc etherscanLinkChanged*(self: WalletView) {.signal.} proc etherscanLinkChanged*(self: WalletView) {.signal.}
@ -325,7 +334,8 @@ QtObject:
self.setCurrentCollectiblesLists(selectedAccount.collectiblesLists) self.setCurrentCollectiblesLists(selectedAccount.collectiblesLists)
self.loadCollectiblesForAccount(selectedAccount.address, selectedAccount.collectiblesLists) self.loadCollectiblesForAccount(selectedAccount.address, selectedAccount.collectiblesLists)
self.setCurrentTransactions(selectedAccount.transactions) self.currentTransactions.setHasMore(selectedAccount.transactions.hasMore)
self.setCurrentTransactions(selectedAccount.transactions.data)
proc getCurrentAccount*(self: WalletView): QVariant {.slot.} = proc getCurrentAccount*(self: WalletView): QVariant {.slot.} =
result = newQVariant(self.currentAccount) result = newQVariant(self.currentAccount)
@ -616,6 +626,18 @@ QtObject:
self.loadCollectibles("setCollectiblesResult", address, collectibleType) self.loadCollectibles("setCollectiblesResult", address, collectibleType)
self.currentCollectiblesLists.setLoadingByType(collectibleType, 1) self.currentCollectiblesLists.setLoadingByType(collectibleType, 1)
proc isNonArchivalNodeChanged*(self: WalletView) {.signal.}
proc setNonArchivalNode*(self: WalletView, isNonArchivalNode: bool = true) {.slot.} =
self.isNonArchivalNode = isNonArchivalNode
self.isNonArchivalNodeChanged()
proc isNonArchivalNode*(self: WalletView): bool {.slot.} = result = ?.self.isNonArchivalNode
QtProperty[bool] isNonArchivalNode:
read = isNonArchivalNode
write = setNonArchivalNode
notify = isNonArchivalNodeChanged
proc gasPricePredictionsChanged*(self: WalletView) {.signal.} proc gasPricePredictionsChanged*(self: WalletView) {.signal.}
proc getGasPricePredictions*(self: WalletView) {.slot.} = proc getGasPricePredictions*(self: WalletView) {.slot.} =
@ -685,50 +707,78 @@ QtObject:
return """{"error":"Unknown token address"}"""; return """{"error":"Unknown token address"}""";
proc historyWasFetched*(self: WalletView) {.signal.} proc isFetchingHistory*(self: WalletView): bool {.slot.} =
if self.fetchingHistoryState.hasKey(self.currentAccount.address):
return self.fetchingHistoryState[self.currentAccount.address]
return false
proc loadingTrxHistoryChanged*(self: WalletView, isLoading: bool, address: string) {.signal.}
proc loadTransactionsForAccount*(self: WalletView, address: string, toBlock: string = "0x0", limit: int = 20, loadMore: bool = false) {.slot.} =
self.loadingTrxHistoryChanged(true, address)
let toBlockParsed = stint.fromHex(Uint256, toBlock)
self.loadTransactions("setTrxHistoryResult", address, toBlockParsed, limit, loadMore)
proc setHistoryFetchState*(self: WalletView, accounts: seq[string], isFetching: bool) = proc setHistoryFetchState*(self: WalletView, accounts: seq[string], isFetching: bool) =
for acc in accounts: for acc in accounts:
self.fetchingHistoryState[acc] = isFetching self.fetchingHistoryState[acc] = isFetching
if not isFetching: self.historyWasFetched() self.loadingTrxHistoryChanged(isFetching, acc)
proc isFetchingHistory*(self: WalletView, address: string): bool {.slot.} = proc initBalance(self: WalletView, acc: WalletAccount, loadTransactions: bool = true) =
if self.fetchingHistoryState.hasKey(address): let
return self.fetchingHistoryState[address] accountAddress = acc.address
return true tokenList = acc.assetList.filter(proc(x:Asset): bool = x.address != "").map(proc(x: Asset): string = x.address)
self.initBalances("getAccountBalanceSuccess", accountAddress, tokenList)
if loadTransactions:
self.loadTransactionsForAccount(accountAddress)
proc isHistoryFetched*(self: WalletView, address: string): bool {.slot.} = proc initBalance(self: WalletView, accountAddress: string, loadTransactions: bool = true) =
return self.currentTransactions.rowCount() > 0 var found = false
let acc = self.status.wallet.accounts.find(acc => acc.address.toLowerAscii == accountAddress.toLowerAscii, found)
proc loadingTrxHistoryChanged*(self: WalletView, isLoading: bool) {.signal.} if not found:
error "Failed to init balance: could not find account", account=accountAddress
proc loadTransactionsForAccount*(self: WalletView, address: string) {.slot.} = return
self.loadingTrxHistoryChanged(true) self.initBalance(acc, loadTransactions)
self.loadTransactions("setTrxHistoryResult", address)
proc getLatestTransactionHistory*(self: WalletView, accounts: seq[string]) =
for acc in accounts:
self.loadTransactionsForAccount(acc)
proc initBalances*(self: WalletView, loadTransactions: bool = true) = proc initBalances*(self: WalletView, loadTransactions: bool = true) =
for acc in self.status.wallet.accounts: for acc in self.status.wallet.accounts:
let accountAddress = acc.address self.initBalance(acc, loadTransactions)
let tokenList = acc.assetList.filter(proc(x:Asset): bool = x.address != "").map(proc(x: Asset): string = x.address)
self.initBalances("getAccountBalanceSuccess", accountAddress, tokenList) proc initBalances*(self: WalletView, accounts: seq[string], loadTransactions: bool = true) =
if loadTransactions: for acc in accounts:
self.loadTransactionsForAccount(accountAddress) self.initBalance(acc, loadTransactions)
proc setTrxHistoryResult(self: WalletView, historyJSON: string) {.slot.} = proc setTrxHistoryResult(self: WalletView, historyJSON: string) {.slot.} =
let historyData = parseJson(historyJSON) let
let transactions = historyData["history"].to(seq[Transaction]); historyData = parseJson(historyJSON)
let address = historyData["address"].getStr transactions = historyData["history"].to(seq[Transaction])
let index = self.accounts.getAccountindexByAddress(address) address = historyData["address"].getStr
wasFetchMore = historyData["loadMore"].getBool
isCurrentAccount = address.toLowerAscii == self.currentAccount.address.toLowerAscii
index = self.accounts.getAccountindexByAddress(address)
if index == -1: return if index == -1: return
self.accounts.getAccount(index).transactions = transactions
if address == self.currentAccount.address: let account = self.accounts.getAccount(index)
self.setCurrentTransactions( # concatenate the new page of txs to existing account transactions,
self.accounts.getAccount(index).transactions) # sort them by block number and nonce, then deduplicate them based on their
self.loadingTrxHistoryChanged(false) # transaction id.
let existingAcctTxIds = account.transactions.data.map(tx => tx.id)
let hasNewTxs = transactions.len > 0 and transactions.any(tx => not existingAcctTxIds.contains(tx.id))
if hasNewTxs or not wasFetchMore:
var allTxs: seq[Transaction] = account.transactions.data.concat(transactions)
allTxs.sort(cmpTransactions, SortOrder.Descending)
allTxs.deduplicate(tx => tx.id)
account.transactions.data = allTxs
account.transactions.hasMore = true
if isCurrentAccount:
self.currentTransactions.setHasMore(true)
self.setCurrentTransactions(allTxs)
else:
account.transactions.hasMore = false
if isCurrentAccount:
self.currentTransactions.setHasMore(false)
self.currentTransactionsChanged()
self.loadingTrxHistoryChanged(false, address)
proc resolveENS*(self: WalletView, ens: string, uuid: string) {.slot.} = proc resolveENS*(self: WalletView, ens: string, uuid: string) {.slot.} =
self.resolveEns("ensResolved", ens, uuid) self.resolveEns("ensResolved", ens, uuid)

View File

@ -1,4 +1,4 @@
import NimQml, Tables, random, strformat, json_serialization import NimQml, Tables, random, strformat, strutils, json_serialization
import sequtils as sequtils import sequtils as sequtils
import account_item, asset_list import account_item, asset_list
from ../../../status/wallet import WalletAccount, Asset, CollectibleList from ../../../status/wallet import WalletAccount, Asset, CollectibleList
@ -54,7 +54,7 @@ QtObject:
proc getAccountindexByAddress*(self: AccountList, address: string): int = proc getAccountindexByAddress*(self: AccountList, address: string): int =
var i = 0 var i = 0
for accountView in self.accounts: for accountView in self.accounts:
if (accountView.account.address == address): if (accountView.account.address.toLowerAscii == address.toLowerAscii):
return i return i
i = i + 1 i = i + 1
return -1 return -1

View File

@ -21,6 +21,7 @@ type
QtObject: QtObject:
type TransactionList* = ref object of QAbstractListModel type TransactionList* = ref object of QAbstractListModel
transactions*: seq[Transaction] transactions*: seq[Transaction]
hasMore*: bool
proc setup(self: TransactionList) = self.QAbstractListModel.setup proc setup(self: TransactionList) = self.QAbstractListModel.setup
@ -31,14 +32,29 @@ QtObject:
proc newTransactionList*(): TransactionList = proc newTransactionList*(): TransactionList =
new(result, delete) new(result, delete)
result.transactions = @[] result.transactions = @[]
result.hasMore = true
result.setup result.setup
proc getLastTxBlockNumber*(self: TransactionList): string = proc getLastTxBlockNumber*(self: TransactionList): string {.slot.} =
return self.transactions[^1].blockNumber return self.transactions[^1].blockNumber
method rowCount*(self: TransactionList, index: QModelIndex = nil): int = method rowCount*(self: TransactionList, index: QModelIndex = nil): int =
return self.transactions.len return self.transactions.len
proc hasMoreChanged*(self: TransactionList) {.signal.}
proc getHasMore*(self: TransactionList): bool {.slot.} =
return self.hasMore
proc setHasMore*(self: TransactionList, hasMore: bool) {.slot.} =
self.hasMore = hasMore
self.hasMoreChanged()
QtProperty[bool] hasMore:
read = getHasMore
write = setHasMore
notify = currentTransactionsChanged
method data(self: TransactionList, index: QModelIndex, role: int): QVariant = method data(self: TransactionList, index: QModelIndex, role: int): QVariant =
if not index.isValid: if not index.isValid:
return return

View File

@ -5,7 +5,7 @@ import
web3/[conversions, ethtypes], stint web3/[conversions, ethtypes], stint
# TODO: make this public in nim-web3 lib # TODO: make this public in nim-web3 lib
template stripLeadingZeros(value: string): string = template stripLeadingZeros*(value: string): string =
var cidx = 0 var cidx = 0
# ignore the last character so we retain '0' on zero value # ignore the last character so we retain '0' on zero value
while cidx < value.len - 1 and value[cidx] == '0': while cidx < value.len - 1 and value[cidx] == '0':

View File

@ -47,8 +47,9 @@ proc getContactByID*(id: string): string =
proc getBlockByNumber*(blockNumber: string): string = proc getBlockByNumber*(blockNumber: string): string =
result = callPrivateRPC("eth_getBlockByNumber", %* [blockNumber, false]) result = callPrivateRPC("eth_getBlockByNumber", %* [blockNumber, false])
proc getTransfersByAddress*(address: string, limit: string, fetchMore: bool = false): string = proc getTransfersByAddress*(address: string, toBlock: string, limit: string, fetchMore: bool = false): string =
result = callPrivateRPC("wallet_getTransfersByAddress", %* [address, newJNull(), limit, fetchMore]) let toBlockParsed = if not fetchMore: newJNull() else: %toBlock
result = callPrivateRPC("wallet_getTransfersByAddress", %* [address, toBlockParsed, limit, fetchMore])
proc signMessage*(rpcParams: string): string = proc signMessage*(rpcParams: string): string =
return $status_go.signMessage(rpcParams) return $status_go.signMessage(rpcParams)

View File

@ -1,7 +1,11 @@
import json, options, typetraits, tables, sequtils import # std libs
import web3/ethtypes, json_serialization, stint json, options, typetraits, tables, sequtils, strutils
import accounts/constants
import ../../eventemitter import # vendor libs
web3/ethtypes, json_serialization, stint
import # status-desktop libs
accounts/constants, ../../eventemitter
type SignalType* {.pure.} = enum type SignalType* {.pure.} = enum
Message = "messages.new" Message = "messages.new"
@ -109,6 +113,7 @@ type
type type
Transaction* = ref object Transaction* = ref object
id*: string
typeValue*: string typeValue*: string
address*: string address*: string
blockNumber*: string blockNumber*: string
@ -123,6 +128,13 @@ type
value*: string value*: string
fromAddress*: string fromAddress*: string
to*: string to*: string
proc cmpTransactions*(x, y: Transaction): int =
# Sort proc to compare transactions from a single account.
# Compares first by block number, then by nonce
result = cmp(x.blockNumber.parseHexInt, y.blockNumber.parseHexInt)
if result == 0:
result = cmp(x.nonce, y.nonce)
type type
RpcException* = object of CatchableError RpcException* = object of CatchableError

View File

@ -1,9 +1,14 @@
import json, random, strutils, strformat, tables, chronicles, unicode import # std libs
import stint json, random, strutils, strformat, tables, times, unicode
from times import getTime, toUnix, nanosecond from sugar import `=>`, `->`
import accounts/signing_phrases
import # vendor libs
stint, chronicles
from web3 import Address, fromHex from web3 import Address, fromHex
import # status-desktop libs
accounts/signing_phrases
proc getTimelineChatId*(pubKey: string = ""): string = proc getTimelineChatId*(pubKey: string = ""): string =
if pubKey == "": if pubKey == "":
return "@timeline70bd746ddcc12beb96b2c9d572d0784ab137ffc774f5383e50585a932080b57cca0484b259e61cecbaa33a4c98a300a" return "@timeline70bd746ddcc12beb96b2c9d572d0784ab137ffc774f5383e50585a932080b57cca0484b259e61cecbaa33a4c98a300a"
@ -116,5 +121,35 @@ proc find*[T](s: seq[T], pred: proc(x: T): bool {.closure.}): T {.inline.} =
return default(type(T)) return default(type(T))
result = results[0] result = results[0]
proc find*[T](s: seq[T], pred: proc(x: T): bool {.closure.}, found: var bool): T {.inline.} =
let results = s.filter(pred)
if results.len == 0:
found = false
return default(type(T))
result = results[0]
found = true
proc parseAddress*(strAddress: string): Address = proc parseAddress*(strAddress: string): Address =
fromHex(Address, strAddress) fromHex(Address, strAddress)
proc hex2Time*(hex: string): Time =
# represents the time since 1970-01-01T00:00:00Z
fromUnix(fromHex[int64](hex))
proc hex2LocalDateTime*(hex: string): DateTime =
# Convert hex time (since 1970-01-01T00:00:00Z) into a DateTime using the
# local timezone.
hex.hex2Time.local
proc isUnique*[T](key: T, existingKeys: var seq[T]): bool =
# If the key doesn't exist in the existingKeys seq, add it and return true.
# Otherwise, the key already existed, so return false.
# Can be used to deduplicate sequences with `deduplicate[T]`.
if not existingKeys.contains(key):
existingKeys.add key
return true
return false
proc deduplicate*[T](txs: var seq[T], key: (T) -> string) =
var existingKeys: seq[string] = @[]
txs.keepIf(tx => tx.key().isUnique(existingKeys))

View File

@ -1,10 +1,12 @@
import json, json, options, json_serialization, stint, chronicles import # std libs
import core, types, utils, strutils, strformat json, times, options, strutils, strformat
import utils
from status_go import validateMnemonic#, startWallet import # vendor libs
import ../wallet/account json_serialization, stint, chronicles, web3/ethtypes
import web3/ethtypes from status_go import validateMnemonic
import ./types
import # status-desktop libs
../wallet/account, ./types, ./conversions, ./core, ./types, ./utils
proc getWalletAccounts*(): seq[WalletAccount] = proc getWalletAccounts*(): seq[WalletAccount] =
try: try:
@ -33,20 +35,24 @@ proc getWalletAccounts*(): seq[WalletAccount] =
proc getTransactionReceipt*(transactionHash: string): string = proc getTransactionReceipt*(transactionHash: string): string =
result = callPrivateRPC("eth_getTransactionReceipt", %* [transactionHash]) result = callPrivateRPC("eth_getTransactionReceipt", %* [transactionHash])
proc getTransfersByAddress*(address: string): seq[types.Transaction] = proc getTransfersByAddress*(address: string, toBlock: Uint256, limit: int, loadMore: bool = false): seq[types.Transaction] =
try: try:
let transactionsResponse = getTransfersByAddress(address, "0x14") let
let transactions = parseJson(transactionsResponse)["result"] toBlockParsed = "0x" & stint.toHex(toBlock)
limitParsed = "0x" & limit.toHex.stripLeadingZeros
transactionsResponse = getTransfersByAddress(address, toBlockParsed, limitParsed, loadMore)
transactions = parseJson(transactionsResponse)["result"]
var accountTransactions: seq[types.Transaction] = @[] var accountTransactions: seq[types.Transaction] = @[]
for transaction in transactions: for transaction in transactions:
accountTransactions.add(types.Transaction( accountTransactions.add(types.Transaction(
id: transaction["id"].getStr,
typeValue: transaction["type"].getStr, typeValue: transaction["type"].getStr,
address: transaction["address"].getStr, address: transaction["address"].getStr,
contract: transaction["contract"].getStr, contract: transaction["contract"].getStr,
blockNumber: transaction["blockNumber"].getStr, blockNumber: transaction["blockNumber"].getStr,
blockHash: transaction["blockhash"].getStr, blockHash: transaction["blockhash"].getStr,
timestamp: transaction["timestamp"].getStr, timestamp: $hex2LocalDateTime(transaction["timestamp"].getStr()),
gasPrice: transaction["gasPrice"].getStr, gasPrice: transaction["gasPrice"].getStr,
gasLimit: transaction["gasLimit"].getStr, gasLimit: transaction["gasLimit"].getStr,
gasUsed: transaction["gasUsed"].getStr, gasUsed: transaction["gasUsed"].getStr,

View File

@ -1,5 +1,8 @@
import # vendor libs import # vendor libs
json_serialization json_serialization#, stint
from eth/common/eth_types_json_serialization import writeValue, readValue
export writeValue, readValue
export json_serialization export json_serialization

View File

@ -1,4 +1,4 @@
import json, strformat, strutils, chronicles, sequtils, httpclient, tables, net import json, strformat, strutils, chronicles, sequtils, sugar, httpclient, tables, net
import json_serialization, stint import json_serialization, stint
from web3/ethtypes import Address, EthSend, Quantity from web3/ethtypes import Address, EthSend, Quantity
from web3/conversions import `$` from web3/conversions import `$`
@ -244,6 +244,11 @@ proc addNewGeneratedAccount(self: WalletModel, generatedAccount: GeneratedAccoun
var derivedAccount: DerivedAccount = status_accounts.saveAccount(generatedAccount, password, color, accountType, isADerivedAccount, walletIndex) var derivedAccount: DerivedAccount = status_accounts.saveAccount(generatedAccount, password, color, accountType, isADerivedAccount, walletIndex)
var account = self.newAccount(accountType, derivedAccount.derivationPath, accountName, derivedAccount.address, color, fmt"0.00 {self.defaultCurrency}", derivedAccount.publicKey) var account = self.newAccount(accountType, derivedAccount.derivationPath, accountName, derivedAccount.address, color, fmt"0.00 {self.defaultCurrency}", derivedAccount.publicKey)
self.accounts.add(account) self.accounts.add(account)
# wallet_checkRecentHistory is required to be called when a new account is
# added before wallet_getTransfersByAddress can be called. This is because
# wallet_checkRecentHistory populates the status-go db that
# wallet_getTransfersByAddress reads from
discard status_wallet.checkRecentHistory(self.accounts.map(account => account.address))
self.events.emit("newAccountAdded", AccountArgs(account: account)) self.events.emit("newAccountAdded", AccountArgs(account: account))
except Exception as e: except Exception as e:
raise newException(StatusGoException, fmt"Error adding new account: {e.msg}") raise newException(StatusGoException, fmt"Error adding new account: {e.msg}")
@ -314,6 +319,7 @@ proc changeAccountSettings*(self: WalletModel, address: string, accountName: str
proc deleteAccount*(self: WalletModel, address: string): string = proc deleteAccount*(self: WalletModel, address: string): string =
result = status_accounts.deleteAccount(address) result = status_accounts.deleteAccount(address)
self.accounts = self.accounts.filter(acc => acc.address.toLowerAscii != address.toLowerAscii)
proc toggleAsset*(self: WalletModel, symbol: string) = proc toggleAsset*(self: WalletModel, symbol: string) =
self.tokens = status_tokens.toggleAsset(symbol) self.tokens = status_tokens.toggleAsset(symbol)
@ -333,8 +339,8 @@ proc hideAsset*(self: WalletModel, symbol: string) =
proc addCustomToken*(self: WalletModel, symbol: string, enable: bool, address: string, name: string, decimals: int, color: string) = proc addCustomToken*(self: WalletModel, symbol: string, enable: bool, address: string, name: string, decimals: int, color: string) =
addCustomToken(address, name, symbol, decimals, color) addCustomToken(address, name, symbol, decimals, color)
proc getTransfersByAddress*(self: WalletModel, address: string): seq[Transaction] = proc getTransfersByAddress*(self: WalletModel, address: string, toBlock: Uint256, limit: int, loadMore: bool): seq[Transaction] =
result = status_wallet.getTransfersByAddress(address) result = status_wallet.getTransfersByAddress(address, toBlock, limit, loadMore)
proc validateMnemonic*(self: WalletModel, mnemonic: string): string = proc validateMnemonic*(self: WalletModel, mnemonic: string): string =
result = status_wallet.validateMnemonic(mnemonic).parseJSON()["error"].getStr result = status_wallet.validateMnemonic(mnemonic).parseJSON()["error"].getStr

View File

@ -22,7 +22,7 @@ type WalletAccount* = ref object
assetList*: seq[Asset] assetList*: seq[Asset]
wallet*, chat*: bool wallet*, chat*: bool
collectiblesLists*: seq[CollectibleList] collectiblesLists*: seq[CollectibleList]
transactions*: seq[Transaction] transactions*: tuple[hasMore: bool, data: seq[Transaction]]
type AccountArgs* = ref object of Args type AccountArgs* = ref object of Args
account*: WalletAccount account*: WalletAccount

View File

@ -189,7 +189,6 @@ Popup {
anchors.leftMargin: 32 anchors.leftMargin: 32
//% "History" //% "History"
btnText: qsTrId("history") btnText: qsTrId("history")
onClicked: historyTab.checkIfHistoryIsBeingFetched()
} }
} }

View File

@ -9,8 +9,9 @@ import "../../../shared/status/core"
import "../../../shared/status" import "../../../shared/status"
Item { Item {
property int pageSize: 20 // number of transactions per page
property var tokens: { property var tokens: {
const count = walletModel.defaultTokenList.rowCount() let count = walletModel.defaultTokenList.rowCount()
const toks = [] const toks = []
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
toks.push({ toks.push({
@ -18,26 +19,25 @@ Item {
"symbol": walletModel.defaultTokenList.rowData(i, 'symbol') "symbol": walletModel.defaultTokenList.rowData(i, 'symbol')
}) })
} }
count = walletModel.customTokenList.rowCount()
for (var i = 0; i < count; i++) {
toks.push({
"address": walletModel.customTokenList.rowData(i, 'address'),
"symbol": walletModel.customTokenList.rowData(i, 'symbol')
})
}
return toks return toks
} }
function checkIfHistoryIsBeingFetched() {
loadMoreButton.loadedMore = false;
// prevent history from being fetched everytime you click on
// the history tab
if (walletModel.isHistoryFetched(walletModel.currentAccount.account))
return;
fetchHistory();
}
function fetchHistory() { function fetchHistory() {
if (walletModel.isFetchingHistory(walletModel.currentAccount.address)) { if (walletModel.isFetchingHistory()) {
loadingImg.active = true loadingImg.active = true
} else { } else {
walletModel.loadTransactionsForAccount( walletModel.loadTransactionsForAccount(
walletModel.currentAccount.address) walletModel.currentAccount.address,
walletModel.transactions.getLastTxBlockNumber(),
pageSize,
true)
} }
} }
@ -50,7 +50,6 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Style.current.padding anchors.rightMargin: Style.current.padding
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: Style.currentPadding
} }
Component { Component {
@ -60,9 +59,10 @@ Item {
Connections { Connections {
target: walletModel target: walletModel
onHistoryWasFetched: checkIfHistoryIsBeingFetched()
onLoadingTrxHistoryChanged: { onLoadingTrxHistoryChanged: {
loadingImg.active = isLoading if (walletModel.currentAccount.address.toLowerCase() === address.toLowerCase()) {
loadingImg.active = isLoading
}
} }
} }
@ -73,6 +73,7 @@ Item {
id: transactionListItem id: transactionListItem
property bool isHovered: false property bool isHovered: false
property string symbol: "" property string symbol: ""
property bool isIncoming: to === walletModel.currentAccount.address
anchors.right: parent.right anchors.right: parent.right
anchors.left: parent.left anchors.left: parent.left
height: 64 height: 64
@ -107,7 +108,12 @@ Item {
id: transactionModal id: transactionModal
} }
Item { Row {
anchors.left: parent.left
anchors.leftMargin: Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter
spacing: 5
Image { Image {
id: assetIcon id: assetIcon
width: 40 width: 40
@ -115,9 +121,7 @@ Item {
source: "../../img/tokens/" source: "../../img/tokens/"
+ (transactionListItem.symbol + (transactionListItem.symbol
!= "" ? transactionListItem.symbol : "ETH") + ".png" != "" ? transactionListItem.symbol : "ETH") + ".png"
anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
anchors.topMargin: 12
onStatusChanged: { onStatusChanged: {
if (assetIcon.status == Image.Error) { if (assetIcon.status == Image.Error) {
assetIcon.source = "../../img/tokens/DEFAULT-TOKEN@3x.png" assetIcon.source = "../../img/tokens/DEFAULT-TOKEN@3x.png"
@ -129,100 +133,107 @@ Item {
StyledText { StyledText {
id: transferIcon id: transferIcon
anchors.topMargin: 25 anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
anchors.left: assetIcon.right
anchors.leftMargin: 22
height: 15 height: 15
width: 15 width: 15
color: to !== walletModel.currentAccount.address ? "#4360DF" : "green" color: isIncoming ? Style.current.success : Style.current.danger
text: to !== walletModel.currentAccount.address ? "↑" : "↓" text: isIncoming ? "↓" : "↑"
} }
StyledText { StyledText {
id: transactionValue id: transactionValue
anchors.left: transferIcon.right anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.current.smallPadding font.pixelSize: Style.current.primaryTextFontSize
anchors.top: parent.top
anchors.topMargin: Style.current.bigPadding
font.pixelSize: 15
text: utilsModel.hex2Eth(value) + " " + transactionListItem.symbol text: utilsModel.hex2Eth(value) + " " + transactionListItem.symbol
} }
} }
Item { Row {
anchors.right: timeInfo.left anchors.right: timeInfo.left
anchors.rightMargin: Style.current.smallPadding
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: Style.current.bigPadding anchors.topMargin: Style.current.bigPadding
width: children[0].width + children[1].width spacing: 5
StyledText { StyledText {
text: to !== walletModel.currentAccount.address ? text: isIncoming ?
//% "To " //% "From "
qsTrId("to-") : qsTrId("from-") :
//% "From " //% "To "
qsTrId("from-") qsTrId("to-")
anchors.right: addressValue.left
color: Style.current.secondaryText color: Style.current.secondaryText
anchors.top: parent.top font.pixelSize: Style.current.primaryTextFontSize
font.pixelSize: 15
font.strikeout: false font.strikeout: false
} }
StyledText { Address {
id: addressValue id: addressValue
font.family: Style.current.fontHexRegular.name text: isIncoming ? fromAddress : to
text: to maxWidth: 120
width: 100 width: 120
elide: Text.ElideMiddle horizontalAlignment: Text.AlignRight
anchors.right: parent.right font.pixelSize: Style.current.primaryTextFontSize
anchors.top: parent.top color: Style.current.textColor
font.pixelSize: 15
} }
} }
Item { Row {
id: timeInfo id: timeInfo
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Style.current.smallPadding
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: Style.current.bigPadding anchors.topMargin: Style.current.bigPadding
width: children[0].width + children[1].width + children[2].width spacing: 5
StyledText { StyledText {
text: "• " text: " • "
font.weight: Font.Bold font.weight: Font.Bold
anchors.right: timeIndicator.left
color: Style.current.secondaryText color: Style.current.secondaryText
anchors.top: parent.top font.pixelSize: Style.current.primaryTextFontSize
font.pixelSize: 15
} }
StyledText { StyledText {
id: timeIndicator id: timeIndicator
text: "At " text: qsTr("At ")
anchors.right: timeValue.left
color: Style.current.secondaryText color: Style.current.secondaryText
anchors.top: parent.top font.pixelSize: Style.current.primaryTextFontSize
font.pixelSize: 15
font.strikeout: false font.strikeout: false
} }
StyledText { StyledText {
id: timeValue id: timeValue
text: timestamp text: new Date(timestamp).toLocaleString(globalSettings.locale)
anchors.right: parent.right font.pixelSize: Style.current.primaryTextFontSize
anchors.top: parent.top
font.pixelSize: 15
anchors.rightMargin: Style.current.smallPadding anchors.rightMargin: Style.current.smallPadding
} }
} }
} }
} }
StyledText {
id: nonArchivalNodeError
visible: walletModel.isNonArchivalNode
height: visible ? implicitHeight : 0
anchors.top: parent.top
text: qsTr("Status Desktop is connected to a non-archival node. Transaction history may be incomplete.")
font.pixelSize: Style.current.primaryTextFontSize
color: Style.current.danger
}
StyledText {
id: noTxs
anchors.top: nonArchivalNodeError.bottom
visible: transactionListRoot.count === 0
height: visible ? implicitHeight : 0
text: qsTr("No transactions found")
font.pixelSize: Style.current.primaryTextFontSize
}
ListView { ListView {
id: transactionListRoot id: transactionListRoot
anchors.topMargin: 20 anchors.top: noTxs.bottom
height: parent.height - extraButtons.height anchors.topMargin: Style.current.padding
anchors.bottom: loadMoreButton.top
anchors.bottomMargin: Style.current.padding
width: parent.width width: parent.width
clip: true clip: true
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
@ -238,26 +249,21 @@ Item {
} }
} }
RowLayout { StatusButton {
id: extraButtons id: loadMoreButton
anchors.left: parent.left //% "Load More"
text: qsTrId("load-more")
// TODO: handle case when requested limit === transaction count -- there
// is currently no way to know that there are no more results
enabled: !loadingImg.active && walletModel.transactions.hasMore
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
height: loadMoreButton.height anchors.bottomMargin: Style.current.padding
property bool loadedMore: false
StatusButton { onClicked: {
id: loadMoreButton fetchHistory()
//% "Load More" loadMoreButton.loadedMore = true
text: qsTrId("load-more")
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
property bool loadedMore: false
Connections {
onClicked: {
fetchHistory()
loadMoreButton.loadedMore = true
}
}
} }
} }
} }

View File

@ -23,7 +23,6 @@ ColumnLayout {
onboardingModel.firstTimeLogin = false onboardingModel.firstTimeLogin = false
walletModel.setInitialRange() walletModel.setInitialRange()
} }
walletModel.checkRecentHistory()
} }
Timer { Timer {
@ -120,7 +119,6 @@ ColumnLayout {
anchors.leftMargin: 32 anchors.leftMargin: 32
//% "History" //% "History"
btnText: qsTrId("history") btnText: qsTrId("history")
onClicked: historyTab.checkIfHistoryIsBeingFetched()
} }
} }

View File

@ -12,6 +12,9 @@ StatusRoundButton {
type: "secondary" type: "secondary"
width: 36 width: 36
height: 36 height: 36
readonly property var onAfterAddAccount: function() {
walletInfoContainer.changeSelectedAccount(walletModel.accounts.rowCount() - 1)
}
onClicked: { onClicked: {
if (newAccountMenu.opened) { if (newAccountMenu.opened) {
@ -24,15 +27,19 @@ StatusRoundButton {
GenerateAccountModal { GenerateAccountModal {
id: generateAccountModal id: generateAccountModal
onAfterAddAccount: function() { btnAdd.onAfterAddAccount() }
} }
AddAccountWithSeed { AddAccountWithSeed {
id: addAccountWithSeedModal id: addAccountWithSeedModal
onAfterAddAccount: function() { btnAdd.onAfterAddAccount() }
} }
AddAccountWithPrivateKey { AddAccountWithPrivateKey {
id: addAccountWithPrivateKeydModal id: addAccountWithPrivateKeydModal
onAfterAddAccount: function() { btnAdd.onAfterAddAccount() }
} }
AddWatchOnlyAccount { AddWatchOnlyAccount {
id: addWatchOnlyAccountModal id: addWatchOnlyAccountModal
onAfterAddAccount: function() { btnAdd.onAfterAddAccount() }
} }
PopupMenu { PopupMenu {

View File

@ -16,6 +16,7 @@ ModalPopup {
property string privateKeyValidationError: "" property string privateKeyValidationError: ""
property string accountNameValidationError: "" property string accountNameValidationError: ""
property bool loading: false property bool loading: false
property var onAfterAddAccount: function() {}
function validate() { function validate() {
if (passwordInput.text === "") { if (passwordInput.text === "") {
@ -141,7 +142,7 @@ ModalPopup {
} }
return return
} }
popup.onAfterAddAccount()
popup.close(); popup.close();
} }
} }

View File

@ -14,6 +14,7 @@ ModalPopup {
property string seedValidationError: "" property string seedValidationError: ""
property string accountNameValidationError: "" property string accountNameValidationError: ""
property bool loading: false property bool loading: false
property var onAfterAddAccount: function() {}
function reset() { function reset() {
passwordInput.text = "" passwordInput.text = ""
@ -142,6 +143,7 @@ ModalPopup {
} }
return return
} }
popup.onAfterAddAccount()
popup.reset() popup.reset()
popup.close(); popup.close();
} }

View File

@ -14,6 +14,7 @@ ModalPopup {
property string addressError: "" property string addressError: ""
property string accountNameValidationError: "" property string accountNameValidationError: ""
property bool loading: false property bool loading: false
property var onAfterAddAccount: function() {}
function validate() { function validate() {
if (addressInput.text === "") { if (addressInput.text === "") {
@ -107,7 +108,7 @@ ModalPopup {
accountError.text = error accountError.text = error
return accountError.open() return accountError.open()
} }
popup.onAfterAddAccount()
popup.close(); popup.close();
} }
} }

View File

@ -14,6 +14,7 @@ ModalPopup {
property string passwordValidationError: "" property string passwordValidationError: ""
property string accountNameValidationError: "" property string accountNameValidationError: ""
property bool loading: false property bool loading: false
property var onAfterAddAccount: function() {}
function validate() { function validate() {
if (passwordInput.text === "") { if (passwordInput.text === "") {
@ -114,7 +115,7 @@ ModalPopup {
} }
return return
} }
popup.onAfterAddAccount();
popup.close(); popup.close();
} }
} }