Skip to content

[Access] Add experimental account transfers rest API#8419

Draft
peterargue wants to merge 41 commits intomasterfrom
peter/account-transfers
Draft

[Access] Add experimental account transfers rest API#8419
peterargue wants to merge 41 commits intomasterfrom
peter/account-transfers

Conversation

@peterargue
Copy link
Contributor

@peterargue peterargue commented Feb 18, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added experimental REST API endpoints for querying account transactions and token transfers with pagination and filtering support.
    • Introduced ability to expand transaction details and results on-demand.
    • Added comprehensive account history indexing for transactions and both fungible and non-fungible token transfers.
  • Documentation

    • Added guidance for experimental API usage and localnet setup.

@peterargue peterargue requested a review from a team as a code owner February 18, 2026 01:06
@peterargue peterargue marked this pull request as draft February 18, 2026 01:07
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This PR adds extended indexing for account transactions and token transfers (fungible and non-fungible) to Flow's access layer. It introduces new Pebble-backed storage indexes, backend APIs with pagination and filtering, experimental REST endpoints for querying account transaction history and transfers, and integration with the access node indexer pipeline to populate these indexes from execution data.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
.gitignore, .mockery.yaml, AGENTS.md, go.mod, integration/go.mod
Added ignore patterns, enabled mock generation for extended backends and indexers, updated Flow dependency version, and documented multi-module repository structure.
Access Backend Extended API
access/backends/extended/api.go, access/backends/extended/backend.go, access/backends/extended/backend_account_transactions.go, access/backends/extended/backend_account_transfers.go, access/backends/extended/backend_base.go, access/backends/extended/mock/api.go
Defined extended API interfaces for paginated account transaction and token transfer queries with filtering. Implemented backends composing transaction and transfer backends, with configuration, dependency wiring, and generated mocks.
Access Backend Tests
access/backends/extended/backend_account_transactions_test.go, access/backends/extended/backend_account_transfers_test.go
Comprehensive test coverage for account transaction and transfer backends, including pagination, filtering, error mapping, and irrecoverable error handling.
Data Models
model/access/account_transaction.go, model/access/account_transfer.go
New data models for account transactions (with roles), transaction cursors, and transfer pages; introduced TransactionRole enum and TransferRole constants for categorizing participants.
REST Experimental API
engine/access/rest/experimental/handler.go, engine/access/rest/experimental/models/..., engine/access/rest/experimental/request/..., engine/access/rest/experimental/routes/..., engine/access/rest/experimental/routes/account_transactions_test.go
Experimental REST handler, request parsers, response models, and route handlers for account transactions, fungible/non-fungible transfers with expansion and pagination support. Includes cursor encoding/decoding and comprehensive tests.
REST Router Integration
engine/access/rest/router/metrics.go, engine/access/rest/router/metrics_test.go, engine/access/rest/router/router.go, engine/access/rest/router/routes_experimental.go, engine/access/rest/router/router_test_helpers.go, engine/access/rest/common/http_request_handler.go
Added experimental routes registry, router metrics support, and REST handler to experimental route registration. Extended HTTP error mapping for FailedPrecondition and OutOfRange status codes. Added test helper for experimental requests.
REST Server & RPC Engine
engine/access/rest/server.go, engine/access/rpc/engine.go, engine/access/rpc/backend/transactions/provider/local.go
Wired extended backend into REST server and RPC engine initialization. Made transaction error message provider optional in LocalTransactionProvider.
Storage Interfaces
storage/account_transactions.go, storage/account_transfers.go, storage/errors.go, storage/locks.go
Defined reader, writer, and bootstrapper interfaces for account transactions and token transfers with generic IndexFilter support. Added three new locks for extended indexing and ErrInvalidQuery error constant.
Storage Indexes - Account Transactions
storage/indexes/account_transactions.go, storage/indexes/account_transactions_bootstrapper.go, storage/indexes/account_transactions_bootstrapper_test.go, storage/indexes/account_transactions_test.go
Pebble-backed account transactions index with bootstrap, store, and paginated query operations. Enforces consecutive height progression and supports cursor-based pagination with optional filtering.
Storage Indexes - FT Transfers
storage/indexes/account_ft_transfers.go, storage/indexes/account_ft_transfers_bootstrapper.go, storage/indexes/account_ft_transfers_bootstrapper_test.go, storage/indexes/account_ft_transfers_test.go
Pebble-backed fungible token transfer index with dual-indexing (source and recipient), bootstrap logic, and cursor-based pagination with optional filtering.
Storage Indexes - NFT Transfers
storage/indexes/account_nft_transfers.go, storage/indexes/account_nft_transfers_bootstrapper.go, storage/indexes/account_nft_transfers_bootstrapper_test.go, storage/indexes/account_nft_transfers_test.go
Pebble-backed non-fungible token transfer index with dual-indexing, bootstrap, and pagination similar to FT transfers. Stores NFT ID and event indices.
Storage Index Support
storage/indexes/prefix.go
Internal prefix constants for height tracking and index type identification in Pebble keys.
Storage Mocks
storage/mock/account_transactions*.go, storage/mock/fungible_token_transfers*.go
Generated mock implementations for all storage interfaces supporting account transactions and token transfers with typed call helpers and flexible return configuration.
Extended Indexer Core
module/state_synchronization/indexer/extended/indexer.go, module/state_synchronization/indexer/extended/extended_indexer.go, module/state_synchronization/indexer/extended/bootstrap.go
Defined Indexer and IndexerManager interfaces. Implemented ExtendedIndexer orchestrating multiple indexers with backfill support, live data processing, and metrics. Provided database bootstrap function for initializing Pebble storage.
Extended Indexer - Account Transactions
module/state_synchronization/indexer/extended/account_transactions.go, module/state_synchronization/indexer/extended/account_transactions_test.go
Account transactions indexer extracting transaction metadata, roles (payer, proposer, authorizer, interacted), and event-derived addresses with comprehensive event payload decoding and grouping logic.
Extended Indexer - Transfers
module/state_synchronization/indexer/extended/account_transfers.go, module/state_synchronization/indexer/extended/account_transfers_test.go
Transfer indexer orchestrating FT and NFT indexers with cross-store synchronization checks and filtering (e.g., excluding fee deposits).
Extended Indexer - Parsers
module/state_synchronization/indexer/extended/transfers/ft_parser.go, module/state_synchronization/indexer/extended/transfers/nft_parser.go, module/state_synchronization/indexer/extended/transfers/ft_events.go, module/state_synchronization/indexer/extended/transfers/nft_events.go, module/state_synchronization/indexer/extended/transfers/ft_group.go, module/state_synchronization/indexer/extended/transfers/nft_group.go, module/state_synchronization/indexer/extended/transfers/helpers.go, module/state_synchronization/indexer/extended/transfers/parser_test.go
Event parsers and event grouping logic for fungible and non-fungible token transfers, including withdrawal/deposit pairing, mint/burn detection, and ancestry chain construction. Helper functions for CCF event decoding and optional address extraction.
Extended Indexer Mocks & Tests
module/state_synchronization/indexer/extended/mock/..., module/state_synchronization/indexer/extended/extended_indexer_test.go
Generated mocks and comprehensive tests for extended indexer components, covering live/backfill processing, synchronization, error handling, and various startup scenarios.
Indexer Core Integration
module/state_synchronization/indexer/indexer_core.go, module/state_synchronization/indexer/indexer_core_test.go, module/execution/scripts_test.go
Wired extended indexer into indexer core with optional extended block data indexing. Updated indexer constructor signatures and test fixtures to support the new parameter.
Metrics
module/metrics.go, module/metrics/extended_indexing.go, module/metrics/namespaces.go, module/metrics/noop.go, module/mock/extended_indexing_metrics.go
Added ExtendedIndexingMetrics interface and implementations (NoopCollector, GaugeVec-based collector) to track indexed block heights per indexer. Generated mock for testing.
Node Builder Integration
cmd/access/node_builder/access_node_builder.go, cmd/observer/node_builder/observer_builder.go, cmd/util/cmd/run-script/cmd.go, engine/access/handle_irrecoverable_state_test.go, engine/access/integration_unsecure_grpc_server_test.go, engine/access/rest_api_test.go, engine/access/rpc/rate_limit_test.go, engine/access/secure_grpcr_test.go
Added extended indexing lifecycle management to access and observer node builders with configuration flags, database initialization, and RPC engine integration. Updated RPC builder call sites with new extended backend parameter across multiple test files.
Integration Tests
integration/localnet/AGENTS.md, integration/localnet/builder/bootstrap.go, integration/testnet/client.go, integration/testnet/experimental_client.go, integration/testnet/network.go, integration/tests/access/cohort3/extended_indexing_test.go, integration/tests/access/cohort3/extended_indexing_transfers_test.go, integration/tests/access/cohort4/grpc_state_stream_test.go, integration/utils/transactions.go
Added experimental API client wrapper for integration tests, network constants, Docker configuration for extended indexing, and comprehensive end-to-end test suites validating extended indexing with transaction role verification, transfer filtering, and pagination.
Documentation
engine/access/rest/experimental/README.md
New documentation linking to OpenAPI specification for the experimental API.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes


