From 8edbe98d216742f8daeecc2c0e606c755c8ca94b Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Thu, 19 Feb 2026 13:00:17 -0800 Subject: [PATCH 1/9] Implement addSacTransferOperation for SAC transfers and update types --- CHANGELOG.md | 3 + src/transaction_builder.js | 182 +++++++++++++- test/unit/transaction_builder_test.js | 326 ++++++++++++++++++++++++++ types/index.d.ts | 20 ++ 4 files changed, 530 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd62edbb9..250097a10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Added +* Implemented `TransactionBuilder.addSacTransferOperation` to remove the need for simulation for SAC (Stellar Asset Contracts) transfers by creating the appropriate auth entries and footprint ([#861](https://github.com/stellar/js-stellar-base/pull/861)). + ## [`v14.0.4`](https://github.com/stellar/js-stellar-base/compare/v14.0.3...v14.0.4): diff --git a/src/transaction_builder.js b/src/transaction_builder.js index cf90d23ff..aa328ac09 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -1,4 +1,4 @@ -import { UnsignedHyper } from '@stellar/js-xdr'; +import { UnsignedHyper, Hyper } from '@stellar/js-xdr'; import BigNumber from './util/bignumber'; import xdr from './xdr'; @@ -14,6 +14,11 @@ import { SorobanDataBuilder } from './sorobandata_builder'; import { StrKey } from './strkey'; import { SignerKey } from './signerkey'; import { Memo } from './memo'; +import { Asset } from './asset'; +import { nativeToScVal } from './scval'; +import { Operation } from './operation'; +import { Address } from './address'; +import { Keypair } from './keypair'; /** * Minimum base fee for transactions. If this fee is below the network @@ -34,6 +39,14 @@ export const BASE_FEE = '100'; // Stroops */ export const TimeoutInfinite = 0; +/** + * @typedef {object} SorobanFees + * @property {number} instructions - the number of instructions executed by the transaction + * @property {number} readBytes - the number of bytes read from the ledger by the transaction + * @property {number} writeBytes - the number of bytes written to the ledger by the transaction + * @property {number} resourceFee - the fee to be paid for the transaction, in stroops (int64) + */ + /** *

Transaction builder helps constructs a new `{@link Transaction}` using the * given {@link Account} as the transaction's "source account". The transaction @@ -576,6 +589,173 @@ export class TransactionBuilder { return this; } + /** + * Creates and adds an invoke host function operation for transferring SAC tokens. + * This method removes the need for simulation by handling the creation of the + * appropriate authorization entries and ledger footprint for the transfer operation. + * + * @param {string} destination - the address of the recipient of the SAC transfer (should be a valid Stellar address or contract ID) + * @param {Asset} asset - the SAC asset to be transferred + * @param {string | BigInt} amount - the amount of tokens to be transferred in stroops IE. 1 token with 7 decimals of precision would be represented as "1_0000000" + * @param {SorobanFees} [sorobanFees] - optional Soroban fees for the transaction + * + * @returns {TransactionBuilder} + */ + addSacTransferOperation(destination, asset, amount, sorobanFees) { + if (BigInt(amount) <= 0n) { + throw new Error('Amount must be a positive integer in stroops'); + } else if (BigInt(amount) > Hyper.MAX_VALUE) { + // The largest supported value for SAC is i64 however the contract interface uses i128 which is why we convert it to i128 + throw new Error('Amount exceeds maximum value for i64'); + } + const contractId = asset.contractId(this.networkPassphrase); + const functionName = 'transfer'; + const source = this.source.accountId(); + const args = [ + nativeToScVal(source, { type: 'address' }), + nativeToScVal(destination, { type: 'address' }), + nativeToScVal(amount, { type: 'i128' }) + ]; + const isDestinationContract = StrKey.isValidContract(destination); + const isAssetNative = asset.isNative(); + + if (!isDestinationContract) { + if ( + !StrKey.isValidEd25519PublicKey(destination) && + !StrKey.isValidMed25519PublicKey(destination) + ) { + throw new Error( + 'Invalid destination address. Must be a valid Stellar address or contract ID.' + ); + } + } + + const auths = new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsSourceAccount(), + rootInvocation: new xdr.SorobanAuthorizedInvocation({ + function: + xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( + new xdr.InvokeContractArgs({ + contractAddress: Address.fromString(contractId).toScAddress(), + functionName, + args + }) + ), + subInvocations: [] + }) + }); + + const footprint = new xdr.LedgerFootprint({ + readOnly: [ + xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: Address.fromString(contractId).toScAddress(), + key: xdr.ScVal.scvLedgerKeyContractInstance(), + durability: xdr.ContractDataDurability.persistent() + }) + ) + ], + readWrite: [] + }); + + // Ledger entries for the destination account + if (isDestinationContract) { + footprint.readWrite().push( + xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: Address.fromString(contractId).toScAddress(), + key: xdr.ScVal.scvVec([ + nativeToScVal('Balance', { type: 'symbol' }), + nativeToScVal(destination, { type: 'address' }) + ]), + durability: xdr.ContractDataDurability.persistent() + }) + ) + ); + + if (!isAssetNative) { + footprint.readOnly().push( + xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(asset.getIssuer()).xdrPublicKey() + }) + ) + ); + } + } else if (isAssetNative) { + footprint.readWrite().push( + xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(destination).xdrPublicKey() + }) + ) + ); + } else if (asset.getIssuer() !== destination) { + footprint.readWrite().push( + xdr.LedgerKey.trustline( + new xdr.LedgerKeyTrustLine({ + accountId: Keypair.fromPublicKey(destination).xdrPublicKey(), + asset: asset.toTrustLineXDRObject() + }) + ) + ); + } + + // Ledger entries for the source account + if (asset.isNative()) { + footprint.readWrite().push( + xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(source).xdrPublicKey() + }) + ) + ); + } else if (asset.getIssuer() !== source) { + footprint.readWrite().push( + xdr.LedgerKey.trustline( + new xdr.LedgerKeyTrustLine({ + accountId: Keypair.fromPublicKey(source).xdrPublicKey(), + asset: asset.toTrustLineXDRObject() + }) + ) + ); + } + + const defaultPaymentFees = { + instructions: 400_000, + readBytes: 1_000, + writeBytes: 1_000, + resourceFee: 5_000_000 + }; + + const sorobanData = new xdr.SorobanTransactionData({ + resources: new xdr.SorobanResources({ + footprint, + instructions: sorobanFees + ? sorobanFees.instructions + : defaultPaymentFees.instructions, + diskReadBytes: sorobanFees + ? sorobanFees.readBytes + : defaultPaymentFees.readBytes, + writeBytes: sorobanFees + ? sorobanFees.writeBytes + : defaultPaymentFees.writeBytes + }), + ext: new xdr.SorobanTransactionDataExt(0), + resourceFee: new xdr.Int64( + sorobanFees ? sorobanFees.resourceFee : defaultPaymentFees.resourceFee + ) + }); + const operation = Operation.invokeContractFunction({ + contract: contractId, + function: functionName, + args, + auth: [auths] + }); + this.setSorobanData(sorobanData); + return this.addOperation(operation); + } + /** * This will build the transaction. * It will also increment the source account's sequence number by 1. diff --git a/test/unit/transaction_builder_test.js b/test/unit/transaction_builder_test.js index f6a47ee79..1affcaa20 100644 --- a/test/unit/transaction_builder_test.js +++ b/test/unit/transaction_builder_test.js @@ -226,6 +226,332 @@ describe('TransactionBuilder', function () { }); }); + describe('addSacTransferOperation footprint', function () { + const { xdr } = StellarBase; + const networkPassphrase = StellarBase.Networks.TESTNET; + + const SOURCE_ACCOUNT = + 'GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ'; + const DESTINATION_ACCOUNT = + 'GDJJRRMBK4IWLEPJGIE6SXD2LP7REGZODU7WDC3I2D6MR37F4XSHBKX2'; + const DESTINATION_CONTRACT = + 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE'; + const ISSUER_ACCOUNT = + 'GC6ACGSA2NJGD6YWUNX2BYBL3VM4MZRSEU2RLIUZZL35NLV5IAHAX2E2'; + + let source; + + beforeEach(function () { + source = new StellarBase.Account(SOURCE_ACCOUNT, '0'); + }); + + function buildSacTx(destination, asset) { + return new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation(destination, asset, '10') + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + } + + function contractAddressFromId(contractId) { + return StellarBase.Address.fromString(contractId).toScAddress(); + } + + function ledgerKeyContractInstance(contractId) { + return new xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: contractAddressFromId(contractId), + key: xdr.ScVal.scvLedgerKeyContractInstance(), + durability: xdr.ContractDataDurability.persistent() + }) + ); + } + + function ledgerKeyAccount(accountId) { + return new xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: StellarBase.Keypair.fromPublicKey(accountId).xdrPublicKey() + }) + ); + } + + function ledgerKeyTrustline(accountId, asset) { + return new xdr.LedgerKey.trustline( + new xdr.LedgerKeyTrustLine({ + accountId: + StellarBase.Keypair.fromPublicKey(accountId).xdrPublicKey(), + asset: asset.toTrustLineXDRObject() + }) + ); + } + + it('creates a source-account-credentialed authorization for the transfer', function () { + const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); + const tx = buildSacTx(DESTINATION_ACCOUNT, asset); + + expect(tx.operations).to.have.length(1); + + // auth is on the decoded operation + const auths = tx.operations[0].auth; + expect(auths).to.have.length(1); + + const auth = auths[0]; + + // credentials: must be source-account (no explicit signature required) + expect(auth.credentials().switch()).to.eql( + xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount() + ); + + const rootInvoc = auth.rootInvocation(); + + // function type: contract function + expect(rootInvoc.function().switch()).to.eql( + xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn() + ); + + // contract address matches the asset's SAC contract + const contractId = asset.contractId(networkPassphrase); + const contractFn = rootInvoc.function().contractFn(); + expect(contractFn.contractAddress().toXDR('base64')).to.equal( + StellarBase.Address.fromString(contractId).toScAddress().toXDR('base64') + ); + + // function name is 'transfer' + expect(Buffer.from(contractFn.functionName()).toString('utf8')).to.equal( + 'transfer' + ); + + // args: [source address, destination address, amount as i128] + const args = contractFn.args(); + expect(args).to.have.length(3); + expect(args[0].toXDR('base64')).to.equal( + StellarBase.nativeToScVal(SOURCE_ACCOUNT, { type: 'address' }).toXDR( + 'base64' + ) + ); + expect(args[1].toXDR('base64')).to.equal( + StellarBase.nativeToScVal(DESTINATION_ACCOUNT, { + type: 'address' + }).toXDR('base64') + ); + expect(args[2].toXDR('base64')).to.equal( + StellarBase.nativeToScVal('10', { type: 'i128' }).toXDR('base64') + ); + + // no sub-invocations + expect(rootInvoc.subInvocations()).to.have.length(0); + }); + + it('native asset + destination contract: writes balance contractData and source account', function () { + const asset = StellarBase.Asset.native(); + const tx = buildSacTx(DESTINATION_CONTRACT, asset); + + expect(tx.toEnvelope().v1().tx().ext().switch()).to.equal(1); + + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); + + const contractId = asset.contractId(networkPassphrase); + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ + new xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: contractAddressFromId(contractId), + key: xdr.ScVal.scvVec([ + StellarBase.nativeToScVal('Balance', { type: 'symbol' }), + StellarBase.nativeToScVal(DESTINATION_CONTRACT, { + type: 'address' + }) + ]), + durability: xdr.ContractDataDurability.persistent() + }) + ), + ledgerKeyAccount(SOURCE_ACCOUNT) + ]; + + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); + + it('native asset + destination account: writes destination and source accounts', function () { + const asset = StellarBase.Asset.native(); + const tx = buildSacTx(DESTINATION_ACCOUNT, asset); + + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); + + const contractId = asset.contractId(networkPassphrase); + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ + ledgerKeyAccount(DESTINATION_ACCOUNT), + ledgerKeyAccount(SOURCE_ACCOUNT) + ]; + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); + + it('credit asset + destination account: writes destination and source trustlines', function () { + const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); + const tx = buildSacTx(DESTINATION_ACCOUNT, asset); + + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); + + const contractId = asset.contractId(networkPassphrase); + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ + ledgerKeyTrustline(DESTINATION_ACCOUNT, asset), + ledgerKeyTrustline(SOURCE_ACCOUNT, asset) + ]; + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); + + it('credit asset + destination contract: reads issuer account and writes source trustline', function () { + const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); + const tx = buildSacTx(DESTINATION_CONTRACT, asset); + + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); + + const contractId = asset.contractId(networkPassphrase); + const expectedReadOnly = [ + ledgerKeyContractInstance(contractId), + ledgerKeyAccount(ISSUER_ACCOUNT) + ]; + const expectedReadWrite = [ + new xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: contractAddressFromId(contractId), + key: xdr.ScVal.scvVec([ + StellarBase.nativeToScVal('Balance', { type: 'symbol' }), + StellarBase.nativeToScVal(DESTINATION_CONTRACT, { + type: 'address' + }) + ]), + durability: xdr.ContractDataDurability.persistent() + }) + ), + ledgerKeyTrustline(SOURCE_ACCOUNT, asset) + ]; + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); + + it('credit asset + destination is issuer: omits destination trustline', function () { + const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); + const tx = buildSacTx(ISSUER_ACCOUNT, asset); + + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); + + const contractId = asset.contractId(networkPassphrase); + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ledgerKeyTrustline(SOURCE_ACCOUNT, asset)]; + + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); + + it('credit asset + source is issuer: omits source trustline', function () { + source = new StellarBase.Account(ISSUER_ACCOUNT, '0'); + const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); + const tx = buildSacTx(DESTINATION_ACCOUNT, asset); + + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); + + const contractId = asset.contractId(networkPassphrase); + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ + ledgerKeyTrustline(DESTINATION_ACCOUNT, asset) + ]; + + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); + + it('credit asset + source and destination are issuer: omits both trustlines', function () { + source = new StellarBase.Account(ISSUER_ACCOUNT, '0'); + const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); + const tx = buildSacTx(ISSUER_ACCOUNT, asset); + + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); + + const contractId = asset.contractId(networkPassphrase); + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = []; + + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite + ); + }); + + it('rejects amount greater than i64 max', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation( + DESTINATION_ACCOUNT, + asset, + '9223372036854775808' + ) + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + }).to.throw(/Amount exceeds maximum value for i64/); + }); + + it('accepts amount equal to i64 max', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation( + DESTINATION_ACCOUNT, + asset, + '9223372036854775807' + ) + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + }).to.not.throw(); + }); + }); + describe('constructs a native payment transaction with two operations', function () { var source; var destination1; diff --git a/types/index.d.ts b/types/index.d.ts index 70ad904ef..f8589bf34 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1062,6 +1062,13 @@ export class Transaction< export const BASE_FEE = '100'; export const TimeoutInfinite = 0; +export interface SorobanFees { + instructions: number; + readBytes: number; + writeBytes: number; + resourceFee: BigInt; +} + export class TransactionBuilder { constructor( sourceAccount: Account, @@ -1080,6 +1087,19 @@ export class TransactionBuilder { setMinAccountSequenceLedgerGap(gap: number): this; setExtraSigners(extraSigners: string[]): this; setSorobanData(sorobanData: string | xdr.SorobanTransactionData): this; + /** + * Creates and adds an invoke host function operation for transferring SAC tokens. + * This method removes the need for simulation by handling the creation of the + * appropriate authorization entries and ledger footprint for the transfer operation. + * + * @param destination - the address of the recipient of the SAC transfer (should be a valid Stellar address or contract ID) + * @param asset - the SAC asset to be transferred + * @param amount - the amount of tokens to be transferred in stroops IE. 1 token with 7 decimals of precision would be represented as "1_0000000" + * @param sorobanFees - optional Soroban fees for the transaction + * + * @returns the TransactionBuilder instance with the SAC transfer operation added + */ + addSacTransferOperation(destination: string, asset: Asset, amount: string | bigint, sorobanFees?: SorobanFees): this; build(): Transaction; setNetworkPassphrase(networkPassphrase: string): this; From 3f1fb068e2dd759f1f640e09e7c1dd17fe2289be Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Thu, 19 Feb 2026 13:02:27 -0800 Subject: [PATCH 2/9] Update transaction builder to include resource fee in total fee calculation --- CHANGELOG.md | 2 ++ src/transaction_builder.js | 16 +++++++++------- test/unit/transaction_builder_test.js | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 250097a10..bb5544fa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added * Implemented `TransactionBuilder.addSacTransferOperation` to remove the need for simulation for SAC (Stellar Asset Contracts) transfers by creating the appropriate auth entries and footprint ([#861](https://github.com/stellar/js-stellar-base/pull/861)). +### Fixed +* `TransactionBuilder.build` now adds `this.sorobanData.resourceFee()` to `baseFee` when provided ([#861](https://github.com/stellar/js-stellar-base/pull/861)). ## [`v14.0.4`](https://github.com/stellar/js-stellar-base/compare/v14.0.3...v14.0.4): diff --git a/src/transaction_builder.js b/src/transaction_builder.js index aa328ac09..792138af7 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -727,7 +727,7 @@ export class TransactionBuilder { writeBytes: 1_000, resourceFee: 5_000_000 }; - + const sorobanData = new xdr.SorobanTransactionData({ resources: new xdr.SorobanResources({ footprint, @@ -842,6 +842,10 @@ export class TransactionBuilder { if (this.sorobanData) { // @ts-ignore attrs.ext = new xdr.TransactionExt(1, this.sorobanData); + // Soroban transactions pay the resource fee in addition to the regular fee, so we need to add it here. + attrs.fee = new BigNumber(attrs.fee) + .plus(this.sorobanData.resourceFee()) + .toNumber(); } else { // @ts-ignore attrs.ext = new xdr.TransactionExt(0, xdr.Void); @@ -903,7 +907,6 @@ export class TransactionBuilder { const innerOps = innerTx.operations.length; const minBaseFee = new BigNumber(BASE_FEE); - let innerInclusionFee = new BigNumber(innerTx.fee).div(innerOps); let resourceFee = new BigNumber(0); // Do we need to do special Soroban fee handling? We only want the fee-bump @@ -913,16 +916,15 @@ export class TransactionBuilder { case xdr.EnvelopeType.envelopeTypeTx().value: { const sorobanData = env.v1().tx().ext().value(); resourceFee = new BigNumber(sorobanData?.resourceFee() ?? 0); - innerInclusionFee = BigNumber.max( - minBaseFee, - innerInclusionFee.minus(resourceFee) - ); + break; } default: break; } - + const innerInclusionFee = new BigNumber(innerTx.fee) + .minus(resourceFee) + .div(innerOps); const base = new BigNumber(baseFee); // The fee rate for fee bump is at least the fee rate of the inner transaction diff --git a/test/unit/transaction_builder_test.js b/test/unit/transaction_builder_test.js index 1affcaa20..a6e419a73 100644 --- a/test/unit/transaction_builder_test.js +++ b/test/unit/transaction_builder_test.js @@ -159,7 +159,7 @@ describe('TransactionBuilder', function () { .build(); let transaction = new StellarBase.TransactionBuilder(source, { - fee: 620 /* assume BASE_FEE*2 + 420 resource fee */, + fee: 200 /* assume BASE_FEE*2 */, networkPassphrase: StellarBase.Networks.TESTNET }) .addOperation( @@ -176,7 +176,7 @@ describe('TransactionBuilder', function () { ) .setSorobanData(sorobanTransactionData) .setTimeout(StellarBase.TimeoutInfinite) - .build(); + .build(); // Building includes resource fee in the total fee expect( transaction.toEnvelope().v1().tx().ext().sorobanData().toXDR('base64') From bf8d266934d6abd4ac7cf053604e748b8ca29840 Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Thu, 19 Feb 2026 13:22:01 -0800 Subject: [PATCH 3/9] ignore lint warning --- src/transaction_builder.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 792138af7..6c3984618 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -14,6 +14,7 @@ import { SorobanDataBuilder } from './sorobandata_builder'; import { StrKey } from './strkey'; import { SignerKey } from './signerkey'; import { Memo } from './memo'; +// eslint-disable-next-line no-unused-vars import { Asset } from './asset'; import { nativeToScVal } from './scval'; import { Operation } from './operation'; From df3228b7de119789217d15d1ace221ea4d12b7af Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Thu, 19 Feb 2026 13:25:34 -0800 Subject: [PATCH 4/9] fmt white space --- test/unit/transaction_builder_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/transaction_builder_test.js b/test/unit/transaction_builder_test.js index a6e419a73..aa7c6da6b 100644 --- a/test/unit/transaction_builder_test.js +++ b/test/unit/transaction_builder_test.js @@ -176,7 +176,7 @@ describe('TransactionBuilder', function () { ) .setSorobanData(sorobanTransactionData) .setTimeout(StellarBase.TimeoutInfinite) - .build(); // Building includes resource fee in the total fee + .build(); // Building includes resource fee in the total fee expect( transaction.toEnvelope().v1().tx().ext().sorobanData().toXDR('base64') From 6e8ea3723ab8614cb0cef35263ab2f6ad17200cc Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Thu, 19 Feb 2026 13:55:23 -0800 Subject: [PATCH 5/9] fix copilot comments --- src/transaction_builder.js | 29 +++++++----- test/unit/transaction_builder_test.js | 66 +++++++++++++++++++++++++-- types/index.d.ts | 8 ++-- 3 files changed, 84 insertions(+), 19 deletions(-) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 6c3984618..8f37911e3 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -597,29 +597,20 @@ export class TransactionBuilder { * * @param {string} destination - the address of the recipient of the SAC transfer (should be a valid Stellar address or contract ID) * @param {Asset} asset - the SAC asset to be transferred - * @param {string | BigInt} amount - the amount of tokens to be transferred in stroops IE. 1 token with 7 decimals of precision would be represented as "1_0000000" + * @param {BigInt} amount - the amount of tokens to be transferred in 7 decimals. IE 1 token with 7 decimals of precision would be represented as "1_0000000" * @param {SorobanFees} [sorobanFees] - optional Soroban fees for the transaction * * @returns {TransactionBuilder} */ addSacTransferOperation(destination, asset, amount, sorobanFees) { if (BigInt(amount) <= 0n) { - throw new Error('Amount must be a positive integer in stroops'); + throw new Error('Amount must be a positive integer'); } else if (BigInt(amount) > Hyper.MAX_VALUE) { // The largest supported value for SAC is i64 however the contract interface uses i128 which is why we convert it to i128 throw new Error('Amount exceeds maximum value for i64'); } - const contractId = asset.contractId(this.networkPassphrase); - const functionName = 'transfer'; - const source = this.source.accountId(); - const args = [ - nativeToScVal(source, { type: 'address' }), - nativeToScVal(destination, { type: 'address' }), - nativeToScVal(amount, { type: 'i128' }) - ]; - const isDestinationContract = StrKey.isValidContract(destination); - const isAssetNative = asset.isNative(); + const isDestinationContract = StrKey.isValidContract(destination); if (!isDestinationContract) { if ( !StrKey.isValidEd25519PublicKey(destination) && @@ -631,6 +622,20 @@ export class TransactionBuilder { } } + if (destination === this.source.accountId()) { + throw new Error('Destination cannot be the same as the source account.'); + } + + const contractId = asset.contractId(this.networkPassphrase); + const functionName = 'transfer'; + const source = this.source.accountId(); + const args = [ + nativeToScVal(source, { type: 'address' }), + nativeToScVal(destination, { type: 'address' }), + nativeToScVal(amount, { type: 'i128' }) + ]; + const isAssetNative = asset.isNative(); + const auths = new xdr.SorobanAuthorizationEntry({ credentials: xdr.SorobanCredentials.sorobanCredentialsSourceAccount(), rootInvocation: new xdr.SorobanAuthorizedInvocation({ diff --git a/test/unit/transaction_builder_test.js b/test/unit/transaction_builder_test.js index aa7c6da6b..ae2b6e365 100644 --- a/test/unit/transaction_builder_test.js +++ b/test/unit/transaction_builder_test.js @@ -497,8 +497,29 @@ describe('TransactionBuilder', function () { ); }); - it('credit asset + source and destination are issuer: omits both trustlines', function () { + it('credit asset + source is issuer: omits source trustline', function () { source = new StellarBase.Account(ISSUER_ACCOUNT, '0'); + const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); + const tx = buildSacTx(DESTINATION_ACCOUNT, asset); + + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); + + const contractId = asset.contractId(networkPassphrase); + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ + ledgerKeyTrustline(DESTINATION_ACCOUNT, asset) + ]; + + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); + + it('credit asset + destination is issuer: omits destination trustline', function () { const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); const tx = buildSacTx(ISSUER_ACCOUNT, asset); @@ -507,13 +528,13 @@ describe('TransactionBuilder', function () { const contractId = asset.contractId(networkPassphrase); const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; - const expectedReadWrite = []; + const expectedReadWrite = [ledgerKeyTrustline(SOURCE_ACCOUNT, asset)]; expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( expectedReadOnly.map((k) => k.toXDR('base64')) ); expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( - expectedReadWrite + expectedReadWrite.map((k) => k.toXDR('base64')) ); }); @@ -550,6 +571,45 @@ describe('TransactionBuilder', function () { .build(); }).to.not.throw(); }); + + it('rejects amount of zero', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation(DESTINATION_ACCOUNT, asset, '0') + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + }).to.throw(/Amount must be a positive integer/); + }); + + it('rejects negative amount', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation(DESTINATION_ACCOUNT, asset, '-1') + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + }).to.throw(/Amount must be a positive integer/); + }); + + it('rejects destination equal to source account', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation(SOURCE_ACCOUNT, asset, '10') + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + }).to.throw(/Destination cannot be the same as the source account/); + }); }); describe('constructs a native payment transaction with two operations', function () { diff --git a/types/index.d.ts b/types/index.d.ts index f8589bf34..642e587aa 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1066,7 +1066,7 @@ export interface SorobanFees { instructions: number; readBytes: number; writeBytes: number; - resourceFee: BigInt; + resourceFee: bigint; } export class TransactionBuilder { @@ -1089,12 +1089,12 @@ export class TransactionBuilder { setSorobanData(sorobanData: string | xdr.SorobanTransactionData): this; /** * Creates and adds an invoke host function operation for transferring SAC tokens. - * This method removes the need for simulation by handling the creation of the + * This method removes the need for simulation by handling the creation of the * appropriate authorization entries and ledger footprint for the transfer operation. - * + * * @param destination - the address of the recipient of the SAC transfer (should be a valid Stellar address or contract ID) * @param asset - the SAC asset to be transferred - * @param amount - the amount of tokens to be transferred in stroops IE. 1 token with 7 decimals of precision would be represented as "1_0000000" + * @param amount - the amount of tokens to be transferred in 7 decimals. IE 1 token with 7 decimals of precision would be represented as "1_0000000" * @param sorobanFees - optional Soroban fees for the transaction * * @returns the TransactionBuilder instance with the SAC transfer operation added From ec12b5866b1e08c7093a16e3e8471e71d075450d Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Fri, 20 Feb 2026 10:25:13 -0800 Subject: [PATCH 6/9] reorganize sac-transfer tests into grouped describe blocks --- test/unit/transaction_builder_test.js | 545 ++++++++++++-------------- 1 file changed, 259 insertions(+), 286 deletions(-) diff --git a/test/unit/transaction_builder_test.js b/test/unit/transaction_builder_test.js index ae2b6e365..139797c36 100644 --- a/test/unit/transaction_builder_test.js +++ b/test/unit/transaction_builder_test.js @@ -226,7 +226,7 @@ describe('TransactionBuilder', function () { }); }); - describe('addSacTransferOperation footprint', function () { + describe('addSacTransferOperation', function () { const { xdr } = StellarBase; const networkPassphrase = StellarBase.Networks.TESTNET; @@ -287,328 +287,301 @@ describe('TransactionBuilder', function () { ); } - it('creates a source-account-credentialed authorization for the transfer', function () { - const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); - const tx = buildSacTx(DESTINATION_ACCOUNT, asset); + describe('authorization', function () { + it('creates a source-account-credentialed authorization for the transfer', function () { + const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); + const tx = buildSacTx(DESTINATION_ACCOUNT, asset); - expect(tx.operations).to.have.length(1); + expect(tx.operations).to.have.length(1); - // auth is on the decoded operation - const auths = tx.operations[0].auth; - expect(auths).to.have.length(1); + // auth is on the decoded operation + const auths = tx.operations[0].auth; + expect(auths).to.have.length(1); - const auth = auths[0]; + const auth = auths[0]; - // credentials: must be source-account (no explicit signature required) - expect(auth.credentials().switch()).to.eql( - xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount() - ); - - const rootInvoc = auth.rootInvocation(); - - // function type: contract function - expect(rootInvoc.function().switch()).to.eql( - xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn() - ); - - // contract address matches the asset's SAC contract - const contractId = asset.contractId(networkPassphrase); - const contractFn = rootInvoc.function().contractFn(); - expect(contractFn.contractAddress().toXDR('base64')).to.equal( - StellarBase.Address.fromString(contractId).toScAddress().toXDR('base64') - ); - - // function name is 'transfer' - expect(Buffer.from(contractFn.functionName()).toString('utf8')).to.equal( - 'transfer' - ); - - // args: [source address, destination address, amount as i128] - const args = contractFn.args(); - expect(args).to.have.length(3); - expect(args[0].toXDR('base64')).to.equal( - StellarBase.nativeToScVal(SOURCE_ACCOUNT, { type: 'address' }).toXDR( - 'base64' - ) - ); - expect(args[1].toXDR('base64')).to.equal( - StellarBase.nativeToScVal(DESTINATION_ACCOUNT, { - type: 'address' - }).toXDR('base64') - ); - expect(args[2].toXDR('base64')).to.equal( - StellarBase.nativeToScVal('10', { type: 'i128' }).toXDR('base64') - ); - - // no sub-invocations - expect(rootInvoc.subInvocations()).to.have.length(0); - }); + // credentials: must be source-account (no explicit signature required) + expect(auth.credentials().switch()).to.eql( + xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount() + ); - it('native asset + destination contract: writes balance contractData and source account', function () { - const asset = StellarBase.Asset.native(); - const tx = buildSacTx(DESTINATION_CONTRACT, asset); - - expect(tx.toEnvelope().v1().tx().ext().switch()).to.equal(1); - - const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); - const footprint = sorobanData.resources().footprint(); - - const contractId = asset.contractId(networkPassphrase); - const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; - const expectedReadWrite = [ - new xdr.LedgerKey.contractData( - new xdr.LedgerKeyContractData({ - contract: contractAddressFromId(contractId), - key: xdr.ScVal.scvVec([ - StellarBase.nativeToScVal('Balance', { type: 'symbol' }), - StellarBase.nativeToScVal(DESTINATION_CONTRACT, { - type: 'address' - }) - ]), - durability: xdr.ContractDataDurability.persistent() - }) - ), - ledgerKeyAccount(SOURCE_ACCOUNT) - ]; + const rootInvoc = auth.rootInvocation(); - expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( - expectedReadOnly.map((k) => k.toXDR('base64')) - ); - expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( - expectedReadWrite.map((k) => k.toXDR('base64')) - ); - }); + // function type: contract function + expect(rootInvoc.function().switch()).to.eql( + xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn() + ); - it('native asset + destination account: writes destination and source accounts', function () { - const asset = StellarBase.Asset.native(); - const tx = buildSacTx(DESTINATION_ACCOUNT, asset); + // contract address matches the asset's SAC contract + const contractId = asset.contractId(networkPassphrase); + const contractFn = rootInvoc.function().contractFn(); + expect(contractFn.contractAddress().toXDR('base64')).to.equal( + StellarBase.Address.fromString(contractId) + .toScAddress() + .toXDR('base64') + ); - const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); - const footprint = sorobanData.resources().footprint(); + // function name is 'transfer' + expect( + Buffer.from(contractFn.functionName()).toString('utf8') + ).to.equal('transfer'); + + // args: [source address, destination address, amount as i128] + const args = contractFn.args(); + expect(args).to.have.length(3); + expect(args[0].toXDR('base64')).to.equal( + StellarBase.nativeToScVal(SOURCE_ACCOUNT, { type: 'address' }).toXDR( + 'base64' + ) + ); + expect(args[1].toXDR('base64')).to.equal( + StellarBase.nativeToScVal(DESTINATION_ACCOUNT, { + type: 'address' + }).toXDR('base64') + ); + expect(args[2].toXDR('base64')).to.equal( + StellarBase.nativeToScVal('10', { type: 'i128' }).toXDR('base64') + ); - const contractId = asset.contractId(networkPassphrase); - const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; - const expectedReadWrite = [ - ledgerKeyAccount(DESTINATION_ACCOUNT), - ledgerKeyAccount(SOURCE_ACCOUNT) - ]; - expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( - expectedReadOnly.map((k) => k.toXDR('base64')) - ); - expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( - expectedReadWrite.map((k) => k.toXDR('base64')) - ); + // no sub-invocations + expect(rootInvoc.subInvocations()).to.have.length(0); + }); }); - it('credit asset + destination account: writes destination and source trustlines', function () { - const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); - const tx = buildSacTx(DESTINATION_ACCOUNT, asset); + describe('native asset footprint', function () { + let asset; + let contractId; - const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); - const footprint = sorobanData.resources().footprint(); + beforeEach(function () { + asset = StellarBase.Asset.native(); + contractId = asset.contractId(networkPassphrase); + }); - const contractId = asset.contractId(networkPassphrase); - const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; - const expectedReadWrite = [ - ledgerKeyTrustline(DESTINATION_ACCOUNT, asset), - ledgerKeyTrustline(SOURCE_ACCOUNT, asset) - ]; - expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( - expectedReadOnly.map((k) => k.toXDR('base64')) - ); - expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( - expectedReadWrite.map((k) => k.toXDR('base64')) - ); - }); + it('destination contract: writes balance contractData and source account', function () { + const tx = buildSacTx(DESTINATION_CONTRACT, asset); + + expect(tx.toEnvelope().v1().tx().ext().switch()).to.equal(1); + + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); + + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ + new xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: contractAddressFromId(contractId), + key: xdr.ScVal.scvVec([ + StellarBase.nativeToScVal('Balance', { type: 'symbol' }), + StellarBase.nativeToScVal(DESTINATION_CONTRACT, { + type: 'address' + }) + ]), + durability: xdr.ContractDataDurability.persistent() + }) + ), + ledgerKeyAccount(SOURCE_ACCOUNT) + ]; + + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); - it('credit asset + destination contract: reads issuer account and writes source trustline', function () { - const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); - const tx = buildSacTx(DESTINATION_CONTRACT, asset); + it('destination account: writes destination and source accounts', function () { + const tx = buildSacTx(DESTINATION_ACCOUNT, asset); - const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); - const footprint = sorobanData.resources().footprint(); + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); - const contractId = asset.contractId(networkPassphrase); - const expectedReadOnly = [ - ledgerKeyContractInstance(contractId), - ledgerKeyAccount(ISSUER_ACCOUNT) - ]; - const expectedReadWrite = [ - new xdr.LedgerKey.contractData( - new xdr.LedgerKeyContractData({ - contract: contractAddressFromId(contractId), - key: xdr.ScVal.scvVec([ - StellarBase.nativeToScVal('Balance', { type: 'symbol' }), - StellarBase.nativeToScVal(DESTINATION_CONTRACT, { - type: 'address' - }) - ]), - durability: xdr.ContractDataDurability.persistent() - }) - ), - ledgerKeyTrustline(SOURCE_ACCOUNT, asset) - ]; - expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( - expectedReadOnly.map((k) => k.toXDR('base64')) - ); - expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( - expectedReadWrite.map((k) => k.toXDR('base64')) - ); + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ + ledgerKeyAccount(DESTINATION_ACCOUNT), + ledgerKeyAccount(SOURCE_ACCOUNT) + ]; + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); }); - it('credit asset + destination is issuer: omits destination trustline', function () { - const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); - const tx = buildSacTx(ISSUER_ACCOUNT, asset); - - const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); - const footprint = sorobanData.resources().footprint(); + describe('credit asset footprint', function () { + let asset; + let contractId; - const contractId = asset.contractId(networkPassphrase); - const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; - const expectedReadWrite = [ledgerKeyTrustline(SOURCE_ACCOUNT, asset)]; - - expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( - expectedReadOnly.map((k) => k.toXDR('base64')) - ); - expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( - expectedReadWrite.map((k) => k.toXDR('base64')) - ); - }); + beforeEach(function () { + asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); + contractId = asset.contractId(networkPassphrase); + }); - it('credit asset + source is issuer: omits source trustline', function () { - source = new StellarBase.Account(ISSUER_ACCOUNT, '0'); - const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); - const tx = buildSacTx(DESTINATION_ACCOUNT, asset); + it('destination account: writes destination and source trustlines', function () { + const tx = buildSacTx(DESTINATION_ACCOUNT, asset); - const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); - const footprint = sorobanData.resources().footprint(); + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); - const contractId = asset.contractId(networkPassphrase); - const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; - const expectedReadWrite = [ - ledgerKeyTrustline(DESTINATION_ACCOUNT, asset) - ]; + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ + ledgerKeyTrustline(DESTINATION_ACCOUNT, asset), + ledgerKeyTrustline(SOURCE_ACCOUNT, asset) + ]; + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); - expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( - expectedReadOnly.map((k) => k.toXDR('base64')) - ); - expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( - expectedReadWrite.map((k) => k.toXDR('base64')) - ); - }); + it('destination contract: reads issuer account and writes source trustline', function () { + const tx = buildSacTx(DESTINATION_CONTRACT, asset); + + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); + + const expectedReadOnly = [ + ledgerKeyContractInstance(contractId), + ledgerKeyAccount(ISSUER_ACCOUNT) + ]; + const expectedReadWrite = [ + new xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: contractAddressFromId(contractId), + key: xdr.ScVal.scvVec([ + StellarBase.nativeToScVal('Balance', { type: 'symbol' }), + StellarBase.nativeToScVal(DESTINATION_CONTRACT, { + type: 'address' + }) + ]), + durability: xdr.ContractDataDurability.persistent() + }) + ), + ledgerKeyTrustline(SOURCE_ACCOUNT, asset) + ]; + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); - it('credit asset + source is issuer: omits source trustline', function () { - source = new StellarBase.Account(ISSUER_ACCOUNT, '0'); - const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); - const tx = buildSacTx(DESTINATION_ACCOUNT, asset); + it('destination is issuer: omits destination trustline', function () { + const tx = buildSacTx(ISSUER_ACCOUNT, asset); - const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); - const footprint = sorobanData.resources().footprint(); + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); - const contractId = asset.contractId(networkPassphrase); - const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; - const expectedReadWrite = [ - ledgerKeyTrustline(DESTINATION_ACCOUNT, asset) - ]; + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ledgerKeyTrustline(SOURCE_ACCOUNT, asset)]; - expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( - expectedReadOnly.map((k) => k.toXDR('base64')) - ); - expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( - expectedReadWrite.map((k) => k.toXDR('base64')) - ); - }); + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); - it('credit asset + destination is issuer: omits destination trustline', function () { - const asset = new StellarBase.Asset('TEST', ISSUER_ACCOUNT); - const tx = buildSacTx(ISSUER_ACCOUNT, asset); + it('source is issuer: omits source trustline', function () { + source = new StellarBase.Account(ISSUER_ACCOUNT, '0'); + const tx = buildSacTx(DESTINATION_ACCOUNT, asset); - const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); - const footprint = sorobanData.resources().footprint(); + const sorobanData = tx.toEnvelope().v1().tx().ext().sorobanData(); + const footprint = sorobanData.resources().footprint(); - const contractId = asset.contractId(networkPassphrase); - const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; - const expectedReadWrite = [ledgerKeyTrustline(SOURCE_ACCOUNT, asset)]; + const expectedReadOnly = [ledgerKeyContractInstance(contractId)]; + const expectedReadWrite = [ + ledgerKeyTrustline(DESTINATION_ACCOUNT, asset) + ]; - expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( - expectedReadOnly.map((k) => k.toXDR('base64')) - ); - expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( - expectedReadWrite.map((k) => k.toXDR('base64')) - ); + expect(footprint.readOnly().map((k) => k.toXDR('base64'))).to.eql( + expectedReadOnly.map((k) => k.toXDR('base64')) + ); + expect(footprint.readWrite().map((k) => k.toXDR('base64'))).to.eql( + expectedReadWrite.map((k) => k.toXDR('base64')) + ); + }); }); - it('rejects amount greater than i64 max', function () { - const asset = StellarBase.Asset.native(); - expect(() => { - new StellarBase.TransactionBuilder(source, { - fee: 100, - networkPassphrase - }) - .addSacTransferOperation( - DESTINATION_ACCOUNT, - asset, - '9223372036854775808' - ) - .setTimeout(StellarBase.TimeoutInfinite) - .build(); - }).to.throw(/Amount exceeds maximum value for i64/); - }); + describe('input validation', function () { + it('rejects amount greater than i64 max', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation( + DESTINATION_ACCOUNT, + asset, + '9223372036854775808' + ) + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + }).to.throw(/Amount exceeds maximum value for i64/); + }); - it('accepts amount equal to i64 max', function () { - const asset = StellarBase.Asset.native(); - expect(() => { - new StellarBase.TransactionBuilder(source, { - fee: 100, - networkPassphrase - }) - .addSacTransferOperation( - DESTINATION_ACCOUNT, - asset, - '9223372036854775807' - ) - .setTimeout(StellarBase.TimeoutInfinite) - .build(); - }).to.not.throw(); - }); + it('accepts amount equal to i64 max', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation( + DESTINATION_ACCOUNT, + asset, + '9223372036854775807' + ) + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + }).to.not.throw(); + }); - it('rejects amount of zero', function () { - const asset = StellarBase.Asset.native(); - expect(() => { - new StellarBase.TransactionBuilder(source, { - fee: 100, - networkPassphrase - }) - .addSacTransferOperation(DESTINATION_ACCOUNT, asset, '0') - .setTimeout(StellarBase.TimeoutInfinite) - .build(); - }).to.throw(/Amount must be a positive integer/); - }); + it('rejects amount of zero', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation(DESTINATION_ACCOUNT, asset, '0') + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + }).to.throw(/Amount must be a positive integer/); + }); - it('rejects negative amount', function () { - const asset = StellarBase.Asset.native(); - expect(() => { - new StellarBase.TransactionBuilder(source, { - fee: 100, - networkPassphrase - }) - .addSacTransferOperation(DESTINATION_ACCOUNT, asset, '-1') - .setTimeout(StellarBase.TimeoutInfinite) - .build(); - }).to.throw(/Amount must be a positive integer/); - }); + it('rejects negative amount', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation(DESTINATION_ACCOUNT, asset, '-1') + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + }).to.throw(/Amount must be a positive integer/); + }); - it('rejects destination equal to source account', function () { - const asset = StellarBase.Asset.native(); - expect(() => { - new StellarBase.TransactionBuilder(source, { - fee: 100, - networkPassphrase - }) - .addSacTransferOperation(SOURCE_ACCOUNT, asset, '10') - .setTimeout(StellarBase.TimeoutInfinite) - .build(); - }).to.throw(/Destination cannot be the same as the source account/); + it('rejects destination equal to source account', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }) + .addSacTransferOperation(SOURCE_ACCOUNT, asset, '10') + .setTimeout(StellarBase.TimeoutInfinite) + .build(); + }).to.throw(/Destination cannot be the same as the source account/); + }); }); }); From c4fe566a6eb93ea8df486cfaf1412f8a11c5c9ae Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Fri, 20 Feb 2026 10:54:05 -0800 Subject: [PATCH 7/9] add input validation for sorobanFee param --- src/transaction_builder.js | 32 +++++- test/unit/transaction_builder_test.js | 144 ++++++++++++++++++++++---- types/index.d.ts | 10 +- 3 files changed, 161 insertions(+), 25 deletions(-) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 8f37911e3..2c90aec33 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -45,7 +45,7 @@ export const TimeoutInfinite = 0; * @property {number} instructions - the number of instructions executed by the transaction * @property {number} readBytes - the number of bytes read from the ledger by the transaction * @property {number} writeBytes - the number of bytes written to the ledger by the transaction - * @property {number} resourceFee - the fee to be paid for the transaction, in stroops (int64) + * @property {bigint} resourceFee - the fee to be paid for the transaction, in stroops */ /** @@ -598,7 +598,7 @@ export class TransactionBuilder { * @param {string} destination - the address of the recipient of the SAC transfer (should be a valid Stellar address or contract ID) * @param {Asset} asset - the SAC asset to be transferred * @param {BigInt} amount - the amount of tokens to be transferred in 7 decimals. IE 1 token with 7 decimals of precision would be represented as "1_0000000" - * @param {SorobanFees} [sorobanFees] - optional Soroban fees for the transaction + * @param {SorobanFees} [sorobanFees] - optional Soroban fees for the transaction to override the default fees used * * @returns {TransactionBuilder} */ @@ -610,6 +610,32 @@ export class TransactionBuilder { throw new Error('Amount exceeds maximum value for i64'); } + if (sorobanFees) { + const { instructions, readBytes, writeBytes, resourceFee } = sorobanFees; + const U32_MAX = 4294967295; + + if (instructions <= 0 || instructions > U32_MAX) { + throw new Error( + `instructions must be greater than 0 and at most ${U32_MAX}` + ); + } + if (readBytes <= 0 || readBytes > U32_MAX) { + throw new Error( + `readBytes must be greater than 0 and at most ${U32_MAX}` + ); + } + if (writeBytes <= 0 || writeBytes > U32_MAX) { + throw new Error( + `writeBytes must be greater than 0 and at most ${U32_MAX}` + ); + } + if (resourceFee <= 0n || resourceFee > Hyper.MAX_VALUE) { + throw new Error( + 'resourceFee must be greater than 0 and at most i64 max' + ); + } + } + const isDestinationContract = StrKey.isValidContract(destination); if (!isDestinationContract) { if ( @@ -731,7 +757,7 @@ export class TransactionBuilder { instructions: 400_000, readBytes: 1_000, writeBytes: 1_000, - resourceFee: 5_000_000 + resourceFee: BigInt(5_000_000) }; const sorobanData = new xdr.SorobanTransactionData({ diff --git a/test/unit/transaction_builder_test.js b/test/unit/transaction_builder_test.js index 139797c36..e3e3bfca8 100644 --- a/test/unit/transaction_builder_test.js +++ b/test/unit/transaction_builder_test.js @@ -516,14 +516,11 @@ describe('TransactionBuilder', function () { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase - }) - .addSacTransferOperation( - DESTINATION_ACCOUNT, - asset, - '9223372036854775808' - ) - .setTimeout(StellarBase.TimeoutInfinite) - .build(); + }).addSacTransferOperation( + DESTINATION_ACCOUNT, + asset, + '9223372036854775808' + ); }).to.throw(/Amount exceeds maximum value for i64/); }); @@ -533,14 +530,11 @@ describe('TransactionBuilder', function () { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase - }) - .addSacTransferOperation( - DESTINATION_ACCOUNT, - asset, - '9223372036854775807' - ) - .setTimeout(StellarBase.TimeoutInfinite) - .build(); + }).addSacTransferOperation( + DESTINATION_ACCOUNT, + asset, + '9223372036854775807' + ); }).to.not.throw(); }); @@ -559,28 +553,136 @@ describe('TransactionBuilder', function () { it('rejects negative amount', function () { const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '-1'); + }).to.throw(/Amount must be a positive integer/); + }); + + it('rejects destination equal to source account', function () { + const asset = StellarBase.Asset.native(); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }).addSacTransferOperation(SOURCE_ACCOUNT, asset, '10'); + }).to.throw(/Destination cannot be the same as the source account/); + }); + + it('validates sorobanFees are greater than zero', function () { + const asset = StellarBase.Asset.native(); + const U32_MAX = 4294967295; + const validFees = { instructions: 1, readBytes: 2, writeBytes: 3, resourceFee: BigInt(4) }; + + // each u32 field rejects 0 + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, instructions: 0 }); + }).to.throw(/instructions must be greater than 0/); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, readBytes: 0 }); + }).to.throw(/readBytes must be greater than 0/); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, writeBytes: 0 }); + }).to.throw(/writeBytes must be greater than 0/); + + // resourceFee rejects 0 + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, resourceFee: BigInt(0) }); + }).to.throw(/resourceFee must be greater than 0/); + }); + + it('rejects u32 fields exceeding u32 max', function () { + const asset = StellarBase.Asset.native(); + const U32_MAX = 4294967295; + const validFees = { instructions: 1, readBytes: 2, writeBytes: 3, resourceFee: BigInt(4) }; + + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, instructions: U32_MAX + 1 }); + }).to.throw(/instructions must be greater than 0/); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, readBytes: U32_MAX + 1 }); + }).to.throw(/readBytes must be greater than 0/); + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, writeBytes: U32_MAX + 1 }); + }).to.throw(/writeBytes must be greater than 0/); + }); + + it('accepts u32 fields at u32 max', function () { + const asset = StellarBase.Asset.native(); + const U32_MAX = 4294967295; + expect(() => { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase }) - .addSacTransferOperation(DESTINATION_ACCOUNT, asset, '-1') + .addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { + instructions: U32_MAX, + readBytes: U32_MAX, + writeBytes: U32_MAX, + resourceFee: BigInt(1) + }) .setTimeout(StellarBase.TimeoutInfinite) .build(); - }).to.throw(/Amount must be a positive integer/); + }).to.not.throw(); }); - it('rejects destination equal to source account', function () { + it('rejects resourceFee exceeding i64 max', function () { const asset = StellarBase.Asset.native(); + + expect(() => { + new StellarBase.TransactionBuilder(source, { + fee: 100, + networkPassphrase + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { + instructions: 1, + readBytes: 1, + writeBytes: 1, + resourceFee: BigInt('9223372036854775808') + }); + }).to.throw(/resourceFee must be greater than 0/); + }); + + it('accepts resourceFee at i64 max', function () { + const asset = StellarBase.Asset.native(); + expect(() => { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase }) - .addSacTransferOperation(SOURCE_ACCOUNT, asset, '10') + .addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { + instructions: 1, + readBytes: 1, + writeBytes: 1, + resourceFee: BigInt('9223372036854775807') + }) .setTimeout(StellarBase.TimeoutInfinite) .build(); - }).to.throw(/Destination cannot be the same as the source account/); + }).to.not.throw(); }); }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 642e587aa..f406d4c22 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1062,6 +1062,14 @@ export class Transaction< export const BASE_FEE = '100'; export const TimeoutInfinite = 0; +/** + * Represents the fees associated with a Soroban transaction, including the number of instructions executed, + * the number of bytes read and written to the ledger, and the total resource fee in stroops. + * @property {number} instructions - the number of instructions executed by the transaction + * @property {number} readBytes - the number of bytes read from the ledger by the transaction + * @property {number} writeBytes - the number of bytes written to the ledger by the transaction + * @property {bigint} resourceFee - the fee to be paid for the transaction, in stroops + */ export interface SorobanFees { instructions: number; readBytes: number; @@ -1095,7 +1103,7 @@ export class TransactionBuilder { * @param destination - the address of the recipient of the SAC transfer (should be a valid Stellar address or contract ID) * @param asset - the SAC asset to be transferred * @param amount - the amount of tokens to be transferred in 7 decimals. IE 1 token with 7 decimals of precision would be represented as "1_0000000" - * @param sorobanFees - optional Soroban fees for the transaction + * @param sorobanFees - optional Soroban fees for the transaction to override the default fees used * * @returns the TransactionBuilder instance with the SAC transfer operation added */ From f0dd43b1cb35844b05888247b7c498fac597880a Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Fri, 20 Feb 2026 11:13:53 -0800 Subject: [PATCH 8/9] fmt --- src/transaction_builder.js | 2 +- test/unit/transaction_builder_test.js | 49 ++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 2c90aec33..48fe454d9 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -45,7 +45,7 @@ export const TimeoutInfinite = 0; * @property {number} instructions - the number of instructions executed by the transaction * @property {number} readBytes - the number of bytes read from the ledger by the transaction * @property {number} writeBytes - the number of bytes written to the ledger by the transaction - * @property {bigint} resourceFee - the fee to be paid for the transaction, in stroops + * @property {bigint} resourceFee - the fee to be paid for the transaction, in stroops */ /** diff --git a/test/unit/transaction_builder_test.js b/test/unit/transaction_builder_test.js index e3e3bfca8..b497e025c 100644 --- a/test/unit/transaction_builder_test.js +++ b/test/unit/transaction_builder_test.js @@ -574,26 +574,40 @@ describe('TransactionBuilder', function () { it('validates sorobanFees are greater than zero', function () { const asset = StellarBase.Asset.native(); const U32_MAX = 4294967295; - const validFees = { instructions: 1, readBytes: 2, writeBytes: 3, resourceFee: BigInt(4) }; + const validFees = { + instructions: 1, + readBytes: 2, + writeBytes: 3, + resourceFee: BigInt(4) + }; // each u32 field rejects 0 expect(() => { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase - }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, instructions: 0 }); + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { + ...validFees, + instructions: 0 + }); }).to.throw(/instructions must be greater than 0/); expect(() => { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase - }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, readBytes: 0 }); + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { + ...validFees, + readBytes: 0 + }); }).to.throw(/readBytes must be greater than 0/); expect(() => { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase - }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, writeBytes: 0 }); + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { + ...validFees, + writeBytes: 0 + }); }).to.throw(/writeBytes must be greater than 0/); // resourceFee rejects 0 @@ -601,32 +615,49 @@ describe('TransactionBuilder', function () { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase - }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, resourceFee: BigInt(0) }); + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { + ...validFees, + resourceFee: BigInt(0) + }); }).to.throw(/resourceFee must be greater than 0/); }); it('rejects u32 fields exceeding u32 max', function () { const asset = StellarBase.Asset.native(); const U32_MAX = 4294967295; - const validFees = { instructions: 1, readBytes: 2, writeBytes: 3, resourceFee: BigInt(4) }; + const validFees = { + instructions: 1, + readBytes: 2, + writeBytes: 3, + resourceFee: BigInt(4) + }; expect(() => { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase - }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, instructions: U32_MAX + 1 }); + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { + ...validFees, + instructions: U32_MAX + 1 + }); }).to.throw(/instructions must be greater than 0/); expect(() => { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase - }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, readBytes: U32_MAX + 1 }); + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { + ...validFees, + readBytes: U32_MAX + 1 + }); }).to.throw(/readBytes must be greater than 0/); expect(() => { new StellarBase.TransactionBuilder(source, { fee: 100, networkPassphrase - }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { ...validFees, writeBytes: U32_MAX + 1 }); + }).addSacTransferOperation(DESTINATION_ACCOUNT, asset, '10', { + ...validFees, + writeBytes: U32_MAX + 1 + }); }).to.throw(/writeBytes must be greater than 0/); }); From f599529a01e27efefc004244aa7945141fc678f0 Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Fri, 20 Feb 2026 11:24:48 -0800 Subject: [PATCH 9/9] fix lint errors in type declaration file --- types/index.d.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index f406d4c22..1977b29af 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1065,13 +1065,9 @@ export const TimeoutInfinite = 0; /** * Represents the fees associated with a Soroban transaction, including the number of instructions executed, * the number of bytes read and written to the ledger, and the total resource fee in stroops. - * @property {number} instructions - the number of instructions executed by the transaction - * @property {number} readBytes - the number of bytes read from the ledger by the transaction - * @property {number} writeBytes - the number of bytes written to the ledger by the transaction - * @property {bigint} resourceFee - the fee to be paid for the transaction, in stroops */ export interface SorobanFees { - instructions: number; + instructions: number; readBytes: number; writeBytes: number; resourceFee: bigint;