feat(wallet): make filter API async

Bump status-go to include the async API changes
Handle the wallet-activity-filtering-done event
Propagate the has more flag
Extend the debugging UX

Update #10994
This commit is contained in:
Stefan 2023-06-09 02:02:39 +02:00 committed by Stefan Dunca
parent c21c7cf705
commit 43c7258328
6 changed files with 171 additions and 53 deletions

View File

@ -7,6 +7,9 @@ import entry
import ../transactions/item import ../transactions/item
import ../transactions/module as transactions_module import ../transactions/module as transactions_module
import app/core/eventemitter
import app/core/signals/types
import backend/activity as backend_activity import backend/activity as backend_activity
import backend/backend as backend import backend/backend as backend
import backend/transactions import backend/transactions
@ -17,12 +20,20 @@ proc toRef*[T](obj: T): ref T =
new(result) new(result)
result[] = obj result[] = obj
const FETCH_BATCH_COUNT_DEFAULT = 10
QtObject: QtObject:
type type
Controller* = ref object of QObject Controller* = ref object of QObject
model: Model model: Model
transactionsModule: transactions_module.AccessInterface transactionsModule: transactions_module.AccessInterface
currentActivityFilter: backend_activity.ActivityFilter currentActivityFilter: backend_activity.ActivityFilter
events: EventEmitter
loadingData: bool
errorCode: backend_activity.ErrorCode
# TODO remove chains and addresses to use the app one # TODO remove chains and addresses to use the app one
addresses: seq[string] addresses: seq[string]
chainIds: seq[int] chainIds: seq[int]
@ -33,27 +44,20 @@ QtObject:
proc delete*(self: Controller) = proc delete*(self: Controller) =
self.QObject.delete 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.} = proc getModel*(self: Controller): QVariant {.slot.} =
return newQVariant(self.model) return newQVariant(self.model)
QtProperty[QVariant] model: QtProperty[QVariant] model:
read = getModel read = getModel
# TODO: move it to service, make it async and lazy load details for transactions
proc backendToPresentation(self: Controller, backendEntities: seq[backend_activity.ActivityEntry]): seq[entry.ActivityEntry] = proc backendToPresentation(self: Controller, backendEntities: seq[backend_activity.ActivityEntry]): seq[entry.ActivityEntry] =
var multiTransactionsIds: seq[int] = @[] var multiTransactionsIds: seq[int] = @[]
var transactionIdentities: seq[backend.TransactionIdentity] = @[] var transactionIdentities: seq[backend.TransactionIdentity] = @[]
var pendingTransactionIdentities: seq[backend.TransactionIdentity] = @[] var pendingTransactionIdentities: seq[backend.TransactionIdentity] = @[]
# Extract metadata required to fetch details # 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 # TODO: temporary here to show the working API. Details for each entry will be done as required
# on a detail request from UI after metadata is extended to include the required info
for backendEntry in backendEntities: for backendEntry in backendEntities:
case backendEntry.payloadType: case backendEntry.payloadType:
of MultiTransaction: of MultiTransaction:
@ -122,25 +126,38 @@ QtObject:
error "failed to find pending transaction with identity: ", identity error "failed to find pending transaction with identity: ", identity
ptIndex += 1 ptIndex += 1
proc refreshData(self: Controller) = proc loadingDataChanged*(self: Controller) {.signal.}
# result type is RpcResponse proc setLoadingData(self: Controller, loadingData: bool) =
let response = backend_activity.getActivityEntries(self.addresses, self.chainIds, self.currentActivityFilter, 0, 10) self.loadingData = loadingData
# RPC returns null for result in case of empty array self.loadingDataChanged()
if response.error != nil or (response.result.kind != JArray and response.result.kind != JNull):
proc errorCodeChanged*(self: Controller) {.signal.}
proc setErrorCode(self: Controller, errorCode: int) =
self.errorCode = backend_activity.ErrorCode(errorCode)
self.errorCodeChanged()
proc refreshData(self: Controller) =
let response = backend_activity.filterActivityAsync(self.addresses, self.chainIds, self.currentActivityFilter, 0, FETCH_BATCH_COUNT_DEFAULT)
if response.error != nil:
error "error fetching activity entries: ", response.error error "error fetching activity entries: ", response.error
return return
if response.result.kind == JNull: self.setLoadingData(true)
self.model.setEntries(@[])
proc processResponse(self: Controller, response: JsonNode) =
let res = fromJson(response, backend_activity.FilterResponse)
defer: self.setErrorCode(res.errorCode.int)
if res.errorCode != ErrorCodeSuccess:
error "error fetching activity entries: ", res.errorCode
return return
var backendEntities = newSeq[backend_activity.ActivityEntry](response.result.len) let entries = self.backendToPresentation(res.activities)
for i in 0 ..< response.result.len: self.model.setEntries(entries, res.thereMightBeMore)
backendEntities[i] = fromJson(response.result[i], backend_activity.ActivityEntry) self.setLoadingData(false)
let entries = self.backendToPresentation(backendEntities)
self.model.setEntries(entries)
proc updateFilter*(self: Controller) {.slot.} = proc updateFilter*(self: Controller) {.slot.} =
self.refreshData() self.refreshData()
@ -160,6 +177,31 @@ QtObject:
self.currentActivityFilter.types = types self.currentActivityFilter.types = types
proc newController*(transactionsModule: transactions_module.AccessInterface, events: EventEmitter): Controller =
new(result, delete)
result.model = newModel()
result.transactionsModule = transactionsModule
result.currentActivityFilter = backend_activity.getIncludeAllActivityFilter()
result.events = events
result.setup()
let controller = result
proc handleEvent(e: Args) =
var data = WalletSignal(e)
case data.eventType:
of backend_activity.eventActivityFilteringDone:
let responseJson = parseJson(data.message)
if responseJson.kind != JObject:
error "unexpected json type", responseJson.kind
return
controller.processResponse(responseJson)
else:
discard
result.events.on(SignalType.Wallet.event, handleEvent)
proc setFilterStatus*(self: Controller, statusesArrayJsonString: string) {.slot.} = proc setFilterStatus*(self: Controller, statusesArrayJsonString: string) {.slot.} =
let statusesJson = parseJson(statusesArrayJsonString) let statusesJson = parseJson(statusesArrayJsonString)
if statusesJson.kind != JArray: if statusesJson.kind != JArray:
@ -221,3 +263,17 @@ QtObject:
chainIds[i] = chainIdsJson[i].getInt() chainIds[i] = chainIdsJson[i].getInt()
self.chainIds = chainIds self.chainIds = chainIds
proc getLoadingData*(self: Controller): bool {.slot.} =
return self.loadingData
QtProperty[bool] loadingData:
read = getLoadingData
notify = loadingDataChanged
proc getErrorCode*(self: Controller): int {.slot.} =
return self.errorCode.int
QtProperty[int] errorCode:
read = getErrorCode
notify = errorCodeChanged

