UNPKG

@stellar/stellar-sdk

Version:

A library for working with the Stellar network, including communication with the Horizon and Soroban RPC servers.

883 lines (880 loc) 34.6 kB
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/int.js'; import { Hyper } from '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/hyper.js'; import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/unsigned-int.js'; import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/unsigned-hyper.js'; import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/xdr-type.js'; import 'buffer'; import BigNumber from './util/bignumber.js'; import types from './generated/curr_generated.js'; import { Account } from './account.js'; import { MuxedAccount } from './muxed_account.js'; import { extractBaseAddress, decodeAddressToMuxedAccount } from './util/decode_encode_muxed_account.js'; import { Transaction } from './transaction.js'; import { FeeBumpTransaction } from './fee_bump_transaction.js'; import { SorobanDataBuilder } from './sorobandata_builder.js'; import { StrKey } from './strkey.js'; import { SignerKey } from './signerkey.js'; import { Memo } from './memo.js'; import { nativeToScVal } from './scval.js'; import { Operation } from './operation.js'; import { Address } from './address.js'; import { Keypair } from './keypair.js'; const HYPER_MAX_VALUE = Hyper.MAX_VALUE; const UINT32_MAX = 4294967295; const BASE_FEE = "100"; const TimeoutInfinite = 0; class TransactionBuilder { source; operations; baseFee; timebounds; ledgerbounds; minAccountSequence; minAccountSequenceAge; minAccountSequenceLedgerGap; extraSigners; memo; networkPassphrase; sorobanData; /** * @param sourceAccount - source account for this transaction * @param opts - options object (see {@link TransactionBuilderOptions}) */ constructor(sourceAccount, opts = {}) { if (!sourceAccount) { throw new Error("must specify source account for the transaction"); } if (opts.fee === void 0) { throw new Error("must specify fee for the transaction (in stroops)"); } this.source = sourceAccount; this.operations = []; this.baseFee = opts.fee; if (opts.timebounds) { const minTime = toEpochSeconds(opts.timebounds.minTime); const maxTime = toEpochSeconds(opts.timebounds.maxTime); if (minTime !== void 0 && minTime < 0) { throw new Error("min_time cannot be negative"); } if (maxTime !== void 0 && maxTime < 0) { throw new Error("max_time cannot be negative"); } if (minTime !== void 0 && maxTime !== void 0 && maxTime > 0 && minTime > maxTime) { throw new Error("min_time cannot be greater than max_time"); } this.timebounds = { ...opts.timebounds }; } else { this.timebounds = null; } if (opts.ledgerbounds) { const minLedger = opts.ledgerbounds.minLedger; const maxLedger = opts.ledgerbounds.maxLedger; if (minLedger !== void 0 && minLedger < 0) { throw new Error("min_ledger cannot be negative"); } if (maxLedger !== void 0 && maxLedger < 0) { throw new Error("max_ledger cannot be negative"); } if (minLedger !== void 0 && maxLedger !== void 0 && maxLedger > 0 && minLedger > maxLedger) { throw new Error("min_ledger cannot be greater than max_ledger"); } this.ledgerbounds = { ...opts.ledgerbounds }; } else { this.ledgerbounds = null; } this.minAccountSequence = opts.minAccountSequence || null; this.minAccountSequenceAge = opts.minAccountSequenceAge !== void 0 ? opts.minAccountSequenceAge : null; this.minAccountSequenceLedgerGap = opts.minAccountSequenceLedgerGap !== void 0 ? opts.minAccountSequenceLedgerGap : null; this.extraSigners = opts.extraSigners ? [...opts.extraSigners] : null; this.memo = opts.memo || Memo.none(); this.networkPassphrase = opts.networkPassphrase || null; this.sorobanData = opts.sorobanData ? new SorobanDataBuilder(opts.sorobanData).build() : null; } /** * Creates a builder instance using an existing {@link Transaction} as a * template, ignoring any existing envelope signatures. * * Note that the sequence number WILL be cloned, so EITHER this transaction or * the one it was cloned from will be valid. This is useful in situations * where you are constructing a transaction in pieces and need to make * adjustments as you go (for example, when filling out Soroban resource * information). * * @param tx - a "template" transaction to clone exactly * @param opts - additional options to override the clone, e.g. * `{fee: '1000'}` will override the existing base fee derived from `tx` * (see the {@link TransactionBuilder} constructor for detailed options) * * **Warning:** This does not clone the transaction's * {@link xdr.SorobanTransactionData} (if applicable), use * {@link SorobanDataBuilder} and {@link TransactionBuilder.setSorobanData} * as needed, instead. * * TODO: This cannot clone {@link FeeBumpTransaction}s, yet. */ static cloneFrom(tx, opts = {}) { if (!(tx instanceof Transaction)) { throw new TypeError(`expected a 'Transaction', got: ${String(tx)}`); } const sequenceNum = (BigInt(tx.sequence) - 1n).toString(); let source; if (StrKey.isValidMed25519PublicKey(tx.source)) { source = MuxedAccount.fromAddress(tx.source, sequenceNum); } else if (StrKey.isValidEd25519PublicKey(tx.source)) { source = new Account(tx.source, sequenceNum); } else { throw new TypeError(`unsupported tx source account: ${tx.source}`); } if (tx.operations.length === 0) { throw new Error( "cannot clone a transaction with no operations: per-operation base fee cannot be determined" ); } let sorobanData; const envelope = tx.toEnvelope(); if (envelope.switch() === types.EnvelopeType.envelopeTypeTx()) { sorobanData = envelope.v1().tx().ext().value() ?? void 0; } let totalFee = parseInt(tx.fee, 10); if (sorobanData) { const resourceFee = Number(sorobanData.resourceFee().toBigInt()); if (totalFee - resourceFee > 0) { totalFee -= resourceFee; } } const unscaledFee = Math.floor(totalFee / tx.operations.length); const builderOpts = { fee: (unscaledFee || BASE_FEE).toString(), memo: tx.memo, networkPassphrase: tx.networkPassphrase }; if (tx.timeBounds) { builderOpts.timebounds = tx.timeBounds; } if (tx.ledgerBounds) { builderOpts.ledgerbounds = tx.ledgerBounds; } if (tx.minAccountSequence) { builderOpts.minAccountSequence = tx.minAccountSequence; } if (tx.minAccountSequenceAge !== void 0) { builderOpts.minAccountSequenceAge = tx.minAccountSequenceAge; } if (tx.minAccountSequenceLedgerGap !== void 0) { builderOpts.minAccountSequenceLedgerGap = tx.minAccountSequenceLedgerGap; } if (tx.extraSigners) { builderOpts.extraSigners = tx.extraSigners.map( (s) => SignerKey.encodeSignerKey(s) ); } Object.assign(builderOpts, opts); const builder = new TransactionBuilder(source, builderOpts); tx.tx.operations().forEach((op) => builder.addOperation(op)); return builder; } /** * Adds an operation to the transaction. * * @param operation - The xdr operation object, use {@link * Operation} static methods. */ addOperation(operation) { this.operations.push(operation); return this; } /** * Adds an operation to the transaction at a specific index. * * @param operation - The xdr operation object to add, use {@link Operation} static methods. * @param index - The index at which to insert the operation. */ addOperationAt(operation, index) { this.operations.splice(index, 0, operation); return this; } /** * Removes the operations from the builder (useful when cloning). */ clearOperations() { this.operations = []; return this; } /** * Removes the operation at the specified index from the transaction. * * @param index - The index of the operation to remove. */ clearOperationAt(index) { this.operations.splice(index, 1); return this; } /** * Adds a memo to the transaction. * @param memo - {@link Memo} object */ addMemo(memo) { this.memo = memo; return this; } /** * Sets a timeout precondition on the transaction. * * Because of the distributed nature of the Stellar network it is possible * that the status of your transaction will be determined after a long time * if the network is highly congested. If you want to be sure to receive the * status of the transaction within a given period you should set the * time bounds with `maxTime` on the transaction (this is what `setTimeout` * does internally; if there's `minTime` set but no `maxTime` it will be * added). * * A call to `TransactionBuilder.setTimeout` is **required** if Transaction * does not have `max_time` set. If you don't want to set timeout, use * {@link TimeoutInfinite}. In general you should set * {@link TimeoutInfinite} only in smart contracts. * * Please note that Horizon may still return <code>504 Gateway Timeout</code> * error, even for short timeouts. In such case you need to resubmit the same * transaction again without making any changes to receive a status. This * method is using the machine system time (UTC), make sure it is set * correctly. * * @param timeoutSeconds - Number of seconds the transaction is good. * Can't be negative. If the value is {@link TimeoutInfinite}, the * transaction is good indefinitely. * * @see {@link TimeoutInfinite} * @see https://developers.stellar.org/docs/tutorials/handling-errors/ */ setTimeout(timeoutSeconds) { if (this.timebounds !== null && Number(this.timebounds.maxTime) > 0) { throw new Error( "TimeBounds.max_time has been already set - setting timeout would overwrite it." ); } if (timeoutSeconds < 0) { throw new Error("timeout cannot be negative"); } if (timeoutSeconds > 0) { const timeoutTimestamp = Math.floor(Date.now() / 1e3) + timeoutSeconds; if (this.timebounds === null) { this.timebounds = { minTime: 0, maxTime: timeoutTimestamp }; } else { this.timebounds = { minTime: this.timebounds.minTime ?? 0, maxTime: timeoutTimestamp }; } } else { this.timebounds = { minTime: 0, maxTime: 0 }; } return this; } /** * If you want to prepare a transaction which will become valid at some point * in the future, or be invalid after some time, you can set a timebounds * precondition. Internally this will set the `minTime`, and `maxTime` * preconditions. Conflicts with `setTimeout`, so use one or the other. * * @param minEpochOrDate - Either a JS Date object, or a number * of UNIX epoch seconds. The transaction is valid after this timestamp. * Can't be negative. If the value is `0`, the transaction is valid * immediately. * @param maxEpochOrDate - Either a JS Date object, or a number * of UNIX epoch seconds. The transaction is valid until this timestamp. * Can't be negative. If the value is `0`, the transaction is valid * indefinitely. */ setTimebounds(minEpochOrDate, maxEpochOrDate) { if (typeof minEpochOrDate === "number") { minEpochOrDate = new Date(minEpochOrDate * 1e3); } if (typeof maxEpochOrDate === "number") { maxEpochOrDate = new Date(maxEpochOrDate * 1e3); } if (this.timebounds !== null) { throw new Error( "TimeBounds has been already set - setting timebounds would overwrite it." ); } const minTime = Math.floor(minEpochOrDate.valueOf() / 1e3); const maxTime = Math.floor(maxEpochOrDate.valueOf() / 1e3); if (minTime < 0) { throw new Error("min_time cannot be negative"); } if (maxTime < 0) { throw new Error("max_time cannot be negative"); } if (maxTime > 0 && minTime > maxTime) { throw new Error("min_time cannot be greater than max_time"); } this.timebounds = { minTime, maxTime }; return this; } /** * If you want to prepare a transaction which will only be valid within some * range of ledgers, you can set a ledgerbounds precondition. * Internally this will set the `minLedger` and `maxLedger` preconditions. * * @param minLedger - The minimum ledger this transaction is valid at * or after. Cannot be negative. If the value is `0` (the default), the * transaction is valid immediately. * * @param maxLedger - The maximum ledger this transaction is valid * before. Cannot be negative. If the value is `0`, the transaction is * valid indefinitely. */ setLedgerbounds(minLedger, maxLedger) { if (this.ledgerbounds !== null) { throw new Error( "LedgerBounds has been already set - setting ledgerbounds would overwrite it." ); } if (minLedger < 0) { throw new Error("min_ledger cannot be negative"); } if (maxLedger < 0) { throw new Error("max_ledger cannot be negative"); } if (maxLedger > 0 && minLedger > maxLedger) { throw new Error("min_ledger cannot be greater than max_ledger"); } this.ledgerbounds = { minLedger, maxLedger }; return this; } /** * If you want to prepare a transaction which will be valid only while the * account sequence number is * * `minAccountSequence <= sourceAccountSequence < tx.seqNum` * * Note that after execution the account's sequence number is always raised to * `tx.seqNum`. Internally this will set the `minAccountSequence` * precondition. * * @param minAccountSequence - The minimum source account sequence * number this transaction is valid for. If the value is `0` (the * default), the transaction is valid when `sourceAccount`'s sequence * number `== tx.seqNum - 1`. */ setMinAccountSequence(minAccountSequence) { if (this.minAccountSequence !== null) { throw new Error( "min_account_sequence has been already set - setting min_account_sequence would overwrite it." ); } this.minAccountSequence = minAccountSequence; return this; } /** * For the transaction to be valid, the current ledger time must be at least * `minAccountSequenceAge` greater than sourceAccount's `sequenceTime`. * Internally this will set the `minAccountSequenceAge` precondition. * * @param durationInSeconds - The minimum amount of time between * source account sequence time and the ledger time when this transaction * will become valid. If the value is `0`, the transaction is unrestricted * by the account sequence age. Cannot be negative. */ setMinAccountSequenceAge(durationInSeconds) { if (typeof durationInSeconds !== "bigint") { throw new Error("min_account_sequence_age must be a bigint"); } if (this.minAccountSequenceAge !== null) { throw new Error( "min_account_sequence_age has been already set - setting min_account_sequence_age would overwrite it." ); } if (durationInSeconds < 0) { throw new Error("min_account_sequence_age cannot be negative"); } this.minAccountSequenceAge = durationInSeconds; return this; } /** * For the transaction to be valid, the current ledger number must be at least * `minAccountSequenceLedgerGap` greater than sourceAccount's ledger sequence. * Internally this will set the `minAccountSequenceLedgerGap` precondition. * * @param gap - The minimum number of ledgers between source account * sequence and the ledger number when this transaction will become valid. * If the value is `0`, the transaction is unrestricted by the account * sequence ledger. Cannot be negative. */ setMinAccountSequenceLedgerGap(gap) { if (this.minAccountSequenceLedgerGap !== null) { throw new Error( "min_account_sequence_ledger_gap has been already set - setting min_account_sequence_ledger_gap would overwrite it." ); } if (gap < 0) { throw new Error("min_account_sequence_ledger_gap cannot be negative"); } this.minAccountSequenceLedgerGap = gap; return this; } /** * For the transaction to be valid, there must be a signature corresponding to * every Signer in this array, even if the signature is not otherwise required * by the sourceAccount or operations. Internally this will set the * `extraSigners` precondition. * * @param extraSigners - required extra signers (as {@link StrKey}s) */ setExtraSigners(extraSigners) { if (!Array.isArray(extraSigners)) { throw new Error("extra_signers must be an array of strings."); } if (this.extraSigners !== null) { throw new Error( "extra_signers has been already set - setting extra_signers would overwrite it." ); } if (extraSigners.length > 2) { throw new Error("extra_signers cannot be longer than 2 elements."); } this.extraSigners = [...extraSigners]; return this; } /** * Set network passphrase for the Transaction that will be built. * * @param networkPassphrase - passphrase of the target Stellar * network (e.g. "Public Global Stellar Network ; September 2015"). */ setNetworkPassphrase(networkPassphrase) { this.networkPassphrase = networkPassphrase; return this; } /** * Sets the transaction's internal Soroban transaction data (resources, * footprint, etc.). * * For non-contract(non-Soroban) transactions, this setting has no effect. In * the case of Soroban transactions, this is either an instance of * {@link xdr.SorobanTransactionData} or a base64-encoded string of said * structure. This is usually obtained from the simulation response based on a * transaction with a Soroban operation (e.g. * {@link Operation.invokeHostFunction}, providing necessary resource * and storage footprint estimations for contract invocation. * * @param sorobanData - the {@link xdr.SorobanTransactionData} as a raw xdr * object or a base64 string to be decoded * * @see {@link SorobanDataBuilder} */ setSorobanData(sorobanData) { this.sorobanData = new SorobanDataBuilder(sorobanData).build(); 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 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 to override the default fees used */ addSacTransferOperation(destination, asset, amount, sorobanFees) { if (BigInt(amount) <= 0n) { throw new Error("Amount must be a positive integer"); } else if (BigInt(amount) > HYPER_MAX_VALUE) { 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 (!StrKey.isValidEd25519PublicKey(destination) && !StrKey.isValidMed25519PublicKey(destination)) { throw new Error( "Invalid destination address. Must be a valid Stellar address or contract ID." ); } } const destinationBaseAddress = isDestinationContract ? destination : extractBaseAddress(destination); if (destinationBaseAddress === extractBaseAddress(this.source.accountId())) { throw new Error("Destination cannot be the same as the source account."); } if (this.networkPassphrase === null) { throw new Error( "networkPassphrase must be set to add a SAC transfer operation" ); } const contractId = asset.contractId(this.networkPassphrase); const functionName = "transfer"; const source = this.source.accountId(); const sourceBaseAddress = extractBaseAddress(source); const args = [ nativeToScVal(source, { type: "address" }), nativeToScVal(destination, { type: "address" }), nativeToScVal(amount, { type: "i128" }) ]; const isAssetNative = asset.isNative(); const auths = new types.SorobanAuthorizationEntry({ credentials: types.SorobanCredentials.sorobanCredentialsSourceAccount(), rootInvocation: new types.SorobanAuthorizedInvocation({ function: types.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( new types.InvokeContractArgs({ contractAddress: Address.fromString(contractId).toScAddress(), functionName, args }) ), subInvocations: [] }) }); const footprint = new types.LedgerFootprint({ readOnly: [ types.LedgerKey.contractData( new types.LedgerKeyContractData({ contract: Address.fromString(contractId).toScAddress(), key: types.ScVal.scvLedgerKeyContractInstance(), durability: types.ContractDataDurability.persistent() }) ) ], readWrite: [] }); if (isDestinationContract) { footprint.readWrite().push( types.LedgerKey.contractData( new types.LedgerKeyContractData({ contract: Address.fromString(contractId).toScAddress(), key: types.ScVal.scvVec([ nativeToScVal("Balance", { type: "symbol" }), nativeToScVal(destination, { type: "address" }) ]), durability: types.ContractDataDurability.persistent() }) ) ); if (!isAssetNative) { const assetIssuer = asset.getIssuer(); if (!assetIssuer) { throw new Error("Asset issuer must be set for non-native assets."); } footprint.readOnly().push( types.LedgerKey.account( new types.LedgerKeyAccount({ accountId: Keypair.fromPublicKey(assetIssuer).xdrPublicKey() }) ) ); } } else if (isAssetNative) { footprint.readWrite().push( types.LedgerKey.account( new types.LedgerKeyAccount({ accountId: Keypair.fromPublicKey( destinationBaseAddress ).xdrPublicKey() }) ) ); } else if (asset.getIssuer() !== destinationBaseAddress) { footprint.readWrite().push( types.LedgerKey.trustline( new types.LedgerKeyTrustLine({ accountId: Keypair.fromPublicKey( destinationBaseAddress ).xdrPublicKey(), asset: asset.toTrustLineXDRObject() }) ) ); } if (asset.isNative()) { footprint.readWrite().push( types.LedgerKey.account( new types.LedgerKeyAccount({ accountId: Keypair.fromPublicKey(sourceBaseAddress).xdrPublicKey() }) ) ); } else if (asset.getIssuer() !== sourceBaseAddress) { footprint.readWrite().push( types.LedgerKey.trustline( new types.LedgerKeyTrustLine({ accountId: Keypair.fromPublicKey(sourceBaseAddress).xdrPublicKey(), asset: asset.toTrustLineXDRObject() }) ) ); } const defaultPaymentFees = { instructions: 4e5, readBytes: 1e3, writeBytes: 1e3, resourceFee: BigInt(5e6) }; const sorobanData = new types.SorobanTransactionData({ resources: new types.SorobanResources({ footprint, instructions: sorobanFees ? sorobanFees.instructions : defaultPaymentFees.instructions, diskReadBytes: sorobanFees ? sorobanFees.readBytes : defaultPaymentFees.readBytes, writeBytes: sorobanFees ? sorobanFees.writeBytes : defaultPaymentFees.writeBytes }), ext: new types.SorobanTransactionDataExt(0), resourceFee: new types.Int64( sorobanFees ? sorobanFees.resourceFee : defaultPaymentFees.resourceFee ) }); const operation = Operation.invokeContractFunction({ contract: contractId, function: functionName, args, auth: [auths] }); this.setSorobanData(sorobanData); return this.addOperation(operation); } /** * Builds the transaction and increments the source account's sequence * number by 1. */ build() { const sequenceNumber = new BigNumber(this.source.sequenceNumber()).plus(1); const fee = new BigNumber(this.baseFee).times(this.operations.length).toNumber(); if (fee > UINT32_MAX) { throw new Error( `Total fee (baseFee * operations) exceeds the maximum uint32 value (${UINT32_MAX}). Got ${fee} from baseFee=${this.baseFee} and ${this.operations.length} operation(s).` ); } const attrs = { fee, seqNum: types.Int64.fromString(sequenceNumber.toString()), memo: this.memo ? this.memo.toXDRObject() : null }; if (this.timebounds === null || typeof this.timebounds.minTime === "undefined" || typeof this.timebounds.maxTime === "undefined") { throw new Error( "TimeBounds has to be set or you must call setTimeout(TimeoutInfinite)." ); } if (isValidDate(this.timebounds.minTime)) { this.timebounds.minTime = Math.floor( this.timebounds.minTime.getTime() / 1e3 ); } if (isValidDate(this.timebounds.maxTime)) { this.timebounds.maxTime = Math.floor( this.timebounds.maxTime.getTime() / 1e3 ); } const minTime = types.Uint64.fromString(this.timebounds.minTime.toString()); const maxTime = types.Uint64.fromString(this.timebounds.maxTime.toString()); const timeBounds = new types.TimeBounds({ minTime, maxTime }); if (this.hasV2Preconditions()) { let ledgerBounds = null; if (this.ledgerbounds !== null) { ledgerBounds = new types.LedgerBounds({ minLedger: this.ledgerbounds.minLedger ?? 0, maxLedger: this.ledgerbounds.maxLedger ?? 0 }); } const minSeqNum = this.minAccountSequence ? types.Int64.fromString(this.minAccountSequence) : null; const minSeqAge = types.Uint64.fromString( this.minAccountSequenceAge !== null ? this.minAccountSequenceAge.toString() : "0" ); const minSeqLedgerGap = this.minAccountSequenceLedgerGap || 0; const extraSigners = this.extraSigners !== null ? this.extraSigners.map((s) => SignerKey.decodeAddress(s)) : []; attrs.cond = types.Preconditions.precondV2( new types.PreconditionsV2({ timeBounds, ledgerBounds, minSeqNum, minSeqAge, minSeqLedgerGap, extraSigners }) ); } else { attrs.cond = types.Preconditions.precondTime(timeBounds); } attrs.sourceAccount = decodeAddressToMuxedAccount(this.source.accountId()); if (this.sorobanData) { attrs.ext = new types.TransactionExt(1, this.sorobanData); attrs.fee = new BigNumber(attrs.fee).plus(this.sorobanData.resourceFee().toString()).toNumber(); if (attrs.fee > UINT32_MAX) { throw new Error( `Total fee (baseFee * operations + resourceFee) exceeds the maximum uint32 value (${UINT32_MAX}). Got ${attrs.fee}.` ); } } else { attrs.ext = new types.TransactionExt(0); } const xtx = new types.Transaction( attrs ); xtx.operations(this.operations); const txEnvelope = types.TransactionEnvelope.envelopeTypeTx( new types.TransactionV1Envelope({ tx: xtx, signatures: [] }) ); if (this.networkPassphrase === null) { throw new Error("networkPassphrase must be set to build a transaction"); } const tx = new Transaction(txEnvelope, this.networkPassphrase); this.source.incrementSequenceNumber(); return tx; } /** * Checks whether any v2 preconditions have been set on this builder. */ hasV2Preconditions() { return this.ledgerbounds !== null || this.minAccountSequence !== null || this.minAccountSequenceAge !== null || this.minAccountSequenceLedgerGap !== null || this.extraSigners !== null && this.extraSigners.length > 0; } /** * Builds a {@link FeeBumpTransaction}, enabling you to resubmit an existing * transaction with a higher fee. * * @param feeSource - account paying for the transaction, * in the form of either a Keypair (only the public key is used) or * an account ID (in G... or M... form, but refer to `withMuxing`) * @param baseFee - max fee willing to pay per operation * in inner transaction (**in stroops**) * @param innerTx - {@link Transaction} to be bumped by * the fee bump transaction * @param networkPassphrase - passphrase of the target * Stellar network (e.g. "Public Global Stellar Network ; September 2015", * see {@link Networks}) * * TODO: Alongside the next major version bump, this type signature can be * changed to be less awkward: accept a MuxedAccount as the `feeSource` * rather than a keypair or string. * * Your fee-bump amount should be `>= 10x` the original fee. * @see https://developers.stellar.org/docs/glossary/fee-bumps/#replace-by-fee */ static buildFeeBumpTransaction(feeSource, baseFee, innerTx, networkPassphrase) { const innerOps = innerTx.operations.length; const minBaseFee = new BigNumber(BASE_FEE); let resourceFee = new BigNumber(0); const env = innerTx.toEnvelope(); switch (env.switch().value) { case types.EnvelopeType.envelopeTypeTx().value: { const sorobanData = env.v1().tx().ext().value(); resourceFee = new BigNumber(sorobanData?.resourceFee().toString() ?? 0); break; } } const innerInclusionFee = new BigNumber(innerTx.fee).minus(resourceFee).div(innerOps); const base = new BigNumber(baseFee); if (base.lt(innerInclusionFee)) { throw new Error( `Invalid baseFee, it should be at least ${innerInclusionFee.toString()} stroops.` ); } if (base.lt(minBaseFee)) { throw new Error( `Invalid baseFee, it should be at least ${minBaseFee.toString()} stroops.` ); } let innerTxEnvelope = innerTx.toEnvelope(); if (innerTxEnvelope.switch() === types.EnvelopeType.envelopeTypeTxV0()) { const v0Tx = innerTxEnvelope.v0().tx(); const v0TimeBounds = v0Tx.timeBounds(); if (v0TimeBounds === null) { throw new Error("Inner transaction must have time bounds"); } const v1Tx = new types.Transaction({ sourceAccount: types.MuxedAccount.keyTypeEd25519( v0Tx.sourceAccountEd25519() ), fee: v0Tx.fee(), seqNum: v0Tx.seqNum(), cond: types.Preconditions.precondTime(v0TimeBounds), memo: v0Tx.memo(), operations: v0Tx.operations(), ext: new types.TransactionExt(0) }); innerTxEnvelope = types.TransactionEnvelope.envelopeTypeTx( new types.TransactionV1Envelope({ tx: v1Tx, signatures: innerTxEnvelope.v0().signatures() }) ); } let feeSourceAccount; if (typeof feeSource === "string") { feeSourceAccount = decodeAddressToMuxedAccount(feeSource); } else { feeSourceAccount = feeSource.xdrMuxedAccount(); } const tx = new types.FeeBumpTransaction({ feeSource: feeSourceAccount, fee: types.Int64.fromString( base.times(innerOps + 1).plus(resourceFee).toString() ), innerTx: types.FeeBumpTransactionInnerTx.envelopeTypeTx( innerTxEnvelope.v1() ), ext: new types.FeeBumpTransactionExt(0) }); const feeBumpTxEnvelope = new types.FeeBumpTransactionEnvelope({ tx, signatures: [] }); const envelope = types.TransactionEnvelope.envelopeTypeTxFeeBump(feeBumpTxEnvelope); return new FeeBumpTransaction(envelope, networkPassphrase); } /** * Build a {@link Transaction} or {@link FeeBumpTransaction} from an * xdr.TransactionEnvelope. * * @param envelope - The transaction envelope * object or base64 encoded string. * @param networkPassphrase - The network passphrase of the target * Stellar network (e.g. "Public Global Stellar Network ; September * 2015"), see {@link Networks}. */ static fromXDR(envelope, networkPassphrase) { if (typeof envelope === "string") { envelope = types.TransactionEnvelope.fromXDR(envelope, "base64"); } if (envelope.switch() === types.EnvelopeType.envelopeTypeTxFeeBump()) { return new FeeBumpTransaction(envelope, networkPassphrase); } return new Transaction(envelope, networkPassphrase); } } function isValidDate(d) { return d instanceof Date && !Number.isNaN(d.getTime()); } function toEpochSeconds(value) { if (value === void 0) { return void 0; } const num = value instanceof Date ? Math.floor(value.getTime() / 1e3) : Number(value); if (!Number.isFinite(num) || num % 1 !== 0) { throw new Error("timebounds value must be a finite integer or Date"); } return num; } export { BASE_FEE, TimeoutInfinite, TransactionBuilder, isValidDate }; //# sourceMappingURL=transaction_builder.js.map