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.
#### `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`
* `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 |
| `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 |
| `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 |
| `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 |

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 |
| `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 |
| `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 |
| `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 |
@ -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 |
| `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. |
| `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 |
| `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 |

View File

@ -87,7 +87,7 @@ QtObject:
# somehow this value crashes the app
if value == "0x0":
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.} =
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 views/[asset_list, account_list, account_item]
@ -34,7 +34,7 @@ proc init*(self: WalletController) =
for account in accounts:
self.view.addAccountToList(account)
self.view.initBalances()
self.view.checkRecentHistory()
self.view.setDappBrowserAddress()
self.status.events.on("accountsUpdated") do(e: Args):
@ -63,14 +63,16 @@ proc init*(self: WalletController) =
# TODO: show notification
of "new-transfers":
for acc in data.accounts:
self.view.loadTransactionsForAccount(acc)
self.view.initBalances(false)
self.view.initBalances(data.accounts)
of "recent-history-fetching":
self.view.setHistoryFetchState(data.accounts, true)
of "recent-history-ready":
self.view.initBalances(data.accounts)
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:
error "Unhandled wallet signal", eventType=data.eventType

View File

@ -1,5 +1,6 @@
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
NimQml, chronicles, stint
@ -36,7 +37,9 @@ type
GasPredictionsTaskArg = ref object of QObjectTaskArg
LoadTransactionsTaskArg = ref object of QObjectTaskArg
address: string
blockNumber: string
toBlock: Uint256
limit: int
loadMore: bool
ResolveEnsTaskArg = ref object of QObjectTaskArg
ens: string
uuid: string
@ -144,16 +147,20 @@ const loadTransactionsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.}
arg = decode[LoadTransactionsTaskArg](argEncoded)
output = %*{
"address": arg.address,
"history": getTransfersByAddress(arg.address)
"history": getTransfersByAddress(arg.address, arg.toBlock, arg.limit, arg.loadMore),
"loadMore": arg.loadMore
}
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(
tptr: cast[ByteAddress](loadTransactionsTask),
vptr: cast[ByteAddress](self.vptr),
slot: slot,
address: address
address: address,
toBlock: toBlock,
limit: limit,
loadMore: loadMore
)
self.status.tasks.threadpool.start(arg)
@ -211,6 +218,7 @@ QtObject:
defaultGasLimit: string
signingPhrase: string
fetchingHistoryState: Table[string, bool]
isNonArchivalNode: bool
proc delete(self: WalletView) =
self.accounts.delete
@ -249,6 +257,7 @@ QtObject:
result.defaultGasLimit = "21000"
result.signingPhrase = ""
result.fetchingHistoryState = initTable[string, bool]()
result.isNonArchivalNode = false
result.setup
proc etherscanLinkChanged*(self: WalletView) {.signal.}
@ -325,7 +334,8 @@ QtObject:
self.setCurrentCollectiblesLists(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.} =
result = newQVariant(self.currentAccount)
@ -616,6 +626,18 @@ QtObject:
self.loadCollectibles("setCollectiblesResult", address, collectibleType)
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 getGasPricePredictions*(self: WalletView) {.slot.} =
@ -685,50 +707,78 @@ QtObject:
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) =
for acc in accounts:
self.fetchingHistoryState[acc] = isFetching
if not isFetching: self.historyWasFetched()
self.loadingTrxHistoryChanged(isFetching, acc)
proc isFetchingHistory*(self: WalletView, address: string): bool {.slot.} =
if self.fetchingHistoryState.hasKey(address):
return self.fetchingHistoryState[address]
return true
proc initBalance(self: WalletView, acc: WalletAccount, loadTransactions: bool = true) =
let
accountAddress = acc.address
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.} =
return self.currentTransactions.rowCount() > 0
proc loadingTrxHistoryChanged*(self: WalletView, isLoading: bool) {.signal.}
proc loadTransactionsForAccount*(self: WalletView, address: string) {.slot.} =
self.loadingTrxHistoryChanged(true)
self.loadTransactions("setTrxHistoryResult", address)
proc getLatestTransactionHistory*(self: WalletView, accounts: seq[string]) =
for acc in accounts:
self.loadTransactionsForAccount(acc)
proc initBalance(self: WalletView, accountAddress: string, loadTransactions: bool = true) =
var found = false
let acc = self.status.wallet.accounts.find(acc => acc.address.toLowerAscii == accountAddress.toLowerAscii, found)
if not found:
error "Failed to init balance: could not find account", account=accountAddress
return
self.initBalance(acc, loadTransactions)
proc initBalances*(self: WalletView, loadTransactions: bool = true) =
for acc in self.status.wallet.accounts:
let accountAddress = acc.address
let 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)
self.initBalance(acc, loadTransactions)
proc initBalances*(self: WalletView, accounts: seq[string], loadTransactions: bool = true) =
for acc in accounts:
self.initBalance(acc, loadTransactions)
proc setTrxHistoryResult(self: WalletView, historyJSON: string) {.slot.} =
let historyData = parseJson(historyJSON)
let transactions = historyData["history"].to(seq[Transaction]);
let address = historyData["address"].getStr
let index = self.accounts.getAccountindexByAddress(address)
let
historyData = parseJson(historyJSON)
transactions = historyData["history"].to(seq[Transaction])
address = historyData["address"].getStr
wasFetchMore = historyData["loadMore"].getBool
isCurrentAccount = address.toLowerAscii == self.currentAccount.address.toLowerAscii
index = self.accounts.getAccountindexByAddress(address)
if index == -1: return
self.accounts.getAccount(index).transactions = transactions
if address == self.currentAccount.address:
self.setCurrentTransactions(
self.accounts.getAccount(index).transactions)
self.loadingTrxHistoryChanged(false)
let account = self.accounts.getAccount(index)
# concatenate the new page of txs to existing account transactions,
# sort them by block number and nonce, then deduplicate them based on their
# 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.} =
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 account_item, asset_list
from ../../../status/wallet import WalletAccount, Asset, CollectibleList
@ -54,7 +54,7 @@ QtObject:
proc getAccountindexByAddress*(self: AccountList, address: string): int =
var i = 0
for accountView in self.accounts:
if (accountView.account.address == address):
if (accountView.account.address.toLowerAscii == address.toLowerAscii):
return i
i = i + 1
return -1

