Skip to content

multi: read List(ACTIVITY) from the canonical store + pagination - #842

Merged
darioAnongba merged 11 commits into
mainfrom
dario/774-list-cutover
Jul 2, 2026
Merged

multi: read List(ACTIVITY) from the canonical store + pagination#842
darioAnongba merged 11 commits into
mainfrom
dario/774-list-cutover

Conversation

@darioAnongba

@darioAnongba darioAnongba commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

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

  • Proto: ActivityList gains has_more + next_cursor; ListRequest gains an opaque cursor. offset stays for the VTXOS/ONCHAIN views and is ignored for ACTIVITY.
  • Read path (swapwallet/history.go): List(ACTIVITY) pages activity_entries by the immutable (created_at_unix, canonical_id) keyset and returns has_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/kinds filters apply over a keyset fill-loop (limit+1 for has_more). No overlay pass (the deadline-timeout FAILED state is already projected into the store) and no dedupe (canonical_id is unique).
  • Merge retained for backfill: the old merge is renamed deriveActivity and 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 via proto.Equal.
  • Interface: darepod.ActivityStore (renamed from ActivityProjector) gains ListEntries.
  • SubscribeWallet: its include_existing snapshot calls List, so it becomes store-backed for free. The resumable event_seq subscribe cursor remains C4/walletdk: durable, resumable SubscribeWallet (monotonic cursor, replay-on-reconnect, no silent drops) #775, out of scope here.

Tests

List reads the store; newest-first order; has_more/next_cursor round-trip across pages; #781 stability (insert an op between pages → no skip/dup); pending_only + kinds filters; malformed-cursor error; rowToWalletEntry round-trip. Existing merge tests still cover deriveActivity via the nil-store fallback.

make rpc / make build / make lint-changed-local and the swapwallet + db unit suites pass.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread swapwallet/history.go
@darioAnongba
darioAnongba force-pushed the dario/774-credit-projector branch from 205ca3e to 623c36c Compare July 1, 2026 17:35
@darioAnongba
darioAnongba force-pushed the dario/774-list-cutover branch from 6875124 to 4ea2269 Compare July 1, 2026 17:36
Base automatically changed from dario/774-credit-projector to main July 1, 2026 18:07
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.
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.
@darioAnongba
darioAnongba force-pushed the dario/774-list-cutover branch from 4ea2269 to e75c4f9 Compare July 1, 2026 18:09
@darioAnongba darioAnongba changed the title multi: read List(ACTIVITY) from the canonical store + pagination (#774, #781) multi: read List(ACTIVITY) from the canonical store + pagination Jul 1, 2026
@darioAnongba darioAnongba self-assigned this Jul 1, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant