From 9a34bee9e262db889ab60095ca9dfe7d0ead1b94 Mon Sep 17 00:00:00 2001 From: chrisx9z <287960381+chrisx9z@users.noreply.github.com> Date: Tue, 26 May 2026 23:35:53 +0700 Subject: [PATCH 1/2] Add claimable balance result ID helper Comment: Extract claimable balance IDs from successful transaction result XDRs, including fee-bump inner results. --- src/get_claimable_balance_id.js | 95 +++++++++++++++++++++ src/index.js | 1 + test/unit/get_claimable_balance_id_test.js | 97 ++++++++++++++++++++++ types/index.d.ts | 4 + types/test.ts | 1 + 5 files changed, 198 insertions(+) create mode 100644 src/get_claimable_balance_id.js create mode 100644 test/unit/get_claimable_balance_id_test.js diff --git a/src/get_claimable_balance_id.js b/src/get_claimable_balance_id.js new file mode 100644 index 000000000..45a2b4a7f --- /dev/null +++ b/src/get_claimable_balance_id.js @@ -0,0 +1,95 @@ +import xdr from './xdr'; + +function parseTransactionResult(transactionResult) { + if (typeof transactionResult === 'string') { + return xdr.TransactionResult.fromXDR(transactionResult, 'base64'); + } + + if (typeof Buffer !== 'undefined' && Buffer.isBuffer(transactionResult)) { + return xdr.TransactionResult.fromXDR(transactionResult); + } + + if (!transactionResult || typeof transactionResult.result !== 'function') { + throw new TypeError( + 'transactionResult must be a base64 string, Buffer, or xdr.TransactionResult' + ); + } + + return transactionResult; +} + +function getOperationResults(transactionResult) { + const result = transactionResult.result(); + const resultCode = result.switch().name; + + if (resultCode === 'txSuccess') { + return result.results(); + } + + if (resultCode === 'txFeeBumpInnerSuccess') { + return result + .innerResultPair() + .result() + .result() + .results(); + } + + throw new Error( + `transaction result does not contain successful operation results: ${resultCode}` + ); +} + +/** + * getClaimableBalanceIdFromResult extracts a successful createClaimableBalance + * operation's balance ID from a transaction result XDR. + * + * @export + * @param {string|Buffer|xdr.TransactionResult} transactionResult - The transaction + * result XDR as base64, raw Buffer, or XDR object. + * @param {number} opIndex - The operation index containing createClaimableBalance. + * + * @return {string} the claimable balance ID as a hex string. + */ +export function getClaimableBalanceIdFromResult( + transactionResult, + opIndex = 0 +) { + if (!Number.isInteger(opIndex) || opIndex < 0) { + throw new RangeError('invalid operation index'); + } + + const operationResults = getOperationResults( + parseTransactionResult(transactionResult) + ); + + if (opIndex >= operationResults.length) { + throw new RangeError('invalid operation index'); + } + + const operationResult = operationResults[opIndex]; + if (operationResult.switch().name !== 'opInner') { + throw new TypeError( + `expected successful operation result, got ${operationResult.switch().name}` + ); + } + + const operationResultTr = operationResult.tr(); + if (operationResultTr.switch().name !== 'createClaimableBalance') { + throw new TypeError( + `expected createClaimableBalance result, got ${operationResultTr.switch().name}` + ); + } + + const createClaimableBalanceResult = + operationResultTr.createClaimableBalanceResult(); + if ( + createClaimableBalanceResult.switch().name !== + 'createClaimableBalanceSuccess' + ) { + throw new TypeError( + `expected successful createClaimableBalance result, got ${createClaimableBalanceResult.switch().name}` + ); + } + + return createClaimableBalanceResult.balanceId().toXDR('hex'); +} diff --git a/src/index.js b/src/index.js index ded100fac..99ad8fa99 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ export { getLiquidityPoolId, LiquidityPoolFeeV18 } from './get_liquidity_pool_id'; +export { getClaimableBalanceIdFromResult } from './get_claimable_balance_id'; export { Keypair } from './keypair'; export { UnsignedHyper, Hyper } from '@stellar/js-xdr'; export { TransactionBase } from './transaction_base'; diff --git a/test/unit/get_claimable_balance_id_test.js b/test/unit/get_claimable_balance_id_test.js new file mode 100644 index 000000000..ea60e6dbc --- /dev/null +++ b/test/unit/get_claimable_balance_id_test.js @@ -0,0 +1,97 @@ +function buildClaimableBalanceResult() { + const balanceId = StellarBase.xdr.ClaimableBalanceId.claimableBalanceIdTypeV0( + Buffer.from( + '536af35c666a28d26775008321655e9eda2039154270484e3f81d72c66d5c26f', + 'hex' + ) + ); + + const createClaimableBalanceResult = + StellarBase.xdr.CreateClaimableBalanceResult.createClaimableBalanceSuccess( + balanceId + ); + const operationResultTr = + StellarBase.xdr.OperationResultTr.createClaimableBalance( + createClaimableBalanceResult + ); + const operationResult = StellarBase.xdr.OperationResult.opInner( + operationResultTr + ); + + return { + balanceId, + operationResult + }; +} + +function buildTransactionResult(result) { + return new StellarBase.xdr.TransactionResult({ + feeCharged: StellarBase.xdr.Int64.fromString('100'), + result, + ext: new StellarBase.xdr.TransactionResultExt(0) + }); +} + +describe('StellarBase#getClaimableBalanceIdFromResult()', function () { + it('extracts the claimable balance ID from a result XDR string', function () { + const { balanceId, operationResult } = buildClaimableBalanceResult(); + const transactionResult = buildTransactionResult( + StellarBase.xdr.TransactionResultResult.txSuccess([operationResult]) + ); + + expect( + StellarBase.getClaimableBalanceIdFromResult( + transactionResult.toXDR('base64') + ) + ).to.equal(balanceId.toXDR('hex')); + }); + + it('extracts the claimable balance ID from a TransactionResult object', function () { + const { balanceId, operationResult } = buildClaimableBalanceResult(); + const transactionResult = buildTransactionResult( + StellarBase.xdr.TransactionResultResult.txSuccess([operationResult]) + ); + + expect( + StellarBase.getClaimableBalanceIdFromResult(transactionResult) + ).to.equal(balanceId.toXDR('hex')); + }); + + it('extracts the claimable balance ID from a successful fee bump inner result', function () { + const { balanceId, operationResult } = buildClaimableBalanceResult(); + const innerResult = new StellarBase.xdr.InnerTransactionResult({ + feeCharged: StellarBase.xdr.Int64.fromString('100'), + result: StellarBase.xdr.InnerTransactionResultResult.txSuccess([ + operationResult + ]), + ext: new StellarBase.xdr.InnerTransactionResultExt(0) + }); + const innerResultPair = new StellarBase.xdr.InnerTransactionResultPair({ + transactionHash: Buffer.alloc(32), + result: innerResult + }); + const transactionResult = buildTransactionResult( + StellarBase.xdr.TransactionResultResult.txFeeBumpInnerSuccess( + innerResultPair + ) + ); + + expect( + StellarBase.getClaimableBalanceIdFromResult(transactionResult) + ).to.equal(balanceId.toXDR('hex')); + }); + + it('throws when the operation index is invalid', function () { + const { operationResult } = buildClaimableBalanceResult(); + const transactionResult = buildTransactionResult( + StellarBase.xdr.TransactionResultResult.txSuccess([operationResult]) + ); + + expect(() => + StellarBase.getClaimableBalanceIdFromResult(transactionResult, -1) + ).to.throw(/operation index/); + expect(() => + StellarBase.getClaimableBalanceIdFromResult(transactionResult, 1) + ).to.throw(/operation index/); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index 1977b29af..20960c6ba 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -172,6 +172,10 @@ export class Keypair { export const LiquidityPoolFeeV18 = 30; export function getLiquidityPoolId(liquidityPoolType: LiquidityPoolType, liquidityPoolParameters: LiquidityPoolParameters): Buffer; +export function getClaimableBalanceIdFromResult( + transactionResult: string | Buffer | xdr.TransactionResult, + opIndex?: number, +): string; export namespace LiquidityPoolParameters { interface ConstantProduct { diff --git a/types/test.ts b/types/test.ts index 43cb60197..59e735d5e 100644 --- a/types/test.ts +++ b/types/test.ts @@ -7,6 +7,7 @@ const usd = new StellarSdk.Asset('USD', 'GDGU5OAPHNPU5UCLE5RDJHG7PXZFQYWKCFOEXSX const account = new StellarSdk.Account(sourceKey.publicKey(), '1'); // $ExpectType Account const muxedAccount = new StellarSdk.MuxedAccount(account, '123'); // $ExpectType MuxedAccount const muxedConforms = muxedAccount as StellarSdk.Account; // $ExpectType Account +StellarSdk.getClaimableBalanceIdFromResult('AAAA'); // $ExpectType string const transaction = new StellarSdk.TransactionBuilder(account, { fee: "100", From d43e11b56ea7dc50896ba52c776d2a1ab9a2bd70 Mon Sep 17 00:00:00 2001 From: chrisx9z <287960381+chrisx9z@users.noreply.github.com> Date: Wed, 27 May 2026 10:46:21 +0700 Subject: [PATCH 2/2] Address claimable balance helper review Comment: Clarifies optional operation index behavior and covers non-success transaction results. --- src/get_claimable_balance_id.js | 11 ++++++++--- test/unit/get_claimable_balance_id_test.js | 11 +++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/get_claimable_balance_id.js b/src/get_claimable_balance_id.js index 45a2b4a7f..bba20745d 100644 --- a/src/get_claimable_balance_id.js +++ b/src/get_claimable_balance_id.js @@ -46,7 +46,8 @@ function getOperationResults(transactionResult) { * @export * @param {string|Buffer|xdr.TransactionResult} transactionResult - The transaction * result XDR as base64, raw Buffer, or XDR object. - * @param {number} opIndex - The operation index containing createClaimableBalance. + * @param {number} [opIndex=0] - The operation index containing + * createClaimableBalance. Defaults to the first operation result. * * @return {string} the claimable balance ID as a hex string. */ @@ -55,7 +56,7 @@ export function getClaimableBalanceIdFromResult( opIndex = 0 ) { if (!Number.isInteger(opIndex) || opIndex < 0) { - throw new RangeError('invalid operation index'); + throw new RangeError(`invalid operation index: ${opIndex}`); } const operationResults = getOperationResults( @@ -63,7 +64,11 @@ export function getClaimableBalanceIdFromResult( ); if (opIndex >= operationResults.length) { - throw new RangeError('invalid operation index'); + throw new RangeError( + `invalid operation index: ${opIndex}; valid range is 0..${ + operationResults.length - 1 + }` + ); } const operationResult = operationResults[opIndex]; diff --git a/test/unit/get_claimable_balance_id_test.js b/test/unit/get_claimable_balance_id_test.js index ea60e6dbc..9df31e4a5 100644 --- a/test/unit/get_claimable_balance_id_test.js +++ b/test/unit/get_claimable_balance_id_test.js @@ -94,4 +94,15 @@ describe('StellarBase#getClaimableBalanceIdFromResult()', function () { StellarBase.getClaimableBalanceIdFromResult(transactionResult, 1) ).to.throw(/operation index/); }); + + it('throws when the transaction result is not successful', function () { + const { operationResult } = buildClaimableBalanceResult(); + const transactionResult = buildTransactionResult( + StellarBase.xdr.TransactionResultResult.txFailed([operationResult]) + ); + + expect(() => + StellarBase.getClaimableBalanceIdFromResult(transactionResult) + ).to.throw(/txFailed/); + }); });