Skip to content

Add paginated VTXO listing with filter support#175

Open
sekulicd wants to merge 5 commits into
arkade-os:masterfrom
sekulicd:feat/pagination-vtxo-list
Open

Add paginated VTXO listing with filter support#175
sekulicd wants to merge 5 commits into
arkade-os:masterfrom
sekulicd:feat/pagination-vtxo-list

Conversation

@sekulicd
Copy link
Copy Markdown
Contributor

@sekulicd sekulicd commented May 14, 2026

Summary

  • Adds offset-based pagination to VTXO listing via Page{PageNum, PageSize} with MaxPageSize=200
  • Merges ListVtxos and ListSpendableVtxos into a single ListVtxos(ctx, page, filter) method
  • Introduces VtxoFilter enum: All, Spendable, Spent, Recoverable
  • Page{} zero-value means "return all" — backward compatible for internal callers
  • SQL pagination uses subquery on vtxo table (not the asset_vtxo_vw view) to correctly handle multi-asset VTXOs at page boundaries

Motivation

Addresses the root cause behind ArkLabsHQ/fulmine#405, where unbounded GetVtxos responses exceed gRPC's default 4 MiB receive limit
on wallets with large VTXO history (12k+ VTXOs observed in production). Raising the gRPC limit is a bandage — pagination is the proper fix.

API Changes (breaking)

// Before (two methods, no pagination):
ListVtxos(ctx) (spendable, spent []Vtxo, err error)                                                                                                                                       
ListSpendableVtxos(ctx) ([]Vtxo, error)                                                                                                                                                   
                                                                                                                                                                                          
// After (single method with pagination + filter):                                                                                                                                        
ListVtxos(ctx, page Page, filter VtxoFilter) ([]Vtxo, error)

Filter semantics
                                                                                                                                                                                          
┌───────────────────────┬─────────────────────────┬─────────────────────────────┐                                                                                                         
│        FilterReturnsSQL condition        │
├───────────────────────┼─────────────────────────┼─────────────────────────────┤                                                                                                         
│ VtxoFilterAllAll VTXOsNo filter                   │
├───────────────────────┼─────────────────────────┼─────────────────────────────┤
│ VtxoFilterSpendableSpendable + recoverableNOT spent AND NOT unrolled  │                                                                                                         
├───────────────────────┼─────────────────────────┼─────────────────────────────┤                                                                                                         
│ VtxoFilterSpentSpent + unrolledspent=true OR unrolled=true │                                                                                                         
├───────────────────────┼─────────────────────────┼─────────────────────────────┤                                                                                                         
│ VtxoFilterRecoverableRecoverable onlyGo-side IsRecoverable()     │
└───────────────────────┴─────────────────────────┴─────────────────────────────┘                                                                                                         
                
VtxoFilterSpendable preserves historical behaviorrecoverable VTXOs are included

                                                                                                                                                                                          
Test plan       
                                                                                                                                                                                          
- 22-VTXO pagination test verifying ordering across 5 pages                                                                                                                               
- Filter tests for all, spendable, spent                                                                                                                                                  
- Multi-asset VTXO test (subquery correctness)                                                                                                                                            
- Beyond-last-page returns empty
- MaxPageSize clamping                                                                                                                                                                    
- Page{} zero-value returns all                                                                                                                                                           
- All existing e2e tests updated and passing                                                                                                                                              


@altafan please review                                                             


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
* Added pagination and filtering for VTXO listings (page number/size; filters: all, spendable, spent, recoverable).

* **Breaking Changes**
* VTXO listing API updated to a paginated/filterable call; previous dual-list return removed (clients must adapt).

* **Bug Fixes**
* Offchain balance now derives from spendable-only VTXOs to avoid extra processing.

* **Tests**
* Added pagination tests and updated end-to-end tests to use the new VTXO listing API.

<!-- review_stack_entry_start -->

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/arkade-os/go-sdk/pull/175)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

sekulicd added 2 commits May 14, 2026 10:50
  Introduce offset-based pagination and filtering for VTXO listing.
  Merge ListVtxos and ListSpendableVtxos into a single ListVtxos method
  that accepts a Page (pageNum, pageSize) and VtxoFilter (all, spendable,
  spent, recoverable).

  - Add Page struct with MaxPageSize=200 and zero-value "return all" semantics
  - Add VtxoFilter enum (All, Spendable, Spent, Recoverable)
  - Implement SQL pagination via subquery on vtxo table to handle multi-asset VTXOs
  - Update all internal callers and e2e tests
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 38a9a6e7-1c6f-4500-881e-475bdc9b30c5

