[ANCHOR-1218]: StellarRpcPaymentObserver indexes filtered op list with full-tx operationIndex → payments never credited - #1953
Merged
Conversation
* fix operation lookup for soroban events using `toid` * refactor operation identification to correctly handle `operationindex` * add a method to get `ledgeroperation` id * add check to skip contract sub-invocations without direct operations * add tests for operation index mapping and sub-invocation scenarios
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes a critical indexing bug in StellarRpcPaymentObserver.processTransferEvent where Soroban-RPC event operationIndex (full on-chain op array) was incorrectly used to index into a filtered/compacted LedgerTransaction.operations list, causing some valid payment events to be dropped and never credited.
Changes:
- Replace positional indexing with a TOID-derived operation-id lookup to find the correct compacted
LedgerOperation. - Add a helper to extract operation ids from the concrete sub-operation type on
LedgerOperation. - Add unit + domain/integration regression tests covering single-op, multi-op-with-filtered-op, and sub-invocation (no matching compacted op) scenarios.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| platform/src/main/java/org/stellar/anchor/platform/observer/stellar/StellarRpcPaymentObserver.java | Uses TOID-based identity matching to select the correct operation from the compacted list and logs/skips when no match exists. |
| platform/src/test/kotlin/org/stellar/anchor/platform/observer/stellar/StellarRpcPaymentObserverTest.kt | Adds focused unit regression tests for TOID-based lookup and “no matching op” skip behavior. |
| platform/src/test/kotlin/org/stellar/anchor/platform/observer/stellar/StellarRpcObserverIndexDomainTest.kt | Adds scheduler-driven domain tests validating end-to-end crediting/skipping behavior across mixed batches. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* refactor error message into multiple lines for improved readability
JiahuiWho
approved these changes
Jun 17, 2026
travertischio
force-pushed
the
develop
branch
2 times, most recently
from
June 23, 2026 15:43
f84f3ae to
2bbb21a
Compare
amandagonsalves
force-pushed
the
fix/rpc-observer-op-index-domain
branch
2 times, most recently
from
June 23, 2026 16:36
e84a53a to
2765f36
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Before this change,
StellarRpcPaymentObserver.processTransferEventselected the credited operation with:event.getOperationIndex()is a 0-based index into the full on-chain operation array as returned by the Soroban-RPCgetEventsAPI.txn.getOperations()is not that full array — it is the compacted output ofLedgerClientHelper.getLedgerOperations(), which silently drops every operation for whichconvert()returnsnull(MANAGE_DATA,CHANGE_TRUST,CREATE_ACCOUNT,BUMP_SEQUENCE,SET_OPTIONS, anyINVOKE_HOST_FUNCTIONwhose direct function name is not"transfer", etc.) with no placeholder. Using the full-list index against the shorter compacted list produced two failure modes:IndexOutOfBoundsException→ caught by the generic handler → event permanently dropped, SEP-6/24/31 transaction frozen, user funds received but never credited.INVOKE_HOST_FUNCTIONwhose direct function name is not"transfer"(contract/SEP-45 sub-invocation) emits atransferevent at the full-list index of that op, whichconvert()drops → same out-of-bounds or wrong-element result.The fix stops trusting positional alignment.
convert()stores the TOID — derived from(sequenceNumber, applicationOrder, 1-based opIndex)— as each operation'sid. The event'soperationIndex(0-based) is converted to the same 1-based coordinate, the TOID for the target operation is derived, and the compacted list is stream-filtered by identity match. When no match is found (sub-invocation variant), the event is logged aterrorlevel and skipped rather than silently swallowed as a genericWARN.Changes
StellarRpcPaymentObserver.processTransferEvent: replacedtxn.getOperations().get(operationIndex)with a TOID-based identity lookup — deriveswantedOpIdfromnew TOID(sequenceNumber, applicationOrder, operationIndex + 1).toInt64(), then stream-filterstxn.getOperations()bygetOperationId(o).equals(wantedOpId). Addsimport org.stellar.sdk.TOID.StellarRpcPaymentObserver.getOperationId: new private helper that extracts theidfield from whichever sub-operation (paymentOperation,pathPaymentOperation, orinvokeHostFunctionOperation) is set on aLedgerOperation.StellarRpcPaymentObserver.processTransferEventnull branch: when TOID lookup yields no match, logs anerrorFmessage identifying the transaction hash and operation index, then returns — no exception is swallowed, the observer staysRUNNING.StellarRpcPaymentObserverTest: three new unit regression tests forprocessTransferEvent— single-op atoperationIndex=0still credited, multi-op with non-payment op at index 0 and payment at index 1 correctly credited, sub-invocation with empty compacted list skips without callinghandleEvent.StellarRpcObserverIndexDomainTest(new file): three integration tests using a real scheduler (observer.start()) and aCaptureListener— multi-op transaction credited end-to-end, sub-invocation skipped with observer remainingRUNNING, mixed batch crediting both normal and multi-op events while skipping the sub-invocation.Acceptance Criteria
[MANAGE_DATA, PAYMENT→dist]withtransferevent atoperationIndex=1is credited with the correct from/to/amount/operationId.transferevent whose corresponding full-list operation is absent from the compacted list (contract sub-invocation) is not credited; noPaymentTransferEventis dispatched to listeners.RUNNINGafter any of the above scenarios.StellarRpcPaymentObserverTestandStellarRpcObserverPoisonResilienceTesttests continue to pass unchanged.HorizonPaymentObserver) is unaffected.Context
#3791580
Testing
./gradlew :platform:test --tests "org.stellar.anchor.platform.observer.stellar.StellarRpcPaymentObserverTest"./gradlew :platform:test --tests "org.stellar.anchor.platform.observer.stellar.StellarRpcObserverIndexDomainTest"./gradlew :platform:testDocumentation
N/A
Known limitations
The sub-invocation case (Variant B) — a
transferevent emitted by a contract that moves tokens internally rather than via a top-level SACtransfercall — is logged as an error and skipped. Crediting from the authenticated event fields (the reporter's Option 2) would handle this variant without requiring a matching operation in the compacted list, but it requires verifying that the SAC contract address can be reliably resolved to an anchor asset from the event alone, which is not currently supported bySacToAssetMapper. Variant B is tracked separately.