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/module as transactions_module
import app/core/eventemitter
import app/core/signals/types
import backend/activity as backend_activity
import backend/backend as backend
import backend/transactions
@ -17,12 +20,20 @@ proc toRef*[T](obj: T): ref T =
new(result)
result[] = obj
const FETCH_BATCH_COUNT_DEFAULT = 10
QtObject:
type
Controller* = ref object of QObject
model: Model
transactionsModule: transactions_module.AccessInterface
currentActivityFilter: backend_activity.ActivityFilter
events: EventEmitter
loadingData: bool
errorCode: backend_activity.ErrorCode
# TODO remove chains and addresses to use the app one
addresses: seq[string]
chainIds: seq[int]
@ -33,27 +44,20 @@ QtObject:
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, backendEntities: 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
# 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:
case backendEntry.payloadType:
of MultiTransaction:
@ -122,25 +126,38 @@ QtObject:
error "failed to find pending transaction with identity: ", identity
ptIndex += 1
proc refreshData(self: Controller) =
proc loadingDataChanged*(self: Controller) {.signal.}
# result type is RpcResponse
let response = backend_activity.getActivityEntries(self.addresses, self.chainIds, 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):
proc setLoadingData(self: Controller, loadingData: bool) =
self.loadingData = loadingData
self.loadingDataChanged()
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
return
if response.result.kind == JNull:
self.model.setEntries(@[])
self.setLoadingData(true)
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
var backendEntities = newSeq[backend_activity.ActivityEntry](response.result.len)
for i in 0 ..< response.result.len:
backendEntities[i] = fromJson(response.result[i], backend_activity.ActivityEntry)
let entries = self.backendToPresentation(backendEntities)
self.model.setEntries(entries)
let entries = self.backendToPresentation(res.activities)
self.model.setEntries(entries, res.thereMightBeMore)
self.setLoadingData(false)
proc updateFilter*(self: Controller) {.slot.} =
self.refreshData()
@ -160,6 +177,31 @@ QtObject:
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.} =
let statusesJson = parseJson(statusesArrayJsonString)
if statusesJson.kind != JArray:
@ -221,3 +263,17 @@ QtObject:
chainIds[i] = chainIdsJson[i].getInt()
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:
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.entries = entries
self.endResetModel()
self.countChanged()
# TODO: update data
self.setHasMore(hasMore)
# 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

View File

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

View File

@ -10,6 +10,9 @@ export response_type
# see status-go/services/wallet/activity/filter.go NoLimitTimestampForPeriod
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 flags instead of list of enums
type
@ -146,6 +149,17 @@ type
activityStatus*: ActivityStatus
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
proc toJson*(ae: ActivityEntry): JsonNode {.inline.} =
return %*(ae)
@ -174,7 +188,22 @@ proc `$`*(self: ActivityEntry): string =
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]
chainIds: seq[int]
filter: ActivityFilter

View File

@ -142,7 +142,7 @@ Control {
id: timeFilterLayout
RowLayout {
Label { text: "Past Days Span: 100" }
Label { text: qsTr("Past Days Span: 100") }
Slider {
id: fromSlider
@ -170,12 +170,12 @@ Control {
stepSize: 1
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 {
Label { text: "Type" }
Label { text: qsTr("Type") }
// Models the ActivityType
ListModel {
id: typeModel
@ -198,7 +198,7 @@ Control {
delegate: ItemOnOffDelegate {}
}
Label { text: "Status" }
Label { text: qsTr("Status") }
// ActivityStatus
ListModel {
id: statusModel
@ -219,7 +219,7 @@ Control {
delegate: ItemOnOffDelegate {}
}
Label { text: "To addresses" }
Label { text: qsTr("To addresses") }
TextField {
id: toAddressesInput
@ -235,7 +235,7 @@ Control {
}
RowLayout {
Label { text: "Addresses" }
Label { text: qsTr("Addresses") }
TextField {
id: addressesInput
@ -244,7 +244,7 @@ Control {
placeholderText: qsTr("0x1234, 0x5678, ...")
}
Label { text: "Chains" }
Label { text: qsTr("Chains") }
ComboBox {
displayText: qsTr("Select chains")
@ -256,7 +256,7 @@ Control {
delegate: ItemOnOffDelegate {}
}
Label { text: "Assets" }
Label { text: qsTr("Assets") }
ComboBox {
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 {
id: listView
@ -328,6 +338,7 @@ Control {
Layout.fillHeight: true
model: controller.model
visible: !controller.loadingData
delegate: Item {
width: parent ? parent.width : 0
@ -339,25 +350,25 @@ Control {
id: itemLayout
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: 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: "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: "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: `{${
function() {
switch (entry.status) {
case Constants.TransactionStatus.Failed: return "F";
case Constants.TransactionStatus.Pending: return "P";
case Constants.TransactionStatus.Complete: return "C";
case Constants.TransactionStatus.Finalized: return "FZ";
case Constants.TransactionStatus.Failed: return qsTr("F");
case Constants.TransactionStatus.Pending: return qsTr("P");
case Constants.TransactionStatus.Complete: return qsTr("C");
case Constants.TransactionStatus.Finalized: return qsTr("FZ");
}
return "-"
return qsTr("-")
}()}}`
Layout.leftMargin: 5;
}
@ -365,6 +376,30 @@ Control {
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