📥 Commits

Reviewing files that changed from the base of the PR and between 274b947 and 0a7b097.

📒 Files selected for processing (12)
  • init.go
  • sdk.go
  • send.go
  • store/service_test.go
  • store/sql/sqlc/queries/query_paginated.go
  • store/sql/vtxo_store.go
  • test/e2e/asset_test.go
  • test/e2e/exit_test.go
  • test/e2e/hd_wallet_test.go
  • test/e2e/transaction_test.go
  • types/types.go
  • wallet.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • types/types.go
  • test/e2e/asset_test.go
  • init.go
  • test/e2e/transaction_test.go
  • wallet.go
  • test/e2e/exit_test.go
  • send.go
  • sdk.go
  • test/e2e/hd_wallet_test.go
  • store/sql/sqlc/queries/query_paginated.go
  • store/service_test.go
  • store/sql/vtxo_store.go

Walkthrough

Adds paginated, filterable VTXO listing (Page, VtxoFilter), implements paginated SQL queries, repurposes store GetVtxos and adds GetVtxosByOutpoint, updates wallet/sdk APIs and call sites, and augments store and E2E tests to use the new API.

Changes

Paginated and filterable VTXO listing API

Layer / File(s) Summary
Pagination and filtering type definitions
types/types.go, types/interfaces.go
Introduces MaxPageSize, Page (1-based, PageSize=0 => all), and VtxoFilter (All, Spendable, Spent, Recoverable). Updates VtxoStore to GetVtxos(ctx, page, filter) and adds GetVtxosByOutpoint.
SQL pagination query implementations
store/sql/sqlc/queries/query_paginated.go
Adds hand-written paginated SQL and Query methods: SelectAllVtxosPaginated, SelectSpendableVtxosPaginated, SelectSpentVtxos, and SelectSpentVtxosPaginated, scanning AssetVtxoVw rows with LIMIT/OFFSET composite-key subquery pagination.
Store layer refactor
store/sql/vtxo_store.go
Repurposes GetVtxos to dispatch on VtxoFilter (with Go-side recoverable filtering), adds GetVtxosByOutpoint, removes GetAllVtxos/GetSpendableVtxos, and adds pageToLimitOffset with clamping and 1-based paging behavior.
Store tests & pagination verification
store/service_test.go
Refactors tests to use GetVtxos/GetVtxosByOutpoint with filters; adds TestVtxoPagination validating ordering, page semantics, clamping, pageNum handling, and multi-asset counting.
Public SDK / Wallet API update
sdk.go
Changes Wallet.ListVtxos signature to accept (ctx, page, filter) and return ([]Vtxo, error); removes ListSpendableVtxos from the interface.
Wallet funding & retrieval changes
funding.go, init.go, send.go, wallet.go
Updates wallet callers: getOffchainBalance, scheduleNextSettlement, getSpendableVtxos, refreshVtxoDb, and others now use GetVtxos(ctx, types.Page{}, types.VtxoFilterSpendable) or GetVtxosByOutpoint as appropriate.
E2E tests migration
test/e2e/{asset,exit,hd_wallet,restore_smoke,transaction}_test.go
Updates E2E tests to call ListVtxos with explicit Page and VtxoFilter arguments and handle the single-slice return value.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Wallet
  participant VtxoStore
  participant SQL

  Client->>Wallet: ListVtxos(ctx, Page{}, VtxoFilterSpendable)
  Wallet->>VtxoStore: GetVtxos(ctx, Page{}, VtxoFilterSpendable)
  VtxoStore->>SQL: SelectSpendableVtxosPaginated(limit, offset)
  SQL-->>VtxoStore: []AssetVtxoVw rows
  VtxoStore-->>Wallet: []Vtxo (grouped by outpoint)
  Wallet-->>Client: []Vtxo, error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • arkade-os/go-sdk#145: Modifies spendable VTXO retrieval in send.go to compute VTXOs via contract manager instead of store queries.
  • arkade-os/go-sdk#118: Updates VTXO persistence logic in store/sql/vtxo_store.go for sweep/unroll/settle handling and directly overlaps in SpendVtxos/SettleVtxos logic.

