diff --git a/nim.cfg b/nim.cfg index c16a8a07be..8b0f41ec61 100644 --- a/nim.cfg +++ b/nim.cfg @@ -1,3 +1,4 @@ # we need to link C++ libraries gcc.linkerexe="g++" +path = "src" \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/activity/controller.nim b/src/app/modules/main/wallet_section/activity/controller.nim new file mode 100644 index 0000000000..d66fbe71fb --- /dev/null +++ b/src/app/modules/main/wallet_section/activity/controller.nim @@ -0,0 +1,131 @@ +import NimQml, logging, std/json, sequtils, sugar, options + +import model +import entry + +import ../transactions/item +import ../transactions/module as transactions_module + +import backend/activity as backend_activity +import backend/backend as backend +import backend/transactions + +import app_service/service/transaction/service as transaction_service + +proc toRef*[T](obj: T): ref T = + new(result) + result[] = obj + +QtObject: + type + Controller* = ref object of QObject + model: Model + transactionsModule: transactions_module.AccessInterface + currentActivityFilter: backend_activity.ActivityFilter + + proc setup(self: Controller) = + self.QObject.setup + + proc delete*(self: Controller) = + self.QObject.delete + + proc newController*(transactionsModule: transactions_module.AccessInterface): Controller = + new(result, delete) + result.model = newModel() + result.transactionsModule = transactionsModule + result.currentActivityFilter = backend_activity.getIncludeAllActivityFilter() + result.setup() + + proc getModel*(self: Controller): QVariant {.slot.} = + return newQVariant(self.model) + + QtProperty[QVariant] model: + read = getModel + + # TODO: move it to service, make it async and lazy load details for transactions + proc backendToPresentation(self: Controller, backendEnties: seq[backend_activity.ActivityEntry]): seq[entry.ActivityEntry] = + var multiTransactionsIds: seq[int] = @[] + var transactionIdentities: seq[backend.TransactionIdentity] = @[] + var pendingTransactionIdentities: seq[backend.TransactionIdentity] = @[] + + # Extract metadata required to fetch details + # TODO: temporary here to show the working API. Will be done as required on a detail request from UI + for backendEntry in backendEnties: + case backendEntry.transactionType: + of MultiTransaction: + multiTransactionsIds.add(backendEntry.id) + of SimpleTransaction: + transactionIdentities.add(backendEntry.transaction.get()) + of PendingTransaction: + pendingTransactionIdentities.add(backendEntry.transaction.get()) + + var multiTransactions: seq[MultiTransactionDto] = @[] + if len(multiTransactionsIds) > 0: + multiTransactions = transaction_service.getMultiTransactions(multiTransactionsIds) + + var transactions: seq[Item] = @[] + if len(transactionIdentities) > 0: + let response = backend.getTransfersForIdentities(transactionIdentities) + let res = response.result + if response.error != nil or res.kind != JArray or res.len == 0: + raise newException(Defect, "failed fetching transaction details") + + let transactionsDtos = res.getElems().map(x => x.toTransactionDto()) + transactions = self.transactionsModule.transactionsToItems(transactionsDtos, @[]) + + var pendingTransactions: seq[Item] = @[] + if len(pendingTransactionIdentities) > 0: + let response = backend.getPendingTransactionsForIdentities(pendingTransactionIdentities) + let res = response.result + if response.error != nil or res.kind != JArray or res.len == 0: + raise newException(Defect, "failed fetching pending transactions details") + + let pendingTransactionsDtos = res.getElems().map(x => x.toPendingTransactionDto()) + pendingTransactions = self.transactionsModule.transactionsToItems(pendingTransactionsDtos, @[]) + + # Merge detailed transaction info in order + result = newSeq[entry.ActivityEntry](multiTransactions.len + transactions.len + pendingTransactions.len) + var mtIndex = 0 + var tIndex = 0 + var ptIndex = 0 + for i in low(backendEnties) .. high(backendEnties): + let backendEntry = backendEnties[i] + case backendEntry.transactionType: + of MultiTransaction: + result[i] = entry.newMultiTransactionActivityEntry(multiTransactions[mtIndex]) + mtIndex += 1 + of SimpleTransaction: + let refInstance = new(Item) + refInstance[] = transactions[tIndex] + result[i] = entry.newTransactionActivityEntry(refInstance, false) + tIndex += 1 + of PendingTransaction: + let refInstance = new(Item) + refInstance[] = pendingTransactions[ptIndex] + result[i] = entry.newTransactionActivityEntry(refInstance, true) + ptIndex += 1 + + proc refreshData*(self: Controller) {.slot.} = + # result type is RpcResponse + let response = backend_activity.getActivityEntries(@["0x0000000000000000000000000000000000000001"], @[1], self.currentActivityFilter, 0, 10) + # RPC returns null for result in case of empty array + if response.error != nil or (response.result.kind != JArray and response.result.kind != JNull): + error "error fetching activity entries: ", response.error + return + + if response.result.kind == JNull: + self.model.setEntries(@[]) + return + + var backendEnties = newSeq[backend_activity.ActivityEntry](response.result.len) + for i in 0 ..< response.result.len: + backendEnties[i] = fromJson(response.result[i], backend_activity.ActivityEntry) + let entries = self.backendToPresentation(backendEnties) + self.model.setEntries(entries) + + # TODO: add all parameters and separate in different methods + proc updateFilter*(self: Controller, startTimestamp: int, endTimestamp: int) {.slot.} = + # Update filter + self.currentActivityFilter.period = backend_activity.newPeriod(startTimestamp, endTimestamp) + + self.refreshData() diff --git a/src/app/modules/main/wallet_section/activity/entry.nim b/src/app/modules/main/wallet_section/activity/entry.nim new file mode 100644 index 0000000000..4609e26808 --- /dev/null +++ b/src/app/modules/main/wallet_section/activity/entry.nim @@ -0,0 +1,127 @@ +import NimQml, tables, json, strformat, sequtils, strutils, logging + +import ../transactions/view +import ../transactions/item +import ./backend/transactions + +# The ActivityEntry contains one of the following instances transaction, pensing transaction or multi-transaction +# It is used to display an activity history entry in the QML UI +QtObject: + type + ActivityEntry* = ref object of QObject + multi_transaction: MultiTransactionDto + transaction: ref Item + isPending: bool + + proc setup(self: ActivityEntry) = + self.QObject.setup + + proc delete*(self: ActivityEntry) = + self.QObject.delete + + proc newMultiTransactionActivityEntry*(mt: MultiTransactionDto): ActivityEntry = + new(result, delete) + result.multi_transaction = mt + result.transaction = nil + result.isPending = false + result.setup() + + proc newTransactionActivityEntry*(tr: ref Item, isPending: bool): ActivityEntry = + new(result, delete) + result.multi_transaction = nil + result.transaction = tr + result.isPending = isPending + result.setup() + + proc isMultiTransaction*(self: ActivityEntry): bool {.slot.} = + return self.multi_transaction != nil + + QtProperty[bool] isMultiTransaction: + read = isMultiTransaction + + proc isPendingTransaction*(self: ActivityEntry): bool {.slot.} = + return (not self.isMultiTransaction()) and self.isPending + + QtProperty[bool] isPendingTransaction: + read = isPendingTransaction + + proc `$`*(self: ActivityEntry): string = + let mtStr = if self.multi_transaction != nil: $(self.multi_transaction.id) else: "0" + let trStr = if self.transaction != nil: $(self.transaction[]) else: "nil" + + return fmt"""ActivityEntry( + multi_transaction.id:{mtStr}, + transaction:{trStr}, + isPending:{self.isPending} + )""" + + proc getMultiTransaction*(self: ActivityEntry): MultiTransactionDto = + if not self.isMultiTransaction(): + raise newException(Defect, "ActivityEntry is not a MultiTransaction") + return self.multi_transaction + + proc getTransaction*(self: ActivityEntry, pending: bool): ref Item = + if self.isMultiTransaction() or self.isPending != pending: + raise newException(Defect, "ActivityEntry is not a " & (if pending: "pending" else: "") & " Transaction") + return self.transaction + + proc getSender*(self: ActivityEntry): string {.slot.} = + # TODO: lookup sender's name from addressbook and cache it or in advance + if self.isMultiTransaction(): + return self.multi_transaction.fromAddress + + return self.transaction[].getfrom() + + QtProperty[string] sender: + read = getSender + + proc getRecipient*(self: ActivityEntry): string {.slot.} = + # TODO: lookup recipient name from addressbook and cache it or in advance + if self.isMultiTransaction(): + return self.multi_transaction.toAddress + + return self.transaction[].getTo() + + QtProperty[string] recipient: + read = getRecipient + + # TODO: use CurrencyAmount? + proc getFromAmount*(self: ActivityEntry): string {.slot.} = + if self.isMultiTransaction(): + return self.multi_transaction.fromAmount + error "getFromAmount: ActivityEntry is not a MultiTransaction" + return "0" + + QtProperty[string] fromAmount: + read = getFromAmount + + proc getToAmount*(self: ActivityEntry): string {.slot.} = + if not self.isMultiTransaction(): + error "getToAmount: ActivityEntry is not a MultiTransaction" + return "0" + + return self.multi_transaction.fromAmount + + QtProperty[string] toAmount: + read = getToAmount + + proc getAmount*(self: ActivityEntry): QVariant {.slot.} = + if not self.isMultiTransaction(): + error "getAmount: ActivityEntry is not an transaction.Item" + return newQVariant(0) + + return newQVariant(self.transaction[].getValue()) + + QtProperty[QVariant] amount: + read = getAmount + + proc getTimestamp*(self: ActivityEntry): int {.slot.} = + if self.isMultiTransaction(): + return self.multi_transaction.timestamp + # TODO: should we account for self.transaction[].isTimeStamp? + return self.transaction[].getTimestamp() + + QtProperty[int] timestamp: + read = getTimestamp + + # TODO: properties - type, fromChains, toChains, fromAsset, toAsset, assetName \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/activity/model.nim b/src/app/modules/main/wallet_section/activity/model.nim new file mode 100644 index 0000000000..061629f92a --- /dev/null +++ b/src/app/modules/main/wallet_section/activity/model.nim @@ -0,0 +1,90 @@ +import NimQml, Tables, strutils, strformat, sequtils + +import ./entry + +# TODO - DEV: remove this +import app_service/service/transaction/dto +import app/modules/shared_models/currency_amount +import ../transactions/item as transaction + +type + ModelRole {.pure.} = enum + ActivityEntryRole = UserRole + 1 + +QtObject: + type + Model* = ref object of QAbstractListModel + entries: seq[ActivityEntry] + hasMore: bool + + proc delete(self: Model) = + self.entries = @[] + self.QAbstractListModel.delete + + proc setup(self: Model) = + self.QAbstractListModel.setup + + proc newModel*(): Model = + new(result, delete) + result.entries = @[] + result.setup + result.hasMore = true + + proc `$`*(self: Model): string = + for i in 0 ..< self.entries.len: + result &= fmt"""[{i}]:({$self.entries[i]})""" + + proc countChanged(self: Model) {.signal.} + + proc getCount*(self: Model): int {.slot.} = + self.entries.len + + QtProperty[int] count: + read = getCount + notify = countChanged + + method rowCount(self: Model, index: QModelIndex = nil): int = + return self.entries.len + + method roleNames(self: Model): Table[int, string] = + { + ModelRole.ActivityEntryRole.int:"activityEntry", + }.toTable + + method data(self: Model, index: QModelIndex, role: int): QVariant = + if (not index.isValid): + return + + if (index.row < 0 or index.row >= self.entries.len): + return + + let entry = self.entries[index.row] + let enumRole = role.ModelRole + + case enumRole: + of ModelRole.ActivityEntryRole: + result = newQVariant(entry) + + proc setEntries*(self: Model, entries: seq[ActivityEntry]) = + self.beginResetModel() + self.entries = entries + self.endResetModel() + self.countChanged() + + # TODO: update data + + # TODO: fetch more + + proc hasMoreChanged*(self: Model) {.signal.} + + proc getHasMore*(self: Model): bool {.slot.} = + return self.hasMore + + proc setHasMore*(self: Model, hasMore: bool) {.slot.} = + self.hasMore = hasMore + self.hasMoreChanged() + + QtProperty[bool] hasMore: + read = getHasMore + write = setHasMore + notify = hasMoreChanged diff --git a/src/app/modules/main/wallet_section/controller.nim b/src/app/modules/main/wallet_section/controller.nim index d1578e28ab..b9d15ec52f 100644 --- a/src/app/modules/main/wallet_section/controller.nim +++ b/src/app/modules/main/wallet_section/controller.nim @@ -15,7 +15,7 @@ type walletAccountService: wallet_account_service.Service currencyService: currency_service.Service networkService: network_service.Service - + proc newController*( delegate: io_interface.AccessInterface, settingsService: settings_service.Service, diff --git a/src/app/modules/main/wallet_section/module.nim b/src/app/modules/main/wallet_section/module.nim index b6c93062a8..f984717f27 100644 --- a/src/app/modules/main/wallet_section/module.nim +++ b/src/app/modules/main/wallet_section/module.nim @@ -16,6 +16,8 @@ import ./networks/module as networks_module import ./overview/module as overview_module import ./send/module as send_module +import ./activity/controller as activityc + import ../../../global/global_singleton import ../../../core/eventemitter import ../../../../app_service/service/keycard/service as keycard_service @@ -34,7 +36,6 @@ import ../../../../app_service/service/network_connection/service as network_con logScope: topics = "wallet-section-module" -import io_interface export io_interface type @@ -42,7 +43,7 @@ type delegate: delegate_interface.AccessInterface events: EventEmitter moduleLoaded: bool - controller: Controller + controller: controller.Controller view: View filter: Filter @@ -61,6 +62,8 @@ type accountsService: accounts_service.Service walletAccountService: wallet_account_service.Service + activityController: activityc.Controller + proc newModule*( delegate: delegate_interface.AccessInterface, events: EventEmitter, @@ -85,7 +88,6 @@ proc newModule*( result.walletAccountService = walletAccountService result.moduleLoaded = false result.controller = newController(result, settingsService, walletAccountService, currencyService, networkService) - result.view = newView(result) result.accountsModule = accounts_module.newModule(result, events, walletAccountService, networkService, currencyService) result.allTokensModule = all_tokens_module.newModule(result, events, tokenService, walletAccountService) @@ -99,6 +101,10 @@ proc newModule*( result.networksModule = networks_module.newModule(result, events, networkService, walletAccountService, settingsService) result.filter = initFilter(result.controller) + result.activityController = activityc.newController(result.transactionsModule) + result.view = newView(result, result.activityController) + + method delete*(self: Module) = self.accountsModule.delete self.allTokensModule.delete @@ -110,6 +116,7 @@ method delete*(self: Module) = self.sendModule.delete self.controller.delete self.view.delete + self.activityController.delete if not self.addAccountModule.isNil: self.addAccountModule.delete diff --git a/src/app/modules/main/wallet_section/transactions/controller.nim b/src/app/modules/main/wallet_section/transactions/controller.nim index fd8b2d517a..d34e607058 100644 --- a/src/app/modules/main/wallet_section/transactions/controller.nim +++ b/src/app/modules/main/wallet_section/transactions/controller.nim @@ -9,6 +9,7 @@ import ../../../shared_modules/keycard_popup/io_interface as keycard_shared_modu import ../../../../core/[main] import ../../../../core/tasks/[qt, threadpool] +import ./backend/transactions type Controller* = ref object of RootObj @@ -100,7 +101,7 @@ proc getWalletAccountByAddress*(self: Controller, address: string): WalletAccoun proc loadTransactions*(self: Controller, address: string, toBlock: Uint256, limit: int = 20, loadMore: bool = false) = self.transactionService.loadTransactions(address, toBlock, limit, loadMore) -proc getAllTransactions*(self: Controller, address: string): seq[TransactionDto] = +proc getAllTransactions*(self: Controller, address: string): seq[TransactionDto] = return self.transactionService.getAllTransactions(address) proc getChainIdForChat*(self: Controller): int = @@ -122,4 +123,4 @@ proc findTokenSymbolByAddress*(self: Controller, address: string): string = return self.walletAccountService.findTokenSymbolByAddress(address) proc getMultiTransactions*(self: Controller, transactionIDs: seq[int]): seq[MultiTransactionDto] = - return self.transactionService.getMultiTransactions(transactionIDs) + return transaction_service.getMultiTransactions(transactionIDs) diff --git a/src/app/modules/main/wallet_section/transactions/io_interface.nim b/src/app/modules/main/wallet_section/transactions/io_interface.nim index cacbf8d4b5..52e486a1a9 100644 --- a/src/app/modules/main/wallet_section/transactions/io_interface.nim +++ b/src/app/modules/main/wallet_section/transactions/io_interface.nim @@ -3,6 +3,8 @@ import ../../../../../app_service/service/collectible/dto as CollectibleDto import ../../../../../app_service/service/transaction/dto export TransactionDto, CollectibleDto +import ./item + type AccessInterface* {.pure inheritable.} = ref object of RootObj ## Abstract class for any input/interaction with this module. @@ -43,13 +45,13 @@ method setIsNonArchivalNode*(self: AccessInterface, isNonArchivalNode: bool) {.b method transactionWasSent*(self: AccessInterface, result: string) {.base.} = raise newException(ValueError, "No implementation available") -method getChainIdForChat*(self: AccessInterface): int = +method getChainIdForChat*(self: AccessInterface): int {.base.} = raise newException(ValueError, "No implementation available") -method getChainIdForBrowser*(self: AccessInterface): int = +method getChainIdForBrowser*(self: AccessInterface): int {.base.} = raise newException(ValueError, "No implementation available") -method refreshTransactions*(self: AccessInterface) {.base.} = +method refreshTransactions*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") # View Delegate Interface @@ -60,3 +62,6 @@ method viewDidLoad*(self: AccessInterface) {.base.} = method getLastTxBlockNumber*(self: AccessInterface): string {.base.} = raise newException(ValueError, "No implementation available") + +method transactionsToItems*(self: AccessInterface, transactions: seq[TransactionDto], collectibles: seq[CollectibleDto]): seq[Item] {.base.} = + raise newException(ValueError, "No implementation available") \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/transactions/item.nim b/src/app/modules/main/wallet_section/transactions/item.nim index 6b89122d4d..47bed10dfc 100644 --- a/src/app/modules/main/wallet_section/transactions/item.nim +++ b/src/app/modules/main/wallet_section/transactions/item.nim @@ -240,6 +240,7 @@ proc getTxStatus*(self: Item): string = proc getValue*(self: Item): CurrencyAmount = return self.value +# TODO: fix naming proc getfrom*(self: Item): string = return self.fro diff --git a/src/app/modules/main/wallet_section/transactions/module.nim b/src/app/modules/main/wallet_section/transactions/module.nim index 5fb45d17bc..92d3a5b3dc 100644 --- a/src/app/modules/main/wallet_section/transactions/module.nim +++ b/src/app/modules/main/wallet_section/transactions/module.nim @@ -59,26 +59,10 @@ proc getResolvedSymbol*(self: Module, transaction: TransactionDto): string = else: result = "ETH" -proc transactionsToItems(self: Module, transactions: seq[TransactionDto], collectibles: seq[CollectibleDto]) : seq[Item] = +method transactionsToItems*(self: Module, transactions: seq[TransactionDto], collectibles: seq[CollectibleDto]): seq[Item] = let gweiFormat = self.controller.getCurrencyFormat("Gwei") let ethFormat = self.controller.getCurrencyFormat("ETH") - # TODO: Continue merging multi-transactions with transactions - # - # let transactionIDs = transactions.filter(t => t.multiTransactionID != MultiTransactionMissingID).map(t => t.multiTransactionID) - # let multiTransactions = self.controller.getMultiTransactions(transactionIDs) - # for mt in multiTransactions: - # let mtItem = multiTransactionToItem(mt) - # - # Tip: depending of the new design best will be to replace the transaction.View - # with a new ActivityEntry that contains eighter a transaction or a multi-transaction - # Refactor transaction Model to serve ActivityEntry istead of Views - # - # Here we should filter all transactions that are part of a multi-transaciton - # and add them to the multi-transaction View associated with an ActivityEntry - # and the remaining "free" transactions to the corresponding ActivityEntry - # TODO: check TransactionsItem changes - transactions.map(t => (block: if t.typeValue == ERC721_TRANSACTION_TYPE: for c in collectibles: @@ -138,7 +122,7 @@ method setHistoryFetchState*(self: Module, addresses: seq[string], isFetching: b method setHistoryFetchState*(self: Module, address: string, allTxLoaded: bool, isFetching: bool) = self.view.setHistoryFetchState(address, allTxLoaded, isFetching) - + method setIsNonArchivalNode*(self: Module, isNonArchivalNode: bool) = self.view.setIsNonArchivalNode(isNonArchivalNode) diff --git a/src/app/modules/main/wallet_section/transactions/multi_transaction_item.nim b/src/app/modules/main/wallet_section/transactions/multi_transaction_item.nim index a8063a75b5..b77b4b6020 100644 --- a/src/app/modules/main/wallet_section/transactions/multi_transaction_item.nim +++ b/src/app/modules/main/wallet_section/transactions/multi_transaction_item.nim @@ -1,9 +1,10 @@ import strformat -import ../../../../../app_service/service/transaction/dto +import ./backend/transactions const MultiTransactionMissingID* = 0 +# TODO: make it a Qt object to be referenced in QML via ActivityView type MultiTransactionItem* = object id: int diff --git a/src/app/modules/main/wallet_section/transactions/utils.nim b/src/app/modules/main/wallet_section/transactions/utils.nim index 29c95fac95..78675af7ce 100644 --- a/src/app/modules/main/wallet_section/transactions/utils.nim +++ b/src/app/modules/main/wallet_section/transactions/utils.nim @@ -10,6 +10,8 @@ import ../../../shared_models/currency_amount import ./item import ./multi_transaction_item +import ./backend/transactions + proc hex2GweiCurrencyAmount(hexValueStr: string, gweiFormat: CurrencyFormatDto): CurrencyAmount = let value = parseFloat(singletonInstance.utils.hex2Gwei(hexValueStr)) return currencyAmountToItem(value, gweiFormat) diff --git a/src/app/modules/main/wallet_section/transactions/view.nim b/src/app/modules/main/wallet_section/transactions/view.nim index 346c3a9d04..44946e1331 100644 --- a/src/app/modules/main/wallet_section/transactions/view.nim +++ b/src/app/modules/main/wallet_section/transactions/view.nim @@ -83,7 +83,7 @@ QtObject: for tx in transactions: if not self.enabledChainIds.contains(tx.getChainId()): continue - + toAddTransactions.add(tx) if not self.models.hasKey(address): diff --git a/src/app/modules/main/wallet_section/view.nim b/src/app/modules/main/wallet_section/view.nim index 0c23799808..a04e33b44e 100644 --- a/src/app/modules/main/wallet_section/view.nim +++ b/src/app/modules/main/wallet_section/view.nim @@ -1,5 +1,6 @@ import NimQml, json +import ./activity/controller as activityc import ./io_interface import ../../shared_models/currency_amount @@ -13,6 +14,7 @@ QtObject: isMnemonicBackedUp: bool tmpAmount: float # shouldn't be used anywhere except in prepare*/getPrepared* procs tmpSymbol: string # shouldn't be used anywhere except in prepare*/getPrepared* procs + activityController: activityc.Controller proc setup(self: View) = self.QObject.setup @@ -20,9 +22,10 @@ QtObject: proc delete*(self: View) = self.QObject.delete - proc newView*(delegate: io_interface.AccessInterface): View = + proc newView*(delegate: io_interface.AccessInterface, activityController: activityc.Controller): View = new(result, delete) result.delegate = delegate + result.activityController = activityController result.setup() proc load*(self: View) = @@ -120,4 +123,9 @@ QtObject: proc destroyAddAccountPopup*(self: View) {.signal.} proc emitDestroyAddAccountPopup*(self: View) = - self.destroyAddAccountPopup() \ No newline at end of file + self.destroyAddAccountPopup() + + proc getActivityController(self: View): QVariant {.slot.} = + return newQVariant(self.activityController) + QtProperty[QVariant] activityController: + read = getActivityController diff --git a/src/app_service/service/transaction/dto.nim b/src/app_service/service/transaction/dto.nim index c7ac3eced8..e05ec9770f 100644 --- a/src/app_service/service/transaction/dto.nim +++ b/src/app_service/service/transaction/dto.nim @@ -1,12 +1,14 @@ import json, strutils, stint, json_serialization, strformat import - web3/ethtypes, json_serialization + web3/ethtypes include ../../common/json_utils import ../network/dto import ../../common/conversion as service_conversion +import ./backend/transactions + type PendingTransactionTypeDto* {.pure.} = enum RegisterENS = "RegisterENS", @@ -20,20 +22,6 @@ type proc event*(self:PendingTransactionTypeDto):string = result = "transaction:" & $self -type - MultiTransactionType* = enum - MultiTransactionSend = 0, MultiTransactionSwap = 1, MultiTransactionBridge = 2 - -type MultiTransactionDto* = ref object of RootObj - id* {.serializedFieldName("id").}: int - timestamp* {.serializedFieldName("timestamp").}: int - fromAddress* {.serializedFieldName("fromAddress").}: string - toAddress* {.serializedFieldName("toAddress").}: string - fromAsset* {.serializedFieldName("fromAsset").}: string - toAsset* {.serializedFieldName("toAsset").}: string - fromAmount* {.serializedFieldName("fromAmount").}: string - multiTxtype* {.serializedFieldName("type").}: MultiTransactionType - type TransactionDto* = ref object of RootObj id*: string diff --git a/src/app_service/service/transaction/service.nim b/src/app_service/service/transaction/service.nim index 68cee343ec..276459e9d8 100644 --- a/src/app_service/service/transaction/service.nim +++ b/src/app_service/service/transaction/service.nim @@ -18,7 +18,7 @@ import ../token/service as token_service import ../settings/service as settings_service import ../collectible/dto import ../eth/dto/transaction as transaction_data_dto -import ../eth/dto/[method_dto, coder, method_dto] +import ../eth/dto/[coder, method_dto] import ./dto as transaction_dto import ./cryptoRampDto import ../eth/utils as eth_utils @@ -158,16 +158,6 @@ QtObject: error "error: ", errDescription return - proc getMultiTransactions*(self: Service, transactionIDs: seq[int]): seq[MultiTransactionDto] = - try: - let response = transactions.getMultiTransactions(transactionIDs).result - - return response.getElems().map(x => x.toMultiTransactionDto()) - except Exception as e: - let errDescription = e.msg - error "error: ", errDescription - return - proc getAllTransactions*(self: Service, address: string): seq[TransactionDto] = if not self.allTransactions.hasKey(address): return @[] @@ -375,7 +365,7 @@ QtObject: fromAsset: tokenSymbol, toAsset: tokenSymbol, fromAmount: "0x" & amountToSend.toHex, - multiTxtype: MultiTransactionType.MultiTransactionSend, + multiTxtype: transactions.MultiTransactionType.MultiTransactionSend, ), paths, password, @@ -442,7 +432,7 @@ QtObject: fromAsset: tokenSymbol, toAsset: tokenSymbol, fromAmount: "0x" & amountToSend.toHex, - multiTxtype: MultiTransactionType.MultiTransactionSend, + multiTxtype: transactions.MultiTransactionType.MultiTransactionSend, ), paths, password, @@ -547,3 +537,13 @@ QtObject: except Exception as e: error "Error getting latest block number", message = e.msg return "" + +proc getMultiTransactions*(transactionIDs: seq[int]): seq[MultiTransactionDto] = + try: + let response = transactions.getMultiTransactions(transactionIDs).result + + return response.getElems().map(x => x.toMultiTransactionDto()) + except Exception as e: + let errDescription = e.msg + error "error: ", errDescription + return diff --git a/src/backend/activity.nim b/src/backend/activity.nim new file mode 100644 index 0000000000..6dde3f4bdc --- /dev/null +++ b/src/backend/activity.nim @@ -0,0 +1,126 @@ +import times, strformat +import json, json_serialization +import options +import ./core, ./response_type +from ./gen import rpc +import ./backend +import transactions + +export response_type + +# TODO: consider using common status-go types via protobuf +# TODO: consider using flags instead of list of enums +type + Period* = object + startTimestamp*: int + endTimestamp*: int + + # see status-go/services/wallet/activity/filter.go Type + ActivityType* {.pure.} = enum + Send, Receive, Buy, Swap, Bridge + + # see status-go/services/wallet/activity/filter.go Status + ActivityStatus* {.pure.} = enum + Failed, Pending, Complete, Finalized + + # see status-go/services/wallet/activity/filter.go TokenType + TokenType* {.pure.} = enum + Asset, Collectibles + + # see status-go/services/wallet/activity/filter.go Filter + ActivityFilter* = object + period* {.serializedFieldName("period").}: Period + types* {.serializedFieldName("types").}: seq[ActivityType] + statuses* {.serializedFieldName("statuses").}: seq[ActivityStatus] + tokenTypes* {.serializedFieldName("tokenTypes").}: seq[TokenType] + counterpartyAddresses* {.serializedFieldName("counterpartyAddresses").}: seq[string] + +proc newPeriod*(startTime: Option[DateTime], endTime: Option[DateTime]): Period = + if startTime.isSome: + result.startTimestamp = startTime.get().toTime().toUnix().int + else: + result.startTimestamp = 0 + if endTime.isSome: + result.endTimestamp = endTime.get().toTime().toUnix().int + else: + result.endTimestamp = 0 + +proc newPeriod*(startTimestamp: int, endTimestamp: int): Period = + result.startTimestamp = startTimestamp + result.endTimestamp = endTimestamp + +proc getIncludeAllActivityFilter*(): ActivityFilter = + result = ActivityFilter(period: newPeriod(none(DateTime), none(DateTime)), types: @[], statuses: @[], tokenTypes: @[], counterpartyAddresses: @[]) + +# Empty sequence for paramters means include all +proc newActivityFilter*(period: Period, activityType: seq[ActivityType], activityStatus: seq[ActivityStatus], tokenType: seq[TokenType], counterpartyAddress: seq[string]): ActivityFilter = + result.period = period + result.types = activityType + result.statuses = activityStatus + result.tokenTypes = tokenType + result.counterpartyAddresses = counterpartyAddress + +# Mirrors status-go/services/wallet/activity/activity.go PayloadType +type + PayloadType* {.pure.} = enum + MultiTransaction = 1 + SimpleTransaction + PendingTransaction + +# Define toJson proc for PayloadType +proc toJson*(x: PayloadType): JsonNode {.inline.} = + return %*(ord(x)) + +# Define fromJson proc for PayloadType +proc fromJson*(x: JsonNode, T: typedesc[PayloadType]): PayloadType {.inline.} = + return cast[PayloadType](x.getInt()) + +# TODO: hide internals behind safe interface +type + ActivityEntry* = object + transactionType* {.serializedFieldName("transactionType").}: PayloadType + transaction* {.serializedFieldName("transaction").}: Option[TransactionIdentity] + id* {.serializedFieldName("id").}: int + timestamp* {.serializedFieldName("timestamp").}: int + activityType* {.serializedFieldName("activityType").}: MultiTransactionType + +proc fromJson[T](jsonObj: JsonNode, TT: typedesc[Option[T]]): Option[T] = + if jsonObj.kind != JNull: + return some(to(jsonObj, T)) + else: + return none(T) + +proc toJson[T](obj: Option[T]): JsonNode = + if obj.isSome: + toJson(obj.get()) + else: + newJNull() + +# Define toJson proc for PayloadType +proc toJson*(ae: ActivityEntry): JsonNode {.inline.} = + return %*(ae) + +# Define fromJson proc for PayloadType +proc fromJson*(e: JsonNode, T: typedesc[ActivityEntry]): ActivityEntry {.inline.} = + result = T( + transactionType: fromJson(e["transactionType"], PayloadType), + transaction: if e.hasKey("transaction"): fromJson(e["transaction"], Option[TransactionIdentity]) else: none(TransactionIdentity), + id: e["id"].getInt(), + timestamp: e["timestamp"].getInt() + ) + +proc `$`*(self: ActivityEntry): string = + let transactionStr = if self.transaction.isSome: $self.transaction.get() else: "none(TransactionIdentity)" + return fmt"""ActivityEntry( + transactionType:{self.transactionType.int}, + transaction:{transactionStr}, + id:{self.id}, + timestamp:{self.timestamp}, + )""" + +rpc(getActivityEntries, "wallet"): + addresses: seq[string] + chainIds: seq[int] + filter: ActivityFilter + offset: int + limit: int \ No newline at end of file diff --git a/src/backend/backend.nim b/src/backend/backend.nim index caadb183a9..ba1553116f 100644 --- a/src/backend/backend.nim +++ b/src/backend/backend.nim @@ -1,4 +1,4 @@ -import json, json_serialization +import json, json_serialization, strformat import ./core, ./response_type from ./gen import rpc @@ -96,6 +96,25 @@ rpc(getTokensBalancesForChainIDs, "wallet"): rpc(getPendingTransactionsByChainIDs, "wallet"): chainIds: seq[int] +type + TransactionIdentity* = ref object + chainId* {.serializedFieldName("chainId").}: int + hash* {.serializedFieldName("hash").}: string + address* {.serializedFieldName("address").}: string + +proc `$`*(self: TransactionIdentity): string = + return fmt"""TransactionIdentity( + chainId:{self.chainId}, + hash:{self.hash}, + address:{self.address}, + )""" + +rpc(getPendingTransactionsForIdentities, "wallet"): + identities = seq[TransactionIdentity] + +rpc(getTransfersForIdentities, "wallet"): + identities = seq[TransactionIdentity] + rpc(getWalletToken, "wallet"): accounts: seq[string] diff --git a/src/backend/transactions.nim b/src/backend/transactions.nim index 7c9617690b..1233e5a919 100644 --- a/src/backend/transactions.nim +++ b/src/backend/transactions.nim @@ -1,10 +1,27 @@ -import json, stint +import json, stint, json_serialization -import ../app_service/service/transaction/dto import ../app_service/service/eth/dto/transaction import ./core as core import ../app_service/common/utils +# mirrors the MultiTransactionType from status-go, services/wallet/transfer/transaction.go +type + MultiTransactionType* = enum + MultiTransactionSend = 0, MultiTransactionSwap = 1, MultiTransactionBridge = 2 + + MultiTransactionDto* = ref object of RootObj + id* {.serializedFieldName("id").}: int + timestamp* {.serializedFieldName("timestamp").}: int + fromAddress* {.serializedFieldName("fromAddress").}: string + toAddress* {.serializedFieldName("toAddress").}: string + fromAsset* {.serializedFieldName("fromAsset").}: string + toAsset* {.serializedFieldName("toAsset").}: string + fromAmount* {.serializedFieldName("fromAmount").}: string + multiTxtype* {.serializedFieldName("type").}: MultiTransactionType + +proc getTransactionByHash*(chainId: int, hash: string): RpcResponse[JsonNode] {.raises: [Exception].} = + core.callPrivateRPCWithChainId("eth_getTransactionByHash", chainId, %* [hash]) + proc checkRecentHistory*(chainIds: seq[int], addresses: seq[string]) {.raises: [Exception].} = let payload = %* [chainIds, addresses] discard core.callPrivateRPC("wallet_checkRecentHistoryForChainIDs", payload) diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index a6529974b2..105a985a71 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -29,6 +29,7 @@ QtObject { property var walletSectionInst: walletSection property var totalCurrencyBalance: walletSection.totalCurrencyBalance + property var activityController: walletSection.activityController property string signingPhrase: walletSection.signingPhrase property string mnemonicBackedUp: walletSection.isMnemonicBackedUp diff --git a/ui/app/AppLayouts/Wallet/views/RightTabView.qml b/ui/app/AppLayouts/Wallet/views/RightTabView.qml index f8cc3c9b48..0238ca3b2a 100644 --- a/ui/app/AppLayouts/Wallet/views/RightTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/RightTabView.qml @@ -83,6 +83,13 @@ Item { width: implicitWidth text: qsTr("Activity") } + // TODO - DEV: remove me + // currentIndex: 3 + // StatusTabButton { + // rightPadding: 0 + // width: implicitWidth + // text: qsTr("DEV activity") + // } } StackLayout { Layout.fillWidth: true @@ -114,6 +121,14 @@ Item { stack.currentIndex = 3 } } + // TODO: replace with the real activity view + // Enable for debugging activity filter + // ActivityView { + // Layout.fillWidth: true + // Layout.fillHeight: true + + // controller: RootStore.activityController + // } } } CollectibleDetailView { diff --git a/ui/imports/shared/views/ActivityView.qml b/ui/imports/shared/views/ActivityView.qml new file mode 100644 index 0000000000..2a5f128043 --- /dev/null +++ b/ui/imports/shared/views/ActivityView.qml @@ -0,0 +1,113 @@ +import QtQuick 2.15 +import QtQml 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +import SortFilterProxyModel 0.2 + +import utils 1.0 + +import "../panels" +import "../popups" +import "../stores" +import "../controls" + +// Temporary developer view to test the filter APIs +Item { + id: root + + property var controller + + ColumnLayout { + anchors.fill: parent + + ColumnLayout { + id: filterLayout + + readonly property int millisInADay: 24 * 60 * 60 * 1000 + property int start: fromSlider.value > 0 ? Math.floor(new Date(new Date() - (fromSlider.value * millisInADay)).getTime() / 1000) : 0 + property int end: toSlider.value > 0 ? Math.floor(new Date(new Date() - (toSlider.value * millisInADay)).getTime() / 1000) : 0 + + function updateFilter() { controller.updateFilter(start, end) } + + RowLayout { + Label { text: "Past Days Span: 100" } + Slider { + id: fromSlider + + Layout.preferredWidth: 200 + Layout.preferredHeight: 50 + + from: 100 + to: 0 + + stepSize: 1 + value: 0 + + onPressedChanged: { if (!pressed) filterLayout.updateFilter() } + } + Label { text: `${fromSlider.value}d - ${toSlider.value}d` } + Slider { + id: toSlider + + Layout.preferredWidth: 200 + Layout.preferredHeight: 50 + + enabled: fromSlider.value > 1 + + from: fromSlider.value - 1 + to: 0 + + stepSize: 1 + value: 0 + + onPressedChanged: { if (!pressed) filterLayout.updateFilter() } + } + Label { text: "0" } + } + Label { text: `Interval: ${filterLayout.start > 0 ? root.epochToDateStr(filterLayout.start) : "all time"} - ${filterLayout.end > 0 ? root.epochToDateStr(filterLayout.end) : "now"}` } + } + + ListView { + id: listView + + Layout.fillWidth: true + Layout.fillHeight: true + + model: controller.model + + delegate: Item { + width: parent ? parent.width : 0 + height: itemLayout.implicitHeight + + readonly property var entry: model.activityEntry + + RowLayout { + id: itemLayout + anchors.fill: parent + + Label { text: entry.isMultiTransaction ? "MT" : entry.isPendingTransaction ? "PT" : " T" } + Label { text: `[${root.epochToDateStr(entry.timestamp)}] ` } + Label { text: entry.isMultiTransaction ? entry.fromAmount : entry.amount } + Label { text: "from"; Layout.leftMargin: 5; Layout.rightMargin: 5 } + Label { text: entry.sender; Layout.maximumWidth: 200; elide: Text.ElideMiddle } + Label { text: "to"; Layout.leftMargin: 5; Layout.rightMargin: 5 } + Label { text: entry.recipient; Layout.maximumWidth: 200; elide: Text.ElideMiddle } + Label { text: "got"; Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction } + Label { text: entry.toAmount; Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction } + RowLayout {} // Spacer + } + } + } + } + + function epochToDateStr(epochTimestamp) { + var date = new Date(epochTimestamp * 1000); + return date.toLocaleString(Qt.locale(), "dd-MM-yyyy hh:mm"); + } +} diff --git a/ui/imports/shared/views/qmldir b/ui/imports/shared/views/qmldir index 2f929c0d4c..2311441b5c 100644 --- a/ui/imports/shared/views/qmldir +++ b/ui/imports/shared/views/qmldir @@ -13,3 +13,4 @@ TokenListView 1.0 TokenListView.qml TransactionPreview 1.0 TransactionPreview.qml TransactionSigner 1.0 TransactionSigner.qml TransactionStackView 1.0 TransactionStackView.qml +ActivityView 1.0 ActivityView.qml