View File

@ -65,26 +65,24 @@ QtObject:
of ModelRole.ActivityEntryRole: of ModelRole.ActivityEntryRole:
result = newQVariant(entry) result = newQVariant(entry)
proc setEntries*(self: Model, entries: seq[ActivityEntry]) = proc hasMoreChanged*(self: Model) {.signal.}
proc setHasMore(self: Model, hasMore: bool) {.slot.} =
self.hasMore = hasMore
self.hasMoreChanged()
proc setEntries*(self: Model, entries: seq[ActivityEntry], hasMore: bool) =
self.beginResetModel() self.beginResetModel()
self.entries = entries self.entries = entries
self.endResetModel() self.endResetModel()
self.countChanged() self.countChanged()
self.setHasMore(hasMore)
# TODO: update data
# TODO: fetch more # TODO: fetch more
proc hasMoreChanged*(self: Model) {.signal.}
proc getHasMore*(self: Model): bool {.slot.} = proc getHasMore*(self: Model): bool {.slot.} =
return self.hasMore return self.hasMore
proc setHasMore*(self: Model, hasMore: bool) {.slot.} =
self.hasMore = hasMore
self.hasMoreChanged()
QtProperty[bool] hasMore: QtProperty[bool] hasMore:
read = getHasMore read = getHasMore
write = setHasMore
notify = hasMoreChanged notify = hasMoreChanged

View File