View File

@ -21,6 +21,7 @@ type
QtObject:
type TransactionList* = ref object of QAbstractListModel
transactions*: seq[Transaction]
hasMore*: bool
proc setup(self: TransactionList) = self.QAbstractListModel.setup
@ -31,14 +32,29 @@ QtObject:
proc newTransactionList*(): TransactionList =
new(result, delete)
result.transactions = @[]
result.hasMore = true
result.setup
proc getLastTxBlockNumber*(self: TransactionList): string =
proc getLastTxBlockNumber*(self: TransactionList): string {.slot.} =
return self.transactions[^1].blockNumber
method rowCount*(self: TransactionList, index: QModelIndex = nil): int =
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 =
if not index.isValid:
return

View File

@ -5,7 +5,7 @@ import
web3/[conversions, ethtypes], stint
# TODO: make this public in nim-web3 lib
template stripLeadingZeros(value: string): string =
template stripLeadingZeros*(value: string): string =
var cidx = 0
# ignore the last character so we retain '0' on zero value
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 =
result = callPrivateRPC("eth_getBlockByNumber", %* [blockNumber, false])
proc getTransfersByAddress*(address: string, limit: string, fetchMore: bool = false): string =
result = callPrivateRPC("wallet_getTransfersByAddress", %* [address, newJNull(), limit, fetchMore])
proc getTransfersByAddress*(address: string, toBlock: string, limit: string, fetchMore: bool = false): string =
let toBlockParsed = if not fetchMore: newJNull() else: %toBlock
result = callPrivateRPC("wallet_getTransfersByAddress", %* [address, toBlockParsed, limit, fetchMore])
proc signMessage*(rpcParams: string): string =
return $status_go.signMessage(rpcParams)

View File

@ -1,7 +1,11 @@
import json, options, typetraits, tables, sequtils
import web3/ethtypes, json_serialization, stint
import accounts/constants
import ../../eventemitter
import # std libs
json, options, typetraits, tables, sequtils, strutils
import # vendor libs
web3/ethtypes, json_serialization, stint
import # status-desktop libs
accounts/constants, ../../eventemitter
type SignalType* {.pure.} = enum
Message = "messages.new"
@ -109,6 +113,7 @@ type
type
Transaction* = ref object
id*: string
typeValue*: string
address*: string
blockNumber*: string
@ -123,6 +128,13 @@ type
value*: string
fromAddress*: 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
RpcException* = object of CatchableError

View File