Possibly related PRs

  • onflow/flow-go#8240: Related through REST router and metric route registration changes that both introduce new route matching and registration logic.

Suggested reviewers

  • zhangchiqing
  • janezpodhostnik

Poem

🐇 Hop! Hop! New indices spring to life,
Accounts and transfers, sorted free of strife,
With queries swift and cursors that align,
Extended indexing makes the data shine!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[Access] Add experimental account transfers rest API' accurately describes the primary change of introducing experimental REST API endpoints for account transfers, which is a substantial and central portion of this changeset.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch peter/account-transfers

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
Contributor

@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: 9

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
engine/access/rest/server.go (1)

43-77: ⚠️ Potential issue | 🔴 Critical

Update cmd/util/cmd/run-script/cmd.go:173 to pass the extendedBackend parameter to rest.NewServer.

The call at line 173 is missing the extendedBackend parameter. It currently passes 10 arguments but should pass 11. Add nil (or the appropriate value) as the final parameter before the closing parenthesis.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/access/rest/server.go` around lines 43 - 77, The NewServer function
signature includes an extendedBackend parameter (extended.API) but the caller in
cmd/util/cmd/run-script/cmd.go currently invokes rest.NewServer with only 10
args; update that call to pass the missing eleventh argument (use nil if no
extended backend is available) so the call matches NewServer(ctx, serverAPI,
config, logger, chain, restCollector, stateStreamApi, stateStreamConfig,
enableNewWebsocketsStreamAPI, wsConfig, extendedBackend).
🟡 Minor comments (14)
integration/localnet/AGENTS.md-3-3 (1)

3-3: ⚠️ Potential issue | 🟡 Minor

Minor markdown lint issues flagged by static analysis.

The heading at Line 3 jumps from h1 (#) to h3 (###), skipping h2. Also, code blocks at lines 40, 45, and 68 are missing language identifiers (e.g., bash or shell).

Proposed fix
-### Localnet (Local Network Testing)
+## Localnet (Local Network Testing)

And for the fenced code blocks, add language identifiers like ```bash instead of bare ```.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@integration/localnet/AGENTS.md` at line 3, Update the markdown structure and
fenced code blocks in AGENTS.md: change the heading "Localnet (Local Network
Testing)" from an h3 to an h2 (use "## Localnet (Local Network Testing)") to
restore proper heading order, and add language identifiers to the bare fenced
code blocks (replace ``` with ```bash or ```shell as appropriate for the
snippets referenced around the Localnet section) so static analysis no longer
flags missing code-block languages.
integration/testnet/client.go-494-494 (1)

494-494: ⚠️ Potential issue | 🟡 Minor

Typo in error message: "cusnctruct" → "construct".

✏️ Proposed fix
-		return sdk.Address{}, nil, fmt.Errorf("failed cusnctruct create account transaction %w", err)
+		return sdk.Address{}, nil, fmt.Errorf("failed to construct create account transaction: %w", err)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@integration/testnet/client.go` at line 494, Fix the typo in the error message
returned from the account creation failure (the statement returning
sdk.Address{}, nil, fmt.Errorf("failed cusnctruct create account transaction
%w", err)); change "cusnctruct" to "construct" so the fmt.Errorf reads "failed
construct create account transaction %w" (or better: "failed to construct create
account transaction %w") to produce a clear, correctly spelled error string.
model/access/account_transfer.go-22-22 (1)

22-22: ⚠️ Potential issue | 🟡 Minor

Minor doc inconsistency: EventIndices comment says "Index" (singular) but the field is a slice.

The same issue exists on line 39 for NonFungibleTokenTransfer.

✏️ Proposed fix
-	EventIndices     []uint32        // Index of the event within the transaction
+	EventIndices     []uint32        // Indices of the events within the transaction
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/access/account_transfer.go` at line 22, The comments for the slice
fields are misleading: update the doc comment for EventIndices (in the struct
that declares EventIndices []uint32) to use the plural "Indices" and reflect
that it's a slice (e.g., "Indices of the events within the transaction"), and do
the same for the EventIndices field inside the NonFungibleTokenTransfer struct
so both comments correctly indicate multiple indices rather than a single
"Index".
engine/access/rest/experimental/request/transfer_cursor.go-43-54 (1)

43-54: ⚠️ Potential issue | 🟡 Minor

EncodeTransferCursor will panic on nil input, same as EncodeAccountTransactionCursor in the sibling file.

🛡️ Proposed fix
 func EncodeTransferCursor(cursor *accessmodel.TransferCursor) (string, error) {
+	if cursor == nil {
+		return "", nil
+	}
 	c := transferCursor{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/access/rest/experimental/request/transfer_cursor.go` around lines 43 -
54, EncodeTransferCursor dereferences a nil pointer and can panic; add a
nil-check at the top of EncodeTransferCursor (same pattern used in
EncodeAccountTransactionCursor) and return an empty string and nil error when
cursor == nil to avoid accessing transferCursor fields; update the
EncodeTransferCursor function to immediately handle nil before constructing
transferCursor and keep existing JSON marshal and base64 encode logic otherwise.
engine/access/rest/experimental/request/get_account_transactions.go-92-102 (1)

92-102: ⚠️ Potential issue | 🟡 Minor

EncodeAccountTransactionCursor will panic on nil input.

If cursor is nil, line 94 dereferences it (cursor.BlockHeight), causing a nil-pointer panic. The corresponding EncodeTransferCursor in transfer_cursor.go has the same pattern. Consider adding a nil guard or documenting the precondition.

🛡️ Proposed fix
 func EncodeAccountTransactionCursor(cursor *accessmodel.AccountTransactionCursor) (string, error) {
+	if cursor == nil {
+		return "", nil
+	}
 	c := accountTransactionCursor{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/access/rest/experimental/request/get_account_transactions.go` around
lines 92 - 102, The EncodeAccountTransactionCursor function dereferences cursor
without a nil check which will panic; add a nil guard at the top of
EncodeAccountTransactionCursor (and mirror the same guard in
EncodeTransferCursor) that returns an appropriate zero value (e.g., empty string
and nil error or a clear error) when cursor == nil; implement the chosen
behavior consistently across accountTransactionCursor encoding functions and
ensure you reference EncodeAccountTransactionCursor and EncodeTransferCursor
when making the change.
integration/tests/access/cohort3/extended_indexing_test.go-270-284 (1)

270-284: ⚠️ Potential issue | 🟡 Minor

Missing length assertion before indexing into allPaginated — potential panic.

Line 276 indexes into allPaginated[i] using the loop counter from allUnpaginated.Transactions, but there's no prior assertion that both slices have the same length. If the paginated result has fewer entries, this will panic with an index-out-of-range.

Proposed fix
 func (s *ExtendedIndexingSuite) verifyPagination(address string) {
 	// Paginating through all results should yield the same count as a single unpaginated request
 	allUnpaginated := s.fetchAccountTransactions(address, nil)
 	allPaginated := s.collectAllPages(address, 1, nil, nil)
 
+	s.Require().Equal(len(allUnpaginated.Transactions), len(allPaginated),
+		"paginated and unpaginated result counts should match")
+
 	for i, unpagedTx := range allUnpaginated.Transactions {
 		pagedTx := allPaginated[i]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@integration/tests/access/cohort3/extended_indexing_test.go` around lines 270
- 284, In verifyPagination, assert that the paginated and unpaginated result
counts match before indexing: after obtaining allUnpaginated :=
s.fetchAccountTransactions(address, nil) and allPaginated :=
s.collectAllPages(address, 1, nil, nil), add a length assertion (e.g.
s.Require().Equal(len(allUnpaginated.Transactions), len(allPaginated)) or
s.Require().Len(allPaginated, len(allUnpaginated.Transactions))) so the test
fails safely instead of panicking when iterating and accessing allPaginated[i];
this keeps the rest of the comparisons in verifyPagination valid.
engine/access/rest/experimental/models/model_account_transactions_response.go-11-13 (1)

11-13: ⚠️ Potential issue | 🟡 Minor

Transactions []AccountTransaction without omitempty will serialize as "transactions":null for a nil slice.

REST clients typically expect [] for empty collections rather than null. The field should either:

  • Always be initialized to a non-nil slice by callers building this response (and rely on the no-omitempty tag to always emit "transactions":[]), or
  • Be initialized in the constructor/builder that produces AccountTransactionsResponse.

If consumers already guarantee the slice is never nil (e.g., the backend returns an empty []AccountTransaction{}), this is fine as-is — just worth an explicit callout in the builder. If the field can be nil, the JSON output will be surprising.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@engine/access/rest/experimental/models/model_account_transactions_response.go`
around lines 11 - 13, AccountTransactionsResponse currently has Transactions
[]AccountTransaction which may serialize to "transactions":null; ensure the
slice is never nil by initializing it to an empty slice in the response
construction path (e.g., in the constructor/builder that produces
AccountTransactionsResponse or wherever AccountTransactionsResponse values are
created) so that Transactions is always set to []AccountTransaction{} for empty
results; alternatively, if you want to allow omission, add `omitempty` to the
`transactions` json tag, but prefer initializing the Transactions slice in the
code that builds AccountTransactionsResponse to guarantee clients see an empty
array rather than null.
engine/access/rest/experimental/README.md-3-3 (1)

3-3: ⚠️ Potential issue | 🟡 Minor

OpenAPI spec link hardcoded to a commit hash — will become stale.

Pinning to cf44613233910eade91759399f94fadfccd37b72 means the README will always point to that specific revision, even after the spec is updated. Consider using a branch reference (e.g., master or main) or a tagged release so the link stays current:

-The OpenAPI spec for this api can be found at https://github.com/onflow/flow/blob/cf44613233910eade91759399f94fadfccd37b72/openapi/experimental/openapi.yaml
+The OpenAPI spec for this api can be found at https://github.com/onflow/flow/blob/main/openapi/experimental/openapi.yaml
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/access/rest/experimental/README.md` at line 3, The README currently
hardcodes the OpenAPI URL with a specific commit hash (the string
"https://github.com/onflow/flow/blob/cf44613233910eade91759399f94fadfccd37b72/openapi/experimental/openapi.yaml");
replace that commit-hash URL with a stable reference (e.g., use the branch name
like "main" or a released tag) so the link remains up-to-date, or point to a
short-lived redirect/manifest if your process requires immutable pins—update the
single line containing the URL in the README to use the chosen branch/tag
reference.
engine/access/rest/experimental/models/fungible_token_transfer.go-26-26 (1)

26-26: ⚠️ Potential issue | 🟡 Minor

Potential nil-pointer panic: transfer.Amount is *big.Int.

Calling .String() on a nil *big.Int will panic. While a well-formed FungibleTokenTransfer from storage should always carry a non-nil Amount, storage or indexer bugs could produce a nil value and crash the request handler.

🛡️ Proposed defensive fix
-	t.Amount = transfer.Amount.String()
+	if transfer.Amount != nil {
+		t.Amount = transfer.Amount.String()
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@engine/access/rest/experimental/models/fungible_token_transfer.go` at line
26, The assignment t.Amount = transfer.Amount.String() can panic if
transfer.Amount is nil; update the code around the FungibleTokenTransfer
conversion (the line assigning t.Amount from transfer.Amount) to defensively
check if transfer.Amount == nil and set a safe default string (e.g., "0" or
empty string) instead of calling .String(); optionally add a debug/warn log
mentioning the nil amount and the enclosing function name to aid tracing.
module/state_synchronization/indexer/extended/transfers/ft_group.go-127-140 (1)

127-140: ⚠️ Potential issue | 🟡 Minor

ResolvePairs is not idempotent — calling it twice appends duplicate burn entries.

Each call appends unmatched withdrawals (burns) to g.pairedResults. If called more than once, burn entries will be duplicated. Consider adding a guard or documenting single-call semantics.

Proposed fix
 func (g *ftTxEventGroup) ResolvePairs() []ftPairedResult {
 	// find all unmatched withdrawals
 	for uuid, wIdx := range g.withdrawalByUUID {
 		if _, ok := g.matchedDeposits[uuid]; ok {
 			continue
 		}
+		// Mark as matched to prevent duplicate burn entries on repeated calls.
+		g.matchedDeposits[uuid] = struct{}{}
 		// Unmatched withdrawal -- treat as burn.
 		g.pairedResults = append(g.pairedResults, ftPairedResult{
 			sourceEvents: mergeEvents(g.withdrawals[wIdx], nil),
 			withdrawal:   &g.withdrawals[wIdx].decoded,
 		})
 	}
 	return g.pairedResults
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@module/state_synchronization/indexer/extended/transfers/ft_group.go` around
lines 127 - 140, ResolvePairs on ftTxEventGroup appends unmatched withdrawals
into g.pairedResults every call, causing duplicate "burn" entries; make it
idempotent by adding a guard or reset. Update ResolvePairs to either (a) return
early if already resolved (e.g., introduce a bool field like pairsResolved on
ftTxEventGroup and check it at the start, setting it true before returning), or
(b) clear/rehydrate g.pairedResults at the start of ResolvePairs (e.g.,
g.pairedResults = g.pairedResults[:0]) before iterating withdrawalByUUID and
matchedDeposits to rebuild results; use the existing symbols ResolvePairs,
ftTxEventGroup, pairedResults, withdrawalByUUID, matchedDeposits to locate and
modify the method.
storage/indexes/account_transactions.go-237-238 (1)

237-238: ⚠️ Potential issue | 🟡 Minor

Potential uint32 overflow if limit equals math.MaxUint32.

fetchLimit := limit + 1 wraps to 0 when limit == math.MaxUint32, causing the function to return an empty page. While unlikely in practice (API likely caps limits much lower), consider guarding or documenting this.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storage/indexes/account_transactions.go` around lines 237 - 238, The
calculation fetchLimit := limit + 1 can overflow when limit == math.MaxUint32;
update the logic in the pagination code in account_transactions.go (the
fetchLimit computation) to guard against overflow by checking if limit ==
math.MaxUint32 and handling that case (e.g., set fetchLimit to limit or promote
to a larger integer type/uint64 before adding) so you never wrap to 0; reference
the fetchLimit variable and the surrounding pagination logic to implement the
check.
storage/indexes/account_ft_transfers.go-243-244 (1)

243-244: ⚠️ Potential issue | 🟡 Minor

Same uint32 overflow edge case as lookupAccountTransactions.

fetchLimit := limit + 1 wraps to 0 when limit == math.MaxUint32, though this is unlikely to occur in practice.

storage/indexes/account_ft_transfers.go-438-451 (1)

438-451: ⚠️ Potential issue | 🟡 Minor

big.Int.Bytes() loses sign information.

big.Int.Bytes() returns the absolute value, and SetBytes() always produces a non-negative result (Line 284). If Amount were ever negative, the sign would be silently dropped during round-trip serialization. This is likely fine for token transfer amounts but worth documenting the non-negative invariant.

storage/indexes/account_transactions.go-59-87 (1)

59-87: ⚠️ Potential issue | 🟡 Minor

Minor typo in doc comment: "constuction" → "construction".

Line 61: constuction should be construction.

Proposed fix
-// If the index has not been initialized, constuction will fail with [storage.ErrNotBootstrapped].
+// If the index has not been initialized, construction will fail with [storage.ErrNotBootstrapped].
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storage/indexes/account_transactions.go` around lines 59 - 87, Fix the typo
in the doc comment for NewAccountTransactions: change "constuction" to
"construction" in the function comment block above the NewAccountTransactions(db
storage.DB) signature so the comment reads "If the index has not been
initialized, construction will fail with [storage.ErrNotBootstrapped]." Ensure
only the comment text is modified and no code behavior is changed.

Comment on lines +866 to +867
extendedIndexerDependable := module.NewProxiedReadyDoneAware()
builder.IndexerDependencies.Add(extendedIndexerDependable)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: extendedIndexerDependable is never initialized when extended indexing is disabled, blocking the execution data indexer.

extendedIndexerDependable is unconditionally added to builder.IndexerDependencies (Line 867), but it is only initialized via Init() inside the if builder.extendedIndexingEnabled block (Line 1131). When executionDataIndexingEnabled=true but extendedIndexingEnabled=false, this dependency is never satisfied, causing the "execution data indexer" DependableComponent (Line 995) to hang indefinitely waiting for it.

Follow the pattern used by other optional dependables (e.g., versionControlDependable at Line 2103, stopControlDependable at Line 2137) by initializing with a noop when the feature is disabled:

🐛 Proposed fix
 		extendedIndexerDependable := module.NewProxiedReadyDoneAware()
 		builder.IndexerDependencies.Add(extendedIndexerDependable)
+
+		if !builder.extendedIndexingEnabled {
+			extendedIndexerDependable.Init(&module.NoopReadyDoneAware{})
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
extendedIndexerDependable := module.NewProxiedReadyDoneAware()
builder.IndexerDependencies.Add(extendedIndexerDependable)
extendedIndexerDependable := module.NewProxiedReadyDoneAware()
builder.IndexerDependencies.Add(extendedIndexerDependable)
if !builder.extendedIndexingEnabled {
extendedIndexerDependable.Init(&module.NoopReadyDoneAware{})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/access/node_builder/access_node_builder.go` around lines 866 - 867,
extendedIndexerDependable is added to builder.IndexerDependencies but only
initialized inside the builder.extendedIndexingEnabled Init() path, so when
extendedIndexingEnabled=false the execution data indexer DependableComponent
blocks waiting for it; fix by following the existing pattern used for
versionControlDependable/stopControlDependable: initialize
extendedIndexerDependable to the noop variant when the feature is disabled
(i.e., create a noop ProxiedReadyDoneAware instance instead of leaving it nil)
and then Add it to builder.IndexerDependencies so Init()/Ready/Done semantics
are satisfied even when builder.extendedIndexingEnabled is false.

Comment on lines +1398 to +1421
}).Module("extended index database", func(node *cmd.NodeConfig) error {
if !builder.extendedIndexingEnabled {
return nil
}

extendedStorage, err := extended.OpenExtendedIndexDB(
node.Logger,
builder.extendedIndexingDBPath,
builder.State.Params().SealedRoot().Height,
)
if err != nil {
return fmt.Errorf("could not open extended index database: %w", err)
}
builder.ExtendedStorage = extendedStorage

builder.ShutdownFunc(func() error {
if err := builder.ExtendedStorage.DB.Close(); err != nil {
return fmt.Errorf("error closing extended indexer db: %w", err)
}
return nil
})

return nil
}).Module("last full block height consumer progress", func(node *cmd.NodeConfig) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard extended indexing config to avoid nil deref when execution-data indexing is off.
ExtendedStorage and lastFullBlockHeight are only initialized inside the executionDataIndexingEnabled block, but ExtendedBackend creation is gated solely by extendedIndexingEnabled. If an operator enables extended indexing without execution-data indexing, this will panic when ExtendedStorage.* or lastFullBlockHeight are used. Please either enforce extendedIndexingEnabled ⇒ executionDataIndexingEnabled in flag validation or move extended index DB/lastFullBlockHeight initialization outside the execution-data-indexing gate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/observer/node_builder/observer_builder.go` around lines 1398 - 1421, The
code opens ExtendedStorage and sets lastFullBlockHeight only when
executionDataIndexingEnabled is true but constructs ExtendedBackend when
extendedIndexingEnabled is true, which can cause nil derefs; update the module
that creates ExtendedStorage/lastFullBlockHeight (using
extended.OpenExtendedIndexDB and setting builder.ExtendedStorage and
builder.lastFullBlockHeight) so it runs whenever extendedIndexingEnabled is true
(move initialization out of the execution-data-indexing gate) or alternatively
add a validation gate that enforces extendedIndexingEnabled ⇒
executionDataIndexingEnabled and return a clear error during flag validation;
also guard the builder.ShutdownFunc and any use-sites of builder.ExtendedStorage
or lastFullBlockHeight behind the same condition so they are never accessed when
nil.

Comment on lines +178 to +209
// NOTE: The filter logic is currently inverted. Despite the comment in the source saying
// "skip flow fee deposits", the code actually KEEPS only transfers TO the flow fees address
// (it continues/skips when RecipientAddress != flowFeesAddress). These tests verify the
// current behavior, not the intended behavior described in the comments.

t.Run("keeps transfers TO flow fees address with non-zero amount (current inverted behavior)", func(t *testing.T) {
transfers := []access.FungibleTokenTransfer{
{
RecipientAddress: flowFeesAddress,
Amount: big.NewInt(100),
TokenType: "A.0x1.FlowToken",
},
}

result := a.filterFTTransfers(transfers)
require.Len(t, result, 1)
assert.Equal(t, flowFeesAddress, result[0].RecipientAddress)
})

t.Run("filters out transfers NOT to flow fees address (current inverted behavior)", func(t *testing.T) {
otherAddress := flow.HexToAddress("0x1234567890abcdef")
transfers := []access.FungibleTokenTransfer{
{
RecipientAddress: otherAddress,
Amount: big.NewInt(100),
TokenType: "A.0x1.FlowToken",
},
}

result := a.filterFTTransfers(transfers)
assert.Empty(t, result)
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the filterFTTransfers method implementation
ast-grep --pattern $'func ($_) filterFTTransfers($$$) $_ {
  $$$
}'

Repository: onflow/flow-go

Length of output: 1764


🏁 Script executed:

# Verify the test expectations are actually failing by checking the test more carefully
cat -n module/state_synchronization/indexer/extended/account_transfers_test.go | sed -n '178,209p'

Repository: onflow/flow-go

Length of output: 1486


🏁 Script executed:

# Let me verify by looking at the complete context of the production code with more lines
cat -n module/state_synchronization/indexer/extended/account_transfers.go | sed -n '143,165p'

Repository: onflow/flow-go

Length of output: 981


🏁 Script executed:

# Let me cross-verify the logic by checking if there are any other implementations or if this test file was recently added
git log --oneline -n 20 module/state_synchronization/indexer/extended/account_transfers_test.go

Repository: onflow/flow-go

Length of output: 121


🏁 Script executed:

# Let me trace through the logic step by step by checking what the tests actually expect vs what the code does
cat -n module/state_synchronization/indexer/extended/account_transfers_test.go | sed -n '40,70p'

Repository: onflow/flow-go

Length of output: 1296


🏁 Script executed:

# Let me verify by running a logical trace: what does the code actually do?
# If RecipientAddress == flowFeesAddress: continue (skip) -> NOT included in result
# If RecipientAddress != flowFeesAddress: append -> IS included in result
# So: transfers TO flowFees are SKIPPED, transfers NOT to flowFees are KEPT

# Let me check if maybe there's another branch that modifies this
cat -n module/state_synchronization/indexer/extended/account_transfers.go | sed -n '130,160p'

Repository: onflow/flow-go

Length of output: 1330


🏁 Script executed:

# Double-check by examining the actual test execution expectations
# Test 1: sends transfer with RecipientAddress = flowFeesAddress, expects len = 1
# Production code: if recipient == flowFees, continue (skip)
# Result: transfer would NOT be in output → test would fail with len = 0

# Let me check if there's a filter in the other direction or if tests are actually passing
git log --all --oneline | grep -i "account_transfer\|filter" | head -20

Repository: onflow/flow-go

Length of output: 1501


🏁 Script executed:

# Let me verify the logic is what I think by creating a simple trace
# Production code: if recipient == flowFees THEN continue (skip it)
# This means: flowFees transfers are REMOVED, others are KEPT
# 
# Test 1 expects: transfer TO flowFees is KEPT (but code removes it)
# Test 2 expects: transfer NOT to flowFees is REMOVED (but code keeps it)
#
# So tests are inverted. Let me confirm by checking if these tests were added in the same commit
git show 3652635fb5 --stat | head -30

Repository: onflow/flow-go

Length of output: 1733


🏁 Script executed:

# Let me verify my analysis is correct by checking the complete context around where filterFTTransfers is called
rg "filterFTTransfers" -A 3 -B 3

Repository: onflow/flow-go

Length of output: 4883


🏁 Script executed:

# Let me check all the test cases for filterFTTransfers to see the full picture
cat -n module/state_synchronization/indexer/extended/account_transfers_test.go | sed -n '163,240p'

Repository: onflow/flow-go

Length of output: 3105


🏁 Script executed:

# Let me get the rest of the test to see the complete picture
cat -n module/state_synchronization/indexer/extended/account_transfers_test.go | sed -n '240,260p'

Repository: onflow/flow-go

Length of output: 776


🏁 Script executed:

# Let me verify the test would actually fail by understanding the complete logic one more time
# Production code at line 147-148: if recipient == flowFees, continue (skip it)
# This means: transfers TO flowFees are FILTERED OUT, others are KEPT
#
# Test at line 193: expects result.Len() == 1 when input has transfer TO flowFees
# This would FAIL because production code filters it out
#
# Let me check if there are any other implementations or recent changes
git log --all --pretty=format:"%h %s" module/state_synchronization/indexer/extended/account_transfers.go | head -5

Repository: onflow/flow-go

Length of output: 121


🏁 Script executed:

# Final verification: run the tests to confirm they would fail
cat -n module/state_synchronization/indexer/extended/account_transfers_test.go | sed -n '163,176p'

Repository: onflow/flow-go

Length of output: 511


These tests are inverted and would fail against the current production code.

The production code at lines 147–148 of account_transfers.go skips (filters out) transfers where RecipientAddress == flowFeesAddress, then keeps all others. However, the tests expect the opposite: keeping transfers TO the flow fees address and filtering out transfers NOT to it.

Specifically:

  • Test at line 193 sends a transfer TO flowFeesAddress and expects len(result) == 1, but the code skips that transfer, so it would fail with len(result) == 0
  • Test at line 208 sends a transfer NOT to flowFeesAddress and expects the result to be empty, but the code keeps that transfer, so it would fail with len(result) == 1
  • The mixed test at line 259 also expects only transfers TO flowFees to be kept, which contradicts the production logic

Additionally, the test comment itself (lines 178–181) is inaccurate—it claims the code "continues/skips when RecipientAddress != flowFeesAddress", but the code actually skips when RecipientAddress == flowFeesAddress.

Either the production code logic is missing a negation (should skip != instead of ==), or all these tests need to be rewritten to match the current (apparently correct) production logic. The current state is not viable—these tests will not pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@module/state_synchronization/indexer/extended/account_transfers_test.go`
around lines 178 - 209, The tests for filterFTTransfers are inverted relative to
production: the code in account_transfers.go currently skips transfers where
RecipientAddress == flowFeesAddress, but the tests expect the opposite. Fix the
tests (and their header comment) to reflect the actual behavior of
filterFTTransfers: 1) update the test cases to expect that transfers TO
flowFeesAddress are filtered out (len==0) and that transfers NOT to
flowFeesAddress are kept (len==1 and RecipientAddress equals the other address);
2) update the descriptive comment that currently claims the logic is inverted to
state the real behavior (skip when RecipientAddress == flowFeesAddress); and 3)
adjust the mixed-case test expectations accordingly so all assertions match the
implementation of filterFTTransfers.

Comment on lines +431 to +435
func ftEventIndex(entry access.FungibleTokenTransfer) uint32 {
// use the last event index. this is either the deposit event or the last withdrawal event
// if the vault was destroyed.
return entry.EventIndices[len(entry.EventIndices)-1]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the file and understand the context around ftEventIndex
fd -t f "account_ft_transfers.go" --exec wc -l {}

Repository: onflow/flow-go

Length of output: 244


🏁 Script executed:

# Search for EventIndices definition and usage
rg -n 'EventIndices' --type=go -B2 -A2 | head -120

Repository: onflow/flow-go

Length of output: 8899


🏁 Script executed:

# Find the FungibleTokenTransfer struct definition
rg -n 'type FungibleTokenTransfer' --type=go -A10

Repository: onflow/flow-go

Length of output: 43567


🏁 Script executed:

# Check the callers of ftEventIndex function
rg -n 'ftEventIndex' --type=go -B3 -A3

Repository: onflow/flow-go

Length of output: 2207


🏁 Script executed:

# Look for any validation of EventIndices before ftEventIndex is called
rg -n 'len.*EventIndices' --type=go -B2 -A2

Repository: onflow/flow-go

Length of output: 1963


🏁 Script executed:

# Look at the Store method implementation to see if it validates EventIndices
rg -n 'func.*Store.*transfers' storage/indexes/account_ft_transfers.go -A 20 | head -60

Repository: onflow/flow-go

Length of output: 842


🏁 Script executed:

# Check for any validation on EventIndices before use
rg -n 'if.*len.*EventIndices' storage/indexes/account_ft_transfers.go

Repository: onflow/flow-go

Length of output: 40


🏁 Script executed:

# Search for where FungibleTokenTransfer is created from execution data
rg -n 'EventIndices.*=|EventIndices:' storage/indexes/account_ft_transfers.go -B 2 -A 2

Repository: onflow/flow-go

Length of output: 498


🏁 Script executed:

# Check the bootstrapper to see how EventIndices is populated
cat storage/indexes/account_ft_transfers_bootstrapper.go | head -100

Repository: onflow/flow-go

Length of output: 3883


🏁 Script executed:

# Search for where FungibleTokenTransfer is created/constructed from execution data
rg -n 'FungibleTokenTransfer{|&access.FungibleTokenTransfer' --type=go | head -40

Repository: onflow/flow-go

Length of output: 214


🏁 Script executed:

# Look for where EventIndices is set during transfer creation
rg -n 'EventIndices:' --type=go -B 3 -A 1 | head -80

Repository: onflow/flow-go

Length of output: 5869


🏁 Script executed:

# Check the indexFTTransfers function to see if it validates transfers
sed -n '220,360p' storage/indexes/account_ft_transfers.go

Repository: onflow/flow-go

Length of output: 5306


🏁 Script executed:

# Check if Store method or any caller validates EventIndices before calling indexFTTransfers
sed -n '185,210p' storage/indexes/account_ft_transfers.go

Repository: onflow/flow-go

Length of output: 840


🏁 Script executed:

# Check initializeFTTransfers to see if it validates EventIndices
rg -n 'func initializeFTTransfers' storage/indexes/account_ft_transfers.go -A 30 | head -50

Repository: onflow/flow-go

Length of output: 1341


🏁 Script executed:

# Verify there's no runtime guarantee that EventIndices is non-empty by checking where transfers come from
rg -n 'Store.*transfers' storage/indexes/account_ft_transfers_bootstrapper.go -A 10 | head -30

Repository: onflow/flow-go

Length of output: 1453


🏁 Script executed:

# Check if there's any validation when transfers slice is received
sed -n '185,210p' storage/indexes/account_ft_transfers_bootstrapper.go

Repository: onflow/flow-go

Length of output: 40


Potential panic if EventIndices is empty or nil.

entry.EventIndices[len(entry.EventIndices)-1] will panic if EventIndices is an empty or nil slice. There is no validation of EventIndices at any layer in the call chain—not in Store(), indexFTTransfers(), initializeFTTransfers(), or lookupFTTransfers(). Per the coding guidelines ("Treat all inputs as potentially byzantine and validate accordingly"), EventIndices should be validated before use.

Add a guard in ftEventIndex or validate EventIndices at the Store() entry point:

Proposed fix
 func ftEventIndex(entry access.FungibleTokenTransfer) uint32 {
+	if len(entry.EventIndices) == 0 {
+		return 0
+	}
 	// use the last event index. this is either the deposit event or the last withdrawal event
 	// if the vault was destroyed.
 	return entry.EventIndices[len(entry.EventIndices)-1]
 }

Alternatively, validate in Store() and return an error for transfers with empty EventIndices.

Note: The same vulnerability exists in account_nft_transfers.go at line 415.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func ftEventIndex(entry access.FungibleTokenTransfer) uint32 {
// use the last event index. this is either the deposit event or the last withdrawal event
// if the vault was destroyed.
return entry.EventIndices[len(entry.EventIndices)-1]
}
func ftEventIndex(entry access.FungibleTokenTransfer) uint32 {
if len(entry.EventIndices) == 0 {
return 0
}
// use the last event index. this is either the deposit event or the last withdrawal event
// if the vault was destroyed.
return entry.EventIndices[len(entry.EventIndices)-1]
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storage/indexes/account_ft_transfers.go` around lines 431 - 435, ftEventIndex
currently dereferences entry.EventIndices without checking length, which can
panic when EventIndices is nil/empty; update the validation to reject transfers
with empty EventIndices at the Store() entry path (where indexFTTransfers(),
initializeFTTransfers(), and lookupFTTransfers() begin) by returning an error
for any access.FungibleTokenTransfer with len(EventIndices) == 0, and also add a
defensive guard inside ftEventIndex that checks len(entry.EventIndices) > 0 and
returns a safe zero value (or a documented sentinel) instead of indexing into
the slice; apply the same validation/defensive-guard pattern to the analogous
function in account_nft_transfers.go.

package mock

import (
"github.com/jordanschalm/lockctx"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check go.mod and go.sum for lockctx
echo "=== Checking go.mod/go.sum ===" 
grep -n "jordanschalm/lockctx" go.mod go.sum 2>/dev/null || echo "lockctx not found in go.mod/go.sum"

# Check usage across the codebase
echo -e "\n=== Checking usage in codebase ===" 
rg -n "jordanschalm/lockctx" --type go

Repository: onflow/flow-go

Length of output: 17769


🏁 Script executed:

# Check the specific file mentioned in the review
echo "=== Reading the file with lockctx import ===" 
cat -n storage/mock/account_transactions_bootstrapper.go | head -30

Repository: onflow/flow-go

Length of output: 1297


🏁 Script executed:

# Check if lockctx is used in production code or only in test/mock code
echo "=== Checking context of lockctx usage ===" 
rg -B 2 -A 5 "lockctx\." --type go | head -50

Repository: onflow/flow-go

Length of output: 3450


Address the v0.0.0 pseudo-version risk for the lockctx dependency.

github.com/jordanschalm/lockctx is pinned in go.mod but uses a v0.0.0 pseudo-version (v0.0.0-20250412215529-226f85c10956), indicating it tracks a specific commit rather than a stable release. This dependency is deeply integrated into production code across 150+ files (storage, state, execution, consensus, and main entry points), not isolated to test/mock code. For long-term stability, consider: (1) whether a v1.0.0+ stable release is available, (2) if the v0.0.0 strategy is intentional and documented, or (3) whether a vendored copy would reduce risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storage/mock/account_transactions_bootstrapper.go` at line 8, The project
imports the module github.com/jordanschalm/lockctx (seen in the import list)
which is pinned to a v0.0.0 pseudo-version; inspect go.mod and replace the
pseudo-version with a stable tag if a v1.0.0+ (or other maintained release)
exists, or explicitly document and justify the pseudo-version choice in the repo
README/CONTRIBUTING and add a comment near the import referencing that decision,
or vendor the dependency (update module vendor directory and add a note in
go.mod/GOVENDOR.json) so production code
(storage/state/execution/consensus/main) does not rely on a floating commit.
Ensure you update go.sum accordingly and run go mod tidy and run tests to
validate behavior after changing github.com/jordanschalm/lockctx.

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

Comments