@ -101,7 +101,7 @@ proc newModule*(
result.networksModule = networks_module.newModule(result, events, networkService, walletAccountService, settingsService) result.networksModule = networks_module.newModule(result, events, networkService, walletAccountService, settingsService)
result.filter = initFilter(result.controller) result.filter = initFilter(result.controller)
result.activityController = activityc.newController(result.transactionsModule) result.activityController = activityc.newController(result.transactionsModule, events)
result.view = newView(result, result.activityController) result.view = newView(result, result.activityController)

View File

@ -10,6 +10,9 @@ export response_type
# see status-go/services/wallet/activity/filter.go NoLimitTimestampForPeriod # see status-go/services/wallet/activity/filter.go NoLimitTimestampForPeriod
const noLimitTimestampForPeriod = 0 const noLimitTimestampForPeriod = 0
# Declared in services/wallet/activity/service.go
const eventActivityFilteringDone*: string = "wallet-activity-filtering-done"
# TODO: consider using common status-go types via protobuf # TODO: consider using common status-go types via protobuf
# TODO: consider using flags instead of list of enums # TODO: consider using flags instead of list of enums
type type
@ -146,6 +149,17 @@ type
activityStatus*: ActivityStatus activityStatus*: ActivityStatus
tokenType*: TokenType tokenType*: TokenType
# Mirrors services/wallet/activity/service.go ErrorCode
ErrorCode* = enum
ErrorCodeSuccess = 1,
ErrorCodeFilterCanceled,
ErrorCodeFilterFailed
FilterResponse* = object
activities*: seq[ActivityEntry]
thereMightBeMore*: bool
errorCode*: ErrorCode
# Define toJson proc for PayloadType # Define toJson proc for PayloadType
proc toJson*(ae: ActivityEntry): JsonNode {.inline.} = proc toJson*(ae: ActivityEntry): JsonNode {.inline.} =
return %*(ae) return %*(ae)
@ -174,7 +188,22 @@ proc `$`*(self: ActivityEntry): string =
tokenType* {$self.tokenType}, tokenType* {$self.tokenType},
)""" )"""
rpc(getActivityEntries, "wallet"): proc fromJson*(e: JsonNode, T: typedesc[FilterResponse]): FilterResponse {.inline.} =
var backendEntities: seq[ActivityEntry]
if e.hasKey("activities"):
let jsonEntries = e["activities"]
backendEntities = newSeq[ActivityEntry](jsonEntries.len)
for i in 0 ..< jsonEntries.len:
backendEntities[i] = fromJson(jsonEntries[i], ActivityEntry)
result = T(
activities: backendEntities,
thereMightBeMore: if e.hasKey("thereMightBeMore"): e["thereMightBeMore"].getBool()
else: false,
errorCode: ErrorCode(e["errorCode"].getInt())
)
rpc(filterActivityAsync, "wallet"):
addresses: seq[string] addresses: seq[string]
chainIds: seq[int] chainIds: seq[int]
filter: ActivityFilter filter: ActivityFilter

View File