@ -1,9 +1,14 @@
import json, random, strutils, strformat, tables, chronicles, unicode
import stint
from times import getTime, toUnix, nanosecond
import accounts/signing_phrases
import # std libs
json, random, strutils, strformat, tables, times, unicode
from sugar import `=>`, `->`
import # vendor libs
stint, chronicles
from web3 import Address, fromHex
import # status-desktop libs
accounts/signing_phrases
proc getTimelineChatId*(pubKey: string = ""): string =
if pubKey == "":
return "@timeline70bd746ddcc12beb96b2c9d572d0784ab137ffc774f5383e50585a932080b57cca0484b259e61cecbaa33a4c98a300a"
@ -116,5 +121,35 @@ proc find*[T](s: seq[T], pred: proc(x: T): bool {.closure.}): T {.inline.} =
return default(type(T))
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 =
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 core, types, utils, strutils, strformat
import utils
from status_go import validateMnemonic#, startWallet
import ../wallet/account
import web3/ethtypes
import ./types
import # std libs
json, times, options, strutils, strformat
import # vendor libs
json_serialization, stint, chronicles, web3/ethtypes
from status_go import validateMnemonic
import # status-desktop libs
../wallet/account, ./types, ./conversions, ./core, ./types, ./utils
proc getWalletAccounts*(): seq[WalletAccount] =
try:
@ -33,20 +35,24 @@ proc getWalletAccounts*(): seq[WalletAccount] =
proc getTransactionReceipt*(transactionHash: string): string =
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:
let transactionsResponse = getTransfersByAddress(address, "0x14")
let transactions = parseJson(transactionsResponse)["result"]
let
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] = @[]
for transaction in transactions:
accountTransactions.add(types.Transaction(
id: transaction["id"].getStr,
typeValue: transaction["type"].getStr,
address: transaction["address"].getStr,
contract: transaction["contract"].getStr,
blockNumber: transaction["blockNumber"].getStr,
blockHash: transaction["blockhash"].getStr,
timestamp: transaction["timestamp"].getStr,
timestamp: $hex2LocalDateTime(transaction["timestamp"].getStr()),
gasPrice: transaction["gasPrice"].getStr,
gasLimit: transaction["gasLimit"].getStr,
gasUsed: transaction["gasUsed"].getStr,

View File

@ -1,5 +1,8 @@
import # vendor libs
json_serialization
json_serialization#, stint
from eth/common/eth_types_json_serialization import writeValue, readValue
export writeValue, readValue
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
from web3/ethtypes import Address, EthSend, Quantity
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 account = self.newAccount(accountType, derivedAccount.derivationPath, accountName, derivedAccount.address, color, fmt"0.00 {self.defaultCurrency}", derivedAccount.publicKey)
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))
except Exception as e:
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 =
result = status_accounts.deleteAccount(address)
self.accounts = self.accounts.filter(acc => acc.address.toLowerAscii != address.toLowerAscii)
proc toggleAsset*(self: WalletModel, symbol: string) =
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) =
addCustomToken(address, name, symbol, decimals, color)
proc getTransfersByAddress*(self: WalletModel, address: string): seq[Transaction] =
result = status_wallet.getTransfersByAddress(address)
proc getTransfersByAddress*(self: WalletModel, address: string, toBlock: Uint256, limit: int, loadMore: bool): seq[Transaction] =
result = status_wallet.getTransfersByAddress(address, toBlock, limit, loadMore)
proc validateMnemonic*(self: WalletModel, mnemonic: string): string =
result = status_wallet.validateMnemonic(mnemonic).parseJSON()["error"].getStr

View File

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

View File

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

View File