Suggested reviewers

  • altafan
  • louisinger
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Add paginated VTXO listing with filter support' directly and clearly summarizes the main changes: introducing pagination and filtering to VTXO listing APIs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
store/sql/vtxo_store.go (1)

448-467: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve row order when collapsing multi-asset VTXOs.

assetVtxoVwRowsToVtxos groups into a map and then ranges that map, which randomizes the final slice order. Even with ordered SQL, callers will still see nondeterministic VTXO ordering across pages.

Suggested fix
 func assetVtxoVwRowsToVtxos(rows []queries.AssetVtxoVw) []clientTypes.Vtxo {
-	byOutpoint := make(map[string][]queries.AssetVtxoVw)
+	byOutpoint := make(map[string][]queries.AssetVtxoVw, len(rows))
+	order := make([]string, 0, len(rows))
 	for _, row := range rows {
 		key := fmt.Sprintf("%s:%d", row.Txid, row.Vout)
+		if _, ok := byOutpoint[key]; !ok {
+			order = append(order, key)
+		}
 		byOutpoint[key] = append(byOutpoint[key], row)
 	}
 
-	vtxos := make([]clientTypes.Vtxo, 0, len(byOutpoint))
-	for _, group := range byOutpoint {
-		vtxo := assetVtxoVwGroupToVtxo(group)
-		vtxos = append(vtxos, vtxo)
+	vtxos := make([]clientTypes.Vtxo, 0, len(order))
+	for _, key := range order {
+		vtxos = append(vtxos, assetVtxoVwGroupToVtxo(byOutpoint[key]))
 	}
 
 	return vtxos
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/sql/vtxo_store.go` around lines 448 - 467, The current
assetVtxoVwRowsToVtxos groups rows into a map and then ranges it, which yields
nondeterministic ordering; change it to preserve input order by recording
outpoint keys as you encounter them (e.g., maintain a []string keys alongside
the byOutpoint map or build groups in an ordered slice of groups) and then
iterate that ordered keys/slice to call assetVtxoVwGroupToVtxo and append
results; this keeps the final []clientTypes.Vtxo in the same order as the
incoming rows while still collapsing multi-asset VTXOs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@init.go`:
- Line 219: The call to GetVtxos on w.store.VtxoStore() is too long for golines;
break the invocation across multiple lines so each argument is on its own line
(or introduce a short ctx variable) to reduce line length—e.g. assign ctx :=
context.Background() and call vtxos, err := w.store.VtxoStore().GetVtxos(ctx,
types.Page{}, types.VtxoFilterSpendable) with the arguments wrapped or split so
the line complies with golines while retaining the same variables and function
names (GetVtxos, VtxoStore, types.Page{}, types.VtxoFilterSpendable, vtxos,
err).

In `@sdk.go`:
- Line 87: The ListVtxos function signature is exceeding golines rules; reformat
the signature for ListVtxos so it complies with golines line-length/formatting
(e.g., break parameters onto separate lines and align the return types) while
preserving types: ListVtxos(ctx context.Context, page types.Page, filter
types.VtxoFilter) ([]clienttypes.Vtxo, error); ensure the new multi-line
signature matches the project's existing function signature style and compiles.

In `@store/sql/sqlc/queries/query_paginated.go`:
- Around line 19-26: The query using the IN(subquery) pattern (e.g.,
selectAllVtxosPaginated) relies on the subquery ORDER BY but IN does not
preserve that order; add an explicit outer ORDER BY clause to the main SELECT to
enforce "created_at DESC, txid ASC, vout ASC" and make page boundaries stable.
Apply the same change to the other paginated query constants referenced in the
file (the spendable/spent variants and selectSpentVtxos) so each top-level
SELECT has the same explicit ORDER BY sequence as the subquery.

In `@store/sql/vtxo_store.go`:
- Around line 323-364: The current code paginates via SelectAllVtxosPaginated
then applies IsRecoverable, which can return fewer than limit results per page;
instead, when filter == types.VtxoFilterRecoverable, fetch the full set using
v.querier.SelectAllVtxos (or SelectAllVtxos when allSpendable/allSpent/all are
not appropriate), convert with assetVtxoVwRowsToVtxos, apply IsRecoverable to
produce a filtered slice, and then apply pagination in Go using limit/offset
before returning; adjust the switch to route Recoverable to the non-paginated
SelectAllVtxos path and perform the manual slice pagination after filtering.

In `@types/types.go`:
- Around line 127-129: The comment for VtxoFilterSpendable is misleading about
"actively usable" — update the doc on the VtxoFilterSpendable constant/enum to
state it matches entries where !spent && !unrolled and therefore still includes
recoverable/expired or swept‑yet‑unspent VTXOs (i.e., not strictly guaranteed to
be usable for new off‑chain transactions); reference the VtxoFilterSpendable
symbol and explicitly mention the boolean semantics (!spent && !unrolled) so the
contract is clear.

---

Outside diff comments:
In `@store/sql/vtxo_store.go`:
- Around line 448-467: The current assetVtxoVwRowsToVtxos groups rows into a map
and then ranges it, which yields nondeterministic ordering; change it to
preserve input order by recording outpoint keys as you encounter them (e.g.,
maintain a []string keys alongside the byOutpoint map or build groups in an
ordered slice of groups) and then iterate that ordered keys/slice to call
assetVtxoVwGroupToVtxo and append results; this keeps the final
[]clientTypes.Vtxo in the same order as the incoming rows while still collapsing
multi-asset VTXOs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60575f72-6e0f-4682-b111-5d5892f7c892

📥 Commits

Reviewing files that changed from the base of the PR and between 625a2e2 and 274b947.

📒 Files selected for processing (15)
  • funding.go
  • init.go
  • sdk.go
  • send.go
  • store/service_test.go
  • store/sql/sqlc/queries/query_paginated.go
  • store/sql/vtxo_store.go
  • test/e2e/asset_test.go
  • test/e2e/exit_test.go
  • test/e2e/hd_wallet_test.go
  • test/e2e/restore_smoke_test.go
  • test/e2e/transaction_test.go
  • types/interfaces.go
  • types/types.go
  • wallet.go

Comment thread init.go Outdated
Comment thread sdk.go Outdated
Comment thread store/sql/sqlc/queries/query_paginated.go Outdated
Comment thread store/sql/vtxo_store.go Outdated
Comment thread types/types.go Outdated
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Arkana Code Review — feat/pagination-vtxo-list

Good refactor overall — unifying the VTXO query surface is the right move. But there are two bugs and one significant downstream breakage concern.


🔴 BUG: Result ordering within pages is non-deterministic

store/sql/vtxo_store.goassetVtxoVwRowsToVtxos() (≈line 432)

The SQL subqueries correctly use ORDER BY created_at DESC, txid ASC, vout ASC to select which VTXOs land on which page. But assetVtxoVwRowsToVtxos groups rows into a map[string][]queries.AssetVtxoVw and then iterates the map:

for _, group := range byOutpoint {
    vtxos = append(vtxos, assetVtxoVwGroupToVtxo(group))
}

Go map iteration order is random. The ORDER BY from SQL is discarded. Callers paginating through results will see VTXOs in random order within each page — this defeats the purpose of pagination.

Fix: Use an ordered structure (e.g., a []string of keys preserving insertion order from the row scan) to iterate byOutpoint deterministically. Alternatively, sort the final vtxos slice by CreatedAt DESC before returning.


🔴 BUG: VtxoFilterRecoverable pagination is broken

store/sql/vtxo_store.goGetVtxos() (≈line 350-355)

When filter == VtxoFilterRecoverable with pagination, the code fetches N rows from SelectAllVtxosPaginated, then filters Go-side:

if filter == types.VtxoFilterRecoverable {
    filtered := make([]clientTypes.Vtxo, 0, len(vtxos))
    for _, v := range vtxos {
        if v.IsRecoverable() { filtered = append(filtered, v) }
    }
    return filtered, nil
}

This means:

  • Request PageSize=10 → fetch 10 "all" VTXOs → filter → might return 0-10 recoverable VTXOs
  • Subsequent pages miss recoverable VTXOs that were counted as "all" but not recoverable on previous pages
  • Pagination invariant is violated: no correct way to enumerate all recoverable VTXOs page-by-page

Fix options:

  1. Don't support pagination for recoverable (return error if PageSize > 0 && filter == VtxoFilterRecoverable)
  2. Move the recoverable check into SQL (e.g., WHERE swept = true AND spent = false AND expires_at < ?) — though this may not capture the full IsRecoverable() logic
  3. Document clearly that recoverable filter always returns all results (ignores pagination)

🟡 API Design: No total count or "has more" indicator

types/interfaces.goGetVtxos returns ([]types.Vtxo, error)

A pagination API without a total count forces callers to:

  • Keep requesting pages until they get an empty result
  • Cannot render "page X of Y" or know the total VTXO count without a separate query

Consider returning a result struct: type VtxoPage struct { Vtxos []Vtxo; Total int; Page int } or at minimum a hasMore bool.


🟡 Cross-repo breakage (5+ repos affected)

This PR removes ListSpendableVtxos() and changes the ListVtxos() signature. The following downstream repos will break at compile time:

Repo File Call
bancod pkg/contract/taker.go:95 ListSpendableVtxos(ctx)
bancod test/e2e/utils_test.go:144 ListVtxos(t.Context()) (old 3-return sig)
demos asset/golang/main.go:100 ListSpendableVtxos(ctx)
asset-demos asset/golang/main.go:100 ListSpendableVtxos(ctx)
layerzero-usdt0-arkade-demo test/e2e_flow_test.go:519 ListVtxos(ctx) (old sig)
introspector-review test/utils_test.go:540, contract_id_test.go:151 ListVtxos(ctx) (old sig)

Please either:

  1. Open companion PRs for all affected repos, or
  2. Keep backward-compatible wrappers (deprecated) during transition

✅ What looks good

  • Subquery-based pagination for multi-asset VTXOs is correct — paginates at the vtxo level, not the view level. Well-documented rationale in query_paginated.go.
  • pageToLimitOffset correctly handles edge cases (PageNum=0 → 1, clamping to MaxPageSize).
  • Removing the manual Spent || Unrolled check in getOffchainBalance is correct — the SQL filter now handles it.
  • Test coverage is solid — TestVtxoPagination covers page boundaries, partial pages, multi-asset, MaxPageSize clamping, and spent filter.
  • GetVtxosByOutpoint rename is clearer than the overloaded GetVtxos.

Verdict: Request changes for the two bugs (ordering + recoverable pagination). The cross-repo breakage needs a migration plan before merge.

⚠️ Protocol note: While this PR touches VTXO querying (read path) rather than VTXO spending/signing (write path), incorrect pagination could cause getOffchainBalance or getSpendableVtxos to return incomplete results if someone accidentally passes a Page with PageSize > 0 in internal callers. Current internal callers all use Page{} (fetch all), which is correct. Flagging for human review as the VTXO store interface is protocol-adjacent.

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Arkana Follow-up Review — 32a3e2f lint

The new commit is formatting-only (line wrapping for linter). Both bugs from my previous review are still open:

🔴 Still open: Map iteration destroys SQL ordering

store/sql/vtxo_store.go:462-466assetVtxoVwRowsToVtxos still iterates byOutpoint map with for _, group := range byOutpoint, which randomizes the order that SQL carefully established. Pagination without deterministic ordering is broken.

🔴 Still open: VtxoFilterRecoverable + pagination returns wrong results

store/sql/vtxo_store.go:348-370 — Recoverable filter still fetches N "all" rows then filters Go-side, violating pagination invariants.

🟡 Still open: Cross-repo breakage plan needed

No companion PRs visible yet for downstream consumers (bancod, demos, etc.).


No new issues introduced by the lint commit. Maintaining request-changes until the two bugs are resolved.

⚠️ Protocol-adjacent — flagging for human review.

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

Comment thread sdk.go
ListVtxos(
ctx context.Context,
page types.Page,
filter types.VtxoFilter,
Copy link
Copy Markdown
Contributor

@altafan altafan May 14, 2026

Choose a reason for hiding this comment

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

wouldn't be nice to keep having the option pattern so the user just calls (?):

ListVtxos(ctx) -> gets all vtxos, paginated by default if the response is too big
// Or
ListVtxos(ctx, WithPage(token)) -> gets a specific page given its token
// Or
ListVtxos(ctx, WithSpendableOnly()) -> gets only spendable vtxos, paginated by default if the response is too big
// Or
ListVtxos(ctx, WithSpendableOnly(), WithPage(token)) -> gets a specific page of only spendable vtxos

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.

2 participants