@ -142,7 +142,7 @@ Control {
id: timeFilterLayout id: timeFilterLayout
RowLayout { RowLayout {
Label { text: "Past Days Span: 100" } Label { text: qsTr("Past Days Span: 100") }
Slider { Slider {
id: fromSlider id: fromSlider
@ -170,12 +170,12 @@ Control {
stepSize: 1 stepSize: 1
value: 0 value: 0
} }
Label { text: "0" } Label { text: qsTr("0") }
} }
Label { text: `Interval: ${d.start > 0 ? root.epochToDateStr(d.start) : "all time"} - ${d.end > 0 ? root.epochToDateStr(d.end) : "now"}` } Label { text: `Interval: ${d.start > 0 ? root.epochToDateStr(d.start) : qsTr("all time")} - ${d.end > 0 ? root.epochToDateStr(d.end) : qsTr("now")}` }
} }
RowLayout { RowLayout {
Label { text: "Type" } Label { text: qsTr("Type") }
// Models the ActivityType // Models the ActivityType
ListModel { ListModel {
id: typeModel id: typeModel
@ -198,7 +198,7 @@ Control {
delegate: ItemOnOffDelegate {} delegate: ItemOnOffDelegate {}
} }
Label { text: "Status" } Label { text: qsTr("Status") }
// ActivityStatus // ActivityStatus
ListModel { ListModel {
id: statusModel id: statusModel
@ -219,7 +219,7 @@ Control {
delegate: ItemOnOffDelegate {} delegate: ItemOnOffDelegate {}
} }
Label { text: "To addresses" } Label { text: qsTr("To addresses") }
TextField { TextField {
id: toAddressesInput id: toAddressesInput
@ -235,7 +235,7 @@ Control {
} }
RowLayout { RowLayout {
Label { text: "Addresses" } Label { text: qsTr("Addresses") }
TextField { TextField {
id: addressesInput id: addressesInput
@ -244,7 +244,7 @@ Control {
placeholderText: qsTr("0x1234, 0x5678, ...") placeholderText: qsTr("0x1234, 0x5678, ...")
} }
Label { text: "Chains" } Label { text: qsTr("Chains") }
ComboBox { ComboBox {
displayText: qsTr("Select chains") displayText: qsTr("Select chains")
@ -256,7 +256,7 @@ Control {
delegate: ItemOnOffDelegate {} delegate: ItemOnOffDelegate {}
} }
Label { text: "Assets" } Label { text: qsTr("Assets") }
ComboBox { ComboBox {
displayText: assetsLoader.status != Loader.Ready ? qsTr("Loading...") : qsTr("Select an asset") displayText: assetsLoader.status != Loader.Ready ? qsTr("Loading...") : qsTr("Select an asset")
@ -321,6 +321,16 @@ Control {
} }
} }
Text {
id: loadingText
Layout.fillWidth: true
Layout.fillHeight: true
text: qsTr("Loading...")
visible: controller.loadingData
horizontalAlignment: Text.AlignHCenter
}
ListView { ListView {
id: listView id: listView
@ -328,6 +338,7 @@ Control {
Layout.fillHeight: true Layout.fillHeight: true
model: controller.model model: controller.model
visible: !controller.loadingData
delegate: Item { delegate: Item {
width: parent ? parent.width : 0 width: parent ? parent.width : 0
@ -339,25 +350,25 @@ Control {
id: itemLayout id: itemLayout
anchors.fill: parent anchors.fill: parent
Label { text: entry.isMultiTransaction ? "MT" : entry.isPendingTransaction ? "PT" : " T" } Label { text: entry.isMultiTransaction ? qsTr("MT") : entry.isPendingTransaction ? qsTr("PT") : qsTr(" T") }
Label { text: `[${root.epochToDateStr(entry.timestamp)}] ` } Label { text: `[${root.epochToDateStr(entry.timestamp)}] ` }
Label { text: entry.isMultiTransaction ? entry.fromAmount : entry.amount } Label { text: entry.isMultiTransaction ? entry.fromAmount : entry.amount }
Label { text: "from"; Layout.leftMargin: 5; Layout.rightMargin: 5 } Label { text: qsTr("from"); Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.sender; Layout.maximumWidth: 200; elide: Text.ElideMiddle } Label { text: entry.sender; Layout.maximumWidth: 200; elide: Text.ElideMiddle }
Label { text: "to"; Layout.leftMargin: 5; Layout.rightMargin: 5 } Label { text: qsTr("to"); Layout.leftMargin: 5; Layout.rightMargin: 5 }
Label { text: entry.recipient; Layout.maximumWidth: 200; elide: Text.ElideMiddle } Label { text: entry.recipient; Layout.maximumWidth: 200; elide: Text.ElideMiddle }
Label { text: "got"; Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction } Label { text: qsTr("got"); Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction }
Label { text: entry.toAmount; Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction } Label { text: entry.toAmount; Layout.leftMargin: 5; Layout.rightMargin: 5; visible: entry.isMultiTransaction }
Label { Label {
text: `{${ text: `{${
function() { function() {
switch (entry.status) { switch (entry.status) {
case Constants.TransactionStatus.Failed: return "F"; case Constants.TransactionStatus.Failed: return qsTr("F");
case Constants.TransactionStatus.Pending: return "P"; case Constants.TransactionStatus.Pending: return qsTr("P");
case Constants.TransactionStatus.Complete: return "C"; case Constants.TransactionStatus.Complete: return qsTr("C");
case Constants.TransactionStatus.Finalized: return "FZ"; case Constants.TransactionStatus.Finalized: return qsTr("FZ");
} }
return "-" return qsTr("-")
}()}}` }()}}`
Layout.leftMargin: 5; Layout.leftMargin: 5;
} }
@ -365,6 +376,30 @@ Control {
RowLayout {} // Spacer RowLayout {} // Spacer
} }
} }
footer: Component {
Item {
width: listView.width
height: footerText.implicitHeight
Text {
id: footerText
text: qsTr("Loading more items...")
anchors.centerIn: parent
}
visible: controller.model.hasMore
// Load more items when this footer comes into view.
onVisibleChanged: {
if (visible) {
controller.loadMoreItems();
}
}
}
}
ScrollBar.vertical: ScrollBar {}
} }
} }

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 1a2ca21070456e153aac27b8a14afa14006652ef Subproject commit d8eb038d7d7026aeece4775771bbb4bcf00c88dc