multi: read List(ACTIVITY) from the canonical store + pagination - #842
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements cursor-based pagination for the wallet's activity history, transitioning from a derive-on-read merge to querying a canonical activity store. It updates the protobuf definitions to support pagination cursors and adds the necessary database retrieval logic. Feedback on the changes suggests optimizing the database query in listActivity by dynamically calculating the remaining number of entries needed to fill the page rather than always requesting limit + 1 rows.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
205ca3e to
623c36c
Compare
6875124 to
4ea2269
Compare
Add has_more + next_cursor to ActivityList and an opaque cursor to ListRequest so the ACTIVITY view can page by a stable keyset instead of a drifting offset.
Regenerated via make rpc.
Cut the ACTIVITY read path over from the derive-on-read merge to the persisted activity store: List pages activity_entries by the immutable (created_at, canonical_id) keyset and returns has_more + next_cursor, so paging a concurrently-growing feed neither skips nor duplicates. The merge is retained as deriveActivity, used only by the startup backfill; a nil store falls back to it. Widen the daemon activity-store interface with ListEntries and add rowToWalletEntry, the lossless inverse of the projection mapping. SubscribeWallet's snapshot is store-backed for free.
4ea2269 to
e75c4f9
Compare
The wallet Status summary's pending count must reflect the whole feed, but the paginated List path caps its total at one page. Add a COUNT-by-status query so the full pending count can be read directly.
Generated by make sqlc from the new CountActivityEntriesByStatus query. No schema change.
Status.pending_count read resp.GetActivity().GetTotal() from a single max-sized List page. After cursor pagination that total is a per-page count, so a wallet with more pending entries than one page under-reported its pending count. Add CountByStatus to the activity store and the darepod.ActivityStore interface, and route the count through history.countPending: it counts the store directly when wired and falls back to the derived merge total when it is not.
The ACTIVITY view paginates by an opaque cursor, but the CLI, MCP, and SDK surfaces only carried offset and dropped has_more/next_cursor, so no client could fetch a second page. Thread cursor through the SDK ListRequest, the MCP activity tool, and the darepocli activity command, and surface has_more/next_cursor on the SDK ActivityList and the CLI table/expanded output.
#842 cut List(ACTIVITY) over to the canonical store. These fixes make that read path correct, bounded, and forward-compatible: - Do not persist the synthetic boarding-unconfirmed row. It is ephemeral live state recomputed from GetBalance with no durable id, so a delete-free upsert store could never clear it once the deposit confirms under its real txid:vout id. It lingered as a phantom PENDING row in List and inflated the Status pending count. - Bound the per-request scan on a filtered page. pending_only/kinds are applied in Go after decode, so a selective filter over a large table could scan and protojson-decode the whole table for one page. Cap scanned rows per call and return a cursor to resume. - Reject a cursor whose decoded timestamp is <= 0 rather than let it collide with the return-all sentinel and silently restart paging. - Decode stored requests with DiscardUnknown so a row written by a newer daemon still decodes; a genuinely corrupt row still fails loudly rather than being silently skipped.
forceUnroll emitted the EXIT row to live subscribers but never projected it, so a store-backed List missed a user-initiated unilateral exit until the next startup backfill. Route it through projectAndEmit off a cancel-safe context, mirroring the cooperative leave and credit-pay paths. The terminal transition remains backfill-only, consistent with the cooperative-leave handling.
The ACTIVITY view paginates by cursor and ignores offset, but the CLI and MCP activity surfaces still advertised and forwarded --offset, so a caller passing it was silently served page one. Remove offset from the activity command and the MCP activity tool; cursor is the only activity pager.
The package doc still claimed the read path was unchanged and List still derived from the live merge. That is now false and masked the completeness gap. State that List and the SubscribeWallet snapshot read the store by keyset cursor, that deriveActivity is fallback and backfill only, and enumerate the producers still seeded only by the startup backfill.
Third PR in the C1 series (#774), stacked on #840 (which stacks on #817).
Cuts the wallet ACTIVITY read path over from the derive-on-read merge to the persisted canonical store, and fixes activity pagination (#781). With every producer now writing the store (#817 + #840) and the startup backfill seeding it,
List(ACTIVITY)can read the store directly.What changes
ActivityListgainshas_more+next_cursor;ListRequestgains an opaquecursor.offsetstays for the VTXOS/ONCHAIN views and is ignored for ACTIVITY.swapwallet/history.go):List(ACTIVITY)pagesactivity_entriesby the immutable(created_at_unix, canonical_id)keyset and returnshas_more+next_cursor. Because the sort key never mutates, paging a concurrently-growing feed neither skips nor duplicates rows (the walletdk: ACTIVITY paging lacks has_more + a stable cursor and has a bounded merge window #781 acceptance criterion), and the single-table read removes the bounded merge-window truncation of deep history.pending_only/kindsfilters apply over a keyset fill-loop (limit+1 forhas_more). No overlay pass (the deadline-timeout FAILED state is already projected into the store) and no dedupe (canonical_idis unique).deriveActivityand is now used only by the startup backfill. A nil store (tests without a DB) falls back to it, so existing behavior is unchanged there.rowToWalletEntry: the lossless inverse of the projection mapping (hex-encode BLOB handles, decode the request oneof from protojson). Verified round-trip viaproto.Equal.darepod.ActivityStore(renamed fromActivityProjector) gainsListEntries.include_existingsnapshot callsList, so it becomes store-backed for free. The resumableevent_seqsubscribe cursor remains C4/walletdk: durable, resumable SubscribeWallet (monotonic cursor, replay-on-reconnect, no silent drops) #775, out of scope here.Tests
Listreads the store; newest-first order;has_more/next_cursorround-trip across pages; #781 stability (insert an op between pages → no skip/dup);pending_only+kindsfilters; malformed-cursor error;rowToWalletEntryround-trip. Existing merge tests still coverderiveActivityvia the nil-store fallback.make rpc/make build/make lint-changed-localand theswapwallet+dbunit suites pass.