From 77799e9b3aad7f44c0569933c0a048c746dd621a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 17:34:04 +0000 Subject: [PATCH] fix(analytics): never read a settled swap amount out of a failed transaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getSettledPathPaymentStrictSendAmount accepted both txSuccess and txFailed before selecting the operation result. A txFailed result still carries per-operation results, and an operation that succeeded before a later one failed reports its own pathPaymentStrictSendSuccess there — but Stellar transactions are atomic, so that path payment was rolled back and nothing settled. The helper would return the rolled-back amount, contradicting the clean-success contract its own docstring states and reporting to_amount / to_amount_usd for a swap that never happened. Require txSuccess. The fee-bump descent is unchanged: an inner result that is not txSuccess now returns null through the same check. The added test builds a txFailed result whose first operation is a settled path payment and whose second failed; it returns "5" against the previous code and null with the fix. The existing txFailed case only covered the path payment itself failing, so this gap was untested. Found by Copilot on the mobile port of this telemetry (stellar/freighter-mobile#996), which carries the identical helper and the same fix. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TVe5MM4mHCXUTevzdvHhq4 --- .../src/helpers/transactionResult.test.ts | 38 +++++++++++++++++++ extension/src/helpers/transactionResult.ts | 10 +++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/extension/src/helpers/transactionResult.test.ts b/extension/src/helpers/transactionResult.test.ts index 9bd1545eef..aec26c2781 100644 --- a/extension/src/helpers/transactionResult.test.ts +++ b/extension/src/helpers/transactionResult.test.ts @@ -100,6 +100,44 @@ describe("getSettledPathPaymentStrictSendAmount", () => { getSettledPathPaymentStrictSendAmount(txResult.toXdr("base64"), 0), ).toBeNull(); }); + + it("returns null when the path payment succeeded but a later operation failed", () => { + // Stellar transactions are atomic: a txFailed result still reports the + // earlier operation's own success, but that path payment was rolled back + // and nothing settled. Reading the amount out of it would report volume + // for a swap that never happened. + const settledPathPayment = xdr.OperationResult.opInner( + xdr.OperationResultTr.pathPaymentStrictSend( + xdr.PathPaymentStrictSendResult.pathPaymentStrictSendSuccess( + new xdr.PathPaymentStrictSendResultSuccess({ + offers: [], + last: new xdr.SimplePaymentResult({ + destination: xdr.PublicKey.publicKeyTypeEd25519( + Keypair.random().rawPublicKey(), + ), + asset: Asset.native().toXdrObject(), + amount: BigInt("50000000"), + }), + }), + ), + ), + ); + const laterFailedOp = xdr.OperationResult.opInner( + xdr.OperationResultTr.payment(xdr.PaymentResult.paymentUnderfunded()), + ); + const txResult = new xdr.TransactionResult({ + feeCharged: BigInt("100"), + result: xdr.TransactionResultResult.txFailed([ + settledPathPayment, + laterFailedOp, + ]), + ext: xdr.TransactionResultExt.v0(), + }); + + expect( + getSettledPathPaymentStrictSendAmount(txResult.toXdr("base64"), 0), + ).toBeNull(); + }); }); describe("findPathPaymentStrictSendIndex", () => { diff --git a/extension/src/helpers/transactionResult.ts b/extension/src/helpers/transactionResult.ts index 2fda3eac6a..c7b94825c8 100644 --- a/extension/src/helpers/transactionResult.ts +++ b/extension/src/helpers/transactionResult.ts @@ -50,10 +50,12 @@ export const getSettledPathPaymentStrictSendAmount = ( ? innerResult.innerResultPair.result.result : innerResult; - if ( - innerTxResult.type !== "txSuccess" && - innerTxResult.type !== "txFailed" - ) { + // txSuccess only. A `txFailed` result still carries per-operation + // results, and an operation that succeeded before a later one failed + // reports its own success there — but Stellar transactions are atomic, so + // that path payment was rolled back and nothing settled. Reading an + // amount out of it would report volume for a swap that never happened. + if (innerTxResult.type !== "txSuccess") { return null; } const opResults = innerTxResult.results;