diff --git a/devuser_guide/content/api/qml/wallet.md b/devuser_guide/content/api/qml/wallet.md
index 9822bb2518..0dba08b38a 100644
--- a/devuser_guide/content/api/qml/wallet.md
+++ b/devuser_guide/content/api/qml/wallet.md
@@ -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
`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
`txHash` (`string`): has of the transaction
`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 |
diff --git a/docs/qml_api.md b/docs/qml_api.md
index 59e04afcaa..6057e8f9b9 100644
--- a/docs/qml_api.md
+++ b/docs/qml_api.md
@@ -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
`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
`txHash` (`string`): has of the transaction
`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
`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 |
diff --git a/src/app/utilsView/view.nim b/src/app/utilsView/view.nim
index 0f0447263f..2526c2e762 100644
--- a/src/app/utilsView/view.nim
+++ b/src/app/utilsView/view.nim
@@ -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)
diff --git a/src/app/wallet/core.nim b/src/app/wallet/core.nim
index 8a29c26e73..7a2eb45c4f 100644
--- a/src/app/wallet/core.nim
+++ b/src/app/wallet/core.nim
@@ -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
diff --git a/src/app/wallet/view.nim b/src/app/wallet/view.nim
index 2d2630659c..1dc7fa7f47 100644
--- a/src/app/wallet/view.nim
+++ b/src/app/wallet/view.nim
@@ -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)
diff --git a/src/app/wallet/views/account_list.nim b/src/app/wallet/views/account_list.nim
index ec5ba6deee..ffaf0503e8 100644
--- a/src/app/wallet/views/account_list.nim
+++ b/src/app/wallet/views/account_list.nim
@@ -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
diff --git a/src/app/wallet/views/transaction_list.nim b/src/app/wallet/views/transaction_list.nim
index 186f3a9f94..76d580ac41 100644
--- a/src/app/wallet/views/transaction_list.nim
+++ b/src/app/wallet/views/transaction_list.nim
@@ -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
diff --git a/src/status/libstatus/conversions.nim b/src/status/libstatus/conversions.nim
index 878235871e..5eee54698e 100644
--- a/src/status/libstatus/conversions.nim
+++ b/src/status/libstatus/conversions.nim
@@ -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':
diff --git a/src/status/libstatus/core.nim b/src/status/libstatus/core.nim
index 6000d9494f..19f458a9c3 100644
--- a/src/status/libstatus/core.nim
+++ b/src/status/libstatus/core.nim
@@ -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)
diff --git a/src/status/libstatus/types.nim b/src/status/libstatus/types.nim
index 39f5e03b46..787949864a 100644
--- a/src/status/libstatus/types.nim
+++ b/src/status/libstatus/types.nim
@@ -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
diff --git a/src/status/libstatus/utils.nim b/src/status/libstatus/utils.nim
index 411c34c3b8..b6c5650ebf 100644
--- a/src/status/libstatus/utils.nim
+++ b/src/status/libstatus/utils.nim
@@ -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))
diff --git a/src/status/libstatus/wallet.nim b/src/status/libstatus/wallet.nim
index 20be200bfc..534ee70eb3 100644
--- a/src/status/libstatus/wallet.nim
+++ b/src/status/libstatus/wallet.nim
@@ -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,
diff --git a/src/status/tasks/common.nim b/src/status/tasks/common.nim
index ae508e5ed7..23ca501c12 100644
--- a/src/status/tasks/common.nim
+++ b/src/status/tasks/common.nim
@@ -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
diff --git a/src/status/wallet.nim b/src/status/wallet.nim
index 713b1da5e4..094cdcf384 100644
--- a/src/status/wallet.nim
+++ b/src/status/wallet.nim
@@ -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
diff --git a/src/status/wallet/account.nim b/src/status/wallet/account.nim
index 4a11648ca7..c11df4a0a9 100644
--- a/src/status/wallet/account.nim
+++ b/src/status/wallet/account.nim
@@ -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
diff --git a/ui/app/AppLayouts/Browser/BrowserWalletMenu.qml b/ui/app/AppLayouts/Browser/BrowserWalletMenu.qml
index 8dcaf31e44..6ece63540f 100644
--- a/ui/app/AppLayouts/Browser/BrowserWalletMenu.qml
+++ b/ui/app/AppLayouts/Browser/BrowserWalletMenu.qml
@@ -189,7 +189,6 @@ Popup {
anchors.leftMargin: 32
//% "History"
btnText: qsTrId("history")
- onClicked: historyTab.checkIfHistoryIsBeingFetched()
}
}
diff --git a/ui/app/AppLayouts/Wallet/HistoryTab.qml b/ui/app/AppLayouts/Wallet/HistoryTab.qml
index 062434b273..6b679f3b7c 100644
--- a/ui/app/AppLayouts/Wallet/HistoryTab.qml
+++ b/ui/app/AppLayouts/Wallet/HistoryTab.qml
@@ -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
}
}
}
diff --git a/ui/app/AppLayouts/Wallet/WalletLayout.qml b/ui/app/AppLayouts/Wallet/WalletLayout.qml
index 222243ce4a..36a237876e 100644
--- a/ui/app/AppLayouts/Wallet/WalletLayout.qml
+++ b/ui/app/AppLayouts/Wallet/WalletLayout.qml
@@ -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()
}
}
diff --git a/ui/app/AppLayouts/Wallet/components/AddAccount.qml b/ui/app/AppLayouts/Wallet/components/AddAccount.qml
index e929b2f284..e6573e1649 100644
--- a/ui/app/AppLayouts/Wallet/components/AddAccount.qml
+++ b/ui/app/AppLayouts/Wallet/components/AddAccount.qml
@@ -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 {
diff --git a/ui/app/AppLayouts/Wallet/components/AddAccountWithPrivateKey.qml b/ui/app/AppLayouts/Wallet/components/AddAccountWithPrivateKey.qml
index 46dedbf48a..5dd9c6b0ff 100644
--- a/ui/app/AppLayouts/Wallet/components/AddAccountWithPrivateKey.qml
+++ b/ui/app/AppLayouts/Wallet/components/AddAccountWithPrivateKey.qml
@@ -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();
}
}
diff --git a/ui/app/AppLayouts/Wallet/components/AddAccountWithSeed.qml b/ui/app/AppLayouts/Wallet/components/AddAccountWithSeed.qml
index 9fb2c52e79..0fbc724000 100644
--- a/ui/app/AppLayouts/Wallet/components/AddAccountWithSeed.qml
+++ b/ui/app/AppLayouts/Wallet/components/AddAccountWithSeed.qml
@@ -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();
}
diff --git a/ui/app/AppLayouts/Wallet/components/AddWatchOnlyAccount.qml b/ui/app/AppLayouts/Wallet/components/AddWatchOnlyAccount.qml
index d477466aca..192bc101ec 100644
--- a/ui/app/AppLayouts/Wallet/components/AddWatchOnlyAccount.qml
+++ b/ui/app/AppLayouts/Wallet/components/AddWatchOnlyAccount.qml
@@ -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();
}
}
diff --git a/ui/app/AppLayouts/Wallet/components/GenerateAccountModal.qml b/ui/app/AppLayouts/Wallet/components/GenerateAccountModal.qml
index a51939f620..12141cf68f 100644
--- a/ui/app/AppLayouts/Wallet/components/GenerateAccountModal.qml
+++ b/ui/app/AppLayouts/Wallet/components/GenerateAccountModal.qml
@@ -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();
}
}