@ -9,8 +9,9 @@ import "../../../shared/status/core"
import "../../../shared/status"
Item {
property int pageSize: 20 // number of transactions per page
property var tokens: {
const count = walletModel.defaultTokenList.rowCount()
let count = walletModel.defaultTokenList.rowCount()
const toks = []
for (var i = 0; i < count; i++) {
toks.push({
@ -18,26 +19,25 @@ Item {
"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
}
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() {
if (walletModel.isFetchingHistory(walletModel.currentAccount.address)) {
if (walletModel.isFetchingHistory()) {
loadingImg.active = true
} else {
walletModel.loadTransactionsForAccount(
walletModel.currentAccount.address)
walletModel.currentAccount.address,
walletModel.transactions.getLastTxBlockNumber(),
pageSize,
true)
}
}
@ -50,7 +50,6 @@ Item {
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
anchors.top: parent.top
anchors.topMargin: Style.currentPadding
}
Component {
@ -60,9 +59,10 @@ Item {
Connections {
target: walletModel
onHistoryWasFetched: checkIfHistoryIsBeingFetched()
onLoadingTrxHistoryChanged: {
loadingImg.active = isLoading
if (walletModel.currentAccount.address.toLowerCase() === address.toLowerCase()) {
loadingImg.active = isLoading
}
}
}
@ -73,6 +73,7 @@ Item {
id: transactionListItem
property bool isHovered: false
property string symbol: ""
property bool isIncoming: to === walletModel.currentAccount.address
anchors.right: parent.right
anchors.left: parent.left
height: 64
@ -107,7 +108,12 @@ Item {
id: transactionModal
}
Item {
Row {
anchors.left: parent.left
anchors.leftMargin: Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter
spacing: 5
Image {
id: assetIcon
width: 40
@ -115,9 +121,7 @@ Item {
source: "../../img/tokens/"
+ (transactionListItem.symbol
!= "" ? transactionListItem.symbol : "ETH") + ".png"
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 12
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (assetIcon.status == Image.Error) {
assetIcon.source = "../../img/tokens/DEFAULT-TOKEN@3x.png"
@ -129,100 +133,107 @@ Item {
StyledText {
id: transferIcon
anchors.topMargin: 25
anchors.top: parent.top
anchors.left: assetIcon.right
anchors.leftMargin: 22
anchors.verticalCenter: parent.verticalCenter
height: 15
width: 15
color: to !== walletModel.currentAccount.address ? "#4360DF" : "green"
text: to !== walletModel.currentAccount.address ? "↑" : "↓"
color: isIncoming ? Style.current.success : Style.current.danger
text: isIncoming ? "↓" : "↑"
}
StyledText {
id: transactionValue
anchors.left: transferIcon.right
anchors.leftMargin: Style.current.smallPadding
anchors.top: parent.top
anchors.topMargin: Style.current.bigPadding
font.pixelSize: 15
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Style.current.primaryTextFontSize
text: utilsModel.hex2Eth(value) + " " + transactionListItem.symbol
}
}
Item {
Row {
anchors.right: timeInfo.left
anchors.rightMargin: Style.current.smallPadding
anchors.top: parent.top
anchors.topMargin: Style.current.bigPadding
width: children[0].width + children[1].width
spacing: 5
StyledText {
text: to !== walletModel.currentAccount.address ?
//% "To "
qsTrId("to-") :
//% "From "
qsTrId("from-")
anchors.right: addressValue.left
text: isIncoming ?
//% "From "
qsTrId("from-") :
//% "To "
qsTrId("to-")
color: Style.current.secondaryText
anchors.top: parent.top
font.pixelSize: 15
font.pixelSize: Style.current.primaryTextFontSize
font.strikeout: false
}
StyledText {
Address {
id: addressValue
font.family: Style.current.fontHexRegular.name
text: to
width: 100
elide: Text.ElideMiddle
anchors.right: parent.right
anchors.top: parent.top
font.pixelSize: 15
text: isIncoming ? fromAddress : to
maxWidth: 120
width: 120
horizontalAlignment: Text.AlignRight
font.pixelSize: Style.current.primaryTextFontSize
color: Style.current.textColor
}
}
Item {
Row {
id: timeInfo
anchors.right: parent.right
anchors.rightMargin: Style.current.smallPadding
anchors.top: parent.top
anchors.topMargin: Style.current.bigPadding
width: children[0].width + children[1].width + children[2].width
spacing: 5
StyledText {
text: "• "
text: " • "
font.weight: Font.Bold
anchors.right: timeIndicator.left
color: Style.current.secondaryText
anchors.top: parent.top
font.pixelSize: 15
font.pixelSize: Style.current.primaryTextFontSize
}
StyledText {
id: timeIndicator
text: "At "
anchors.right: timeValue.left
text: qsTr("At ")
color: Style.current.secondaryText
anchors.top: parent.top
font.pixelSize: 15
font.pixelSize: Style.current.primaryTextFontSize
font.strikeout: false
}
StyledText {
id: timeValue
text: timestamp
anchors.right: parent.right
anchors.top: parent.top
font.pixelSize: 15
text: new Date(timestamp).toLocaleString(globalSettings.locale)
font.pixelSize: Style.current.primaryTextFontSize
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 {
id: transactionListRoot
anchors.topMargin: 20
height: parent.height - extraButtons.height
anchors.top: noTxs.bottom
anchors.topMargin: Style.current.padding
anchors.bottom: loadMoreButton.top
anchors.bottomMargin: Style.current.padding
width: parent.width
clip: true
boundsBehavior: Flickable.StopAtBounds
@ -238,26 +249,21 @@ Item {
}
}
RowLayout {
id: extraButtons
anchors.left: parent.left
StatusButton {
id: loadMoreButton
//% "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.bottom: parent.bottom
height: loadMoreButton.height
anchors.bottomMargin: Style.current.padding
property bool loadedMore: false
StatusButton {
id: loadMoreButton
//% "Load More"
text: qsTrId("load-more")
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
property bool loadedMore: false
Connections {
onClicked: {
fetchHistory()
loadMoreButton.loadedMore = true
}
}
onClicked: {
fetchHistory()
loadMoreButton.loadedMore = true
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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