Source

js-stellar-base/src/transaction_builder.js

  1. import { UnsignedHyper } from '@stellar/js-xdr';
  2. import BigNumber from './util/bignumber';
  3. import xdr from './xdr';
  4. import { Account } from './account';
  5. import { MuxedAccount } from './muxed_account';
  6. import { decodeAddressToMuxedAccount } from './util/decode_encode_muxed_account';
  7. import { Transaction } from './transaction';
  8. import { FeeBumpTransaction } from './fee_bump_transaction';
  9. import { SorobanDataBuilder } from './sorobandata_builder';
  10. import { StrKey } from './strkey';
  11. import { SignerKey } from './signerkey';
  12. import { Memo } from './memo';
  13. /**
  14. * Minimum base fee for transactions. If this fee is below the network
  15. * minimum, the transaction will fail. The more operations in the
  16. * transaction, the greater the required fee. Use {@link
  17. * Server#fetchBaseFee} to get an accurate value of minimum transaction
  18. * fee on the network.
  19. *
  20. * @constant
  21. * @see [Fees](https://developers.stellar.org/docs/glossary/fees/)
  22. */
  23. export const BASE_FEE = '100'; // Stroops
  24. /**
  25. * @constant
  26. * @see {@link TransactionBuilder#setTimeout}
  27. * @see [Timeout](https://developers.stellar.org/api/resources/transactions/post/)
  28. */
  29. export const TimeoutInfinite = 0;
  30. /**
  31. * <p>Transaction builder helps constructs a new `{@link Transaction}` using the
  32. * given {@link Account} as the transaction's "source account". The transaction
  33. * will use the current sequence number of the given account as its sequence
  34. * number and increment the given account's sequence number by one. The given
  35. * source account must include a private key for signing the transaction or an
  36. * error will be thrown.</p>
  37. *
  38. * <p>Operations can be added to the transaction via their corresponding builder
  39. * methods, and each returns the TransactionBuilder object so they can be
  40. * chained together. After adding the desired operations, call the `build()`
  41. * method on the `TransactionBuilder` to return a fully constructed `{@link
  42. * Transaction}` that can be signed. The returned transaction will contain the
  43. * sequence number of the source account and include the signature from the
  44. * source account.</p>
  45. *
  46. * <p><strong>Be careful about unsubmitted transactions!</strong> When you build
  47. * a transaction, `stellar-sdk` automatically increments the source account's
  48. * sequence number. If you end up not submitting this transaction and submitting
  49. * another one instead, it'll fail due to the sequence number being wrong. So if
  50. * you decide not to use a built transaction, make sure to update the source
  51. * account's sequence number with
  52. * [Server.loadAccount](https://stellar.github.io/js-stellar-sdk/Server.html#loadAccount)
  53. * before creating another transaction.</p>
  54. *
  55. * <p>The following code example creates a new transaction with {@link
  56. * Operation.createAccount} and {@link Operation.payment} operations. The
  57. * Transaction's source account first funds `destinationA`, then sends a payment
  58. * to `destinationB`. The built transaction is then signed by
  59. * `sourceKeypair`.</p>
  60. *
  61. * ```
  62. * var transaction = new TransactionBuilder(source, { fee, networkPassphrase: Networks.TESTNET })
  63. * .addOperation(Operation.createAccount({
  64. * destination: destinationA,
  65. * startingBalance: "20"
  66. * })) // <- funds and creates destinationA
  67. * .addOperation(Operation.payment({
  68. * destination: destinationB,
  69. * amount: "100",
  70. * asset: Asset.native()
  71. * })) // <- sends 100 XLM to destinationB
  72. * .setTimeout(30)
  73. * .build();
  74. *
  75. * transaction.sign(sourceKeypair);
  76. * ```
  77. *
  78. * @constructor
  79. *
  80. * @param {Account} sourceAccount - source account for this transaction
  81. * @param {object} opts - Options object
  82. * @param {string} opts.fee - max fee you're willing to pay per
  83. * operation in this transaction (**in stroops**)
  84. *
  85. * @param {object} [opts.timebounds] - timebounds for the
  86. * validity of this transaction
  87. * @param {number|string|Date} [opts.timebounds.minTime] - 64-bit UNIX
  88. * timestamp or Date object
  89. * @param {number|string|Date} [opts.timebounds.maxTime] - 64-bit UNIX
  90. * timestamp or Date object
  91. * @param {object} [opts.ledgerbounds] - ledger bounds for the
  92. * validity of this transaction
  93. * @param {number} [opts.ledgerbounds.minLedger] - number of the minimum
  94. * ledger sequence
  95. * @param {number} [opts.ledgerbounds.maxLedger] - number of the maximum
  96. * ledger sequence
  97. * @param {string} [opts.minAccountSequence] - number for
  98. * the minimum account sequence
  99. * @param {number} [opts.minAccountSequenceAge] - number of
  100. * seconds for the minimum account sequence age
  101. * @param {number} [opts.minAccountSequenceLedgerGap] - number of
  102. * ledgers for the minimum account sequence ledger gap
  103. * @param {string[]} [opts.extraSigners] - list of the extra signers
  104. * required for this transaction
  105. * @param {Memo} [opts.memo] - memo for the transaction
  106. * @param {string} [opts.networkPassphrase] passphrase of the
  107. * target Stellar network (e.g. "Public Global Stellar Network ; September
  108. * 2015" for the pubnet)
  109. * @param {xdr.SorobanTransactionData | string} [opts.sorobanData] - an
  110. * optional instance of {@link xdr.SorobanTransactionData} to be set as the
  111. * internal `Transaction.Ext.SorobanData` field (either the xdr object or a
  112. * base64 string). In the case of Soroban transactions, this can be obtained
  113. * from a prior simulation of the transaction with a contract invocation and
  114. * provides necessary resource estimations. You can also use
  115. * {@link SorobanDataBuilder} to construct complicated combinations of
  116. * parameters without mucking with XDR directly. **Note:** For
  117. * non-contract(non-Soroban) transactions, this has no effect.
  118. */
  119. export class TransactionBuilder {
  120. constructor(sourceAccount, opts = {}) {
  121. if (!sourceAccount) {
  122. throw new Error('must specify source account for the transaction');
  123. }
  124. if (opts.fee === undefined) {
  125. throw new Error('must specify fee for the transaction (in stroops)');
  126. }
  127. this.source = sourceAccount;
  128. this.operations = [];
  129. this.baseFee = opts.fee;
  130. this.timebounds = opts.timebounds ? { ...opts.timebounds } : null;
  131. this.ledgerbounds = opts.ledgerbounds ? { ...opts.ledgerbounds } : null;
  132. this.minAccountSequence = opts.minAccountSequence || null;
  133. this.minAccountSequenceAge = opts.minAccountSequenceAge || null;
  134. this.minAccountSequenceLedgerGap = opts.minAccountSequenceLedgerGap || null;
  135. this.extraSigners = opts.extraSigners ? [...opts.extraSigners] : null;
  136. this.memo = opts.memo || Memo.none();
  137. this.networkPassphrase = opts.networkPassphrase || null;
  138. this.sorobanData = opts.sorobanData
  139. ? new SorobanDataBuilder(opts.sorobanData).build()
  140. : null;
  141. }
  142. /**
  143. * Creates a builder instance using an existing {@link Transaction} as a
  144. * template, ignoring any existing envelope signatures.
  145. *
  146. * Note that the sequence number WILL be cloned, so EITHER this transaction or
  147. * the one it was cloned from will be valid. This is useful in situations
  148. * where you are constructing a transaction in pieces and need to make
  149. * adjustments as you go (for example, when filling out Soroban resource
  150. * information).
  151. *
  152. * @param {Transaction} tx a "template" transaction to clone exactly
  153. * @param {object} [opts] additional options to override the clone, e.g.
  154. * {fee: '1000'} will override the existing base fee derived from `tx` (see
  155. * the {@link TransactionBuilder} constructor for detailed options)
  156. *
  157. * @returns {TransactionBuilder} a "prepared" builder instance with the same
  158. * configuration and operations as the given transaction
  159. *
  160. * @warning This does not clone the transaction's
  161. * {@link xdr.SorobanTransactionData} (if applicable), use
  162. * {@link SorobanDataBuilder} and {@link TransactionBuilder.setSorobanData}
  163. * as needed, instead..
  164. *
  165. * @todo This cannot clone {@link FeeBumpTransaction}s, yet.
  166. */
  167. static cloneFrom(tx, opts = {}) {
  168. if (!(tx instanceof Transaction)) {
  169. throw new TypeError(`expected a 'Transaction', got: ${tx}`);
  170. }
  171. const sequenceNum = (BigInt(tx.sequence) - 1n).toString();
  172. let source;
  173. // rebuild the source account based on the strkey
  174. if (StrKey.isValidMed25519PublicKey(tx.source)) {
  175. source = MuxedAccount.fromAddress(tx.source, sequenceNum);
  176. } else if (StrKey.isValidEd25519PublicKey(tx.source)) {
  177. source = new Account(tx.source, sequenceNum);
  178. } else {
  179. throw new TypeError(`unsupported tx source account: ${tx.source}`);
  180. }
  181. // the initial fee passed to the builder gets scaled up based on the number
  182. // of operations at the end, so we have to down-scale first
  183. const unscaledFee = parseInt(tx.fee, 10) / tx.operations.length;
  184. const builder = new TransactionBuilder(source, {
  185. fee: (unscaledFee || BASE_FEE).toString(),
  186. memo: tx.memo,
  187. networkPassphrase: tx.networkPassphrase,
  188. timebounds: tx.timeBounds,
  189. ledgerbounds: tx.ledgerBounds,
  190. minAccountSequence: tx.minAccountSequence,
  191. minAccountSequenceAge: tx.minAccountSequenceAge,
  192. minAccountSequenceLedgerGap: tx.minAccountSequenceLedgerGap,
  193. extraSigners: tx.extraSigners,
  194. ...opts
  195. });
  196. tx._tx.operations().forEach((op) => builder.addOperation(op));
  197. return builder;
  198. }
  199. /**
  200. * Adds an operation to the transaction.
  201. *
  202. * @param {xdr.Operation} operation The xdr operation object, use {@link
  203. * Operation} static methods.
  204. *
  205. * @returns {TransactionBuilder}
  206. */
  207. addOperation(operation) {
  208. this.operations.push(operation);
  209. return this;
  210. }
  211. /**
  212. * Adds an operation to the transaction at a specific index.
  213. *
  214. * @param {xdr.Operation} operation - The xdr operation object to add, use {@link Operation} static methods.
  215. * @param {number} index - The index at which to insert the operation.
  216. *
  217. * @returns {TransactionBuilder} - The TransactionBuilder instance for method chaining.
  218. */
  219. addOperationAt(operation, index) {
  220. this.operations.splice(index, 0, operation);
  221. return this;
  222. }
  223. /**
  224. * Removes the operations from the builder (useful when cloning).
  225. * @returns {TransactionBuilder} this builder instance
  226. */
  227. clearOperations() {
  228. this.operations = [];
  229. return this;
  230. }
  231. /**
  232. * Removes the operation at the specified index from the transaction.
  233. *
  234. * @param {number} index - The index of the operation to remove.
  235. *
  236. * @returns {TransactionBuilder} The TransactionBuilder instance for method chaining.
  237. */
  238. clearOperationAt(index) {
  239. this.operations.splice(index, 1);
  240. return this;
  241. }
  242. /**
  243. * Adds a memo to the transaction.
  244. * @param {Memo} memo {@link Memo} object
  245. * @returns {TransactionBuilder}
  246. */
  247. addMemo(memo) {
  248. this.memo = memo;
  249. return this;
  250. }
  251. /**
  252. * Sets a timeout precondition on the transaction.
  253. *
  254. * Because of the distributed nature of the Stellar network it is possible
  255. * that the status of your transaction will be determined after a long time
  256. * if the network is highly congested. If you want to be sure to receive the
  257. * status of the transaction within a given period you should set the {@link
  258. * TimeBounds} with `maxTime` on the transaction (this is what `setTimeout`
  259. * does internally; if there's `minTime` set but no `maxTime` it will be
  260. * added).
  261. *
  262. * A call to `TransactionBuilder.setTimeout` is **required** if Transaction
  263. * does not have `max_time` set. If you don't want to set timeout, use
  264. * `{@link TimeoutInfinite}`. In general you should set `{@link
  265. * TimeoutInfinite}` only in smart contracts.
  266. *
  267. * Please note that Horizon may still return <code>504 Gateway Timeout</code>
  268. * error, even for short timeouts. In such case you need to resubmit the same
  269. * transaction again without making any changes to receive a status. This
  270. * method is using the machine system time (UTC), make sure it is set
  271. * correctly.
  272. *
  273. * @param {number} timeoutSeconds Number of seconds the transaction is good.
  274. * Can't be negative. If the value is {@link TimeoutInfinite}, the
  275. * transaction is good indefinitely.
  276. *
  277. * @returns {TransactionBuilder}
  278. *
  279. * @see {@link TimeoutInfinite}
  280. * @see https://developers.stellar.org/docs/tutorials/handling-errors/
  281. */
  282. setTimeout(timeoutSeconds) {
  283. if (this.timebounds !== null && this.timebounds.maxTime > 0) {
  284. throw new Error(
  285. 'TimeBounds.max_time has been already set - setting timeout would overwrite it.'
  286. );
  287. }
  288. if (timeoutSeconds < 0) {
  289. throw new Error('timeout cannot be negative');
  290. }
  291. if (timeoutSeconds > 0) {
  292. const timeoutTimestamp = Math.floor(Date.now() / 1000) + timeoutSeconds;
  293. if (this.timebounds === null) {
  294. this.timebounds = { minTime: 0, maxTime: timeoutTimestamp };
  295. } else {
  296. this.timebounds = {
  297. minTime: this.timebounds.minTime,
  298. maxTime: timeoutTimestamp
  299. };
  300. }
  301. } else {
  302. this.timebounds = {
  303. minTime: 0,
  304. maxTime: 0
  305. };
  306. }
  307. return this;
  308. }
  309. /**
  310. * If you want to prepare a transaction which will become valid at some point
  311. * in the future, or be invalid after some time, you can set a timebounds
  312. * precondition. Internally this will set the `minTime`, and `maxTime`
  313. * preconditions. Conflicts with `setTimeout`, so use one or the other.
  314. *
  315. * @param {Date|number} minEpochOrDate Either a JS Date object, or a number
  316. * of UNIX epoch seconds. The transaction is valid after this timestamp.
  317. * Can't be negative. If the value is `0`, the transaction is valid
  318. * immediately.
  319. * @param {Date|number} maxEpochOrDate Either a JS Date object, or a number
  320. * of UNIX epoch seconds. The transaction is valid until this timestamp.
  321. * Can't be negative. If the value is `0`, the transaction is valid
  322. * indefinitely.
  323. *
  324. * @returns {TransactionBuilder}
  325. */
  326. setTimebounds(minEpochOrDate, maxEpochOrDate) {
  327. // Force it to a date type
  328. if (typeof minEpochOrDate === 'number') {
  329. minEpochOrDate = new Date(minEpochOrDate * 1000);
  330. }
  331. if (typeof maxEpochOrDate === 'number') {
  332. maxEpochOrDate = new Date(maxEpochOrDate * 1000);
  333. }
  334. if (this.timebounds !== null) {
  335. throw new Error(
  336. 'TimeBounds has been already set - setting timebounds would overwrite it.'
  337. );
  338. }
  339. // Convert that date to the epoch seconds
  340. const minTime = Math.floor(minEpochOrDate.valueOf() / 1000);
  341. const maxTime = Math.floor(maxEpochOrDate.valueOf() / 1000);
  342. if (minTime < 0) {
  343. throw new Error('min_time cannot be negative');
  344. }
  345. if (maxTime < 0) {
  346. throw new Error('max_time cannot be negative');
  347. }
  348. if (maxTime > 0 && minTime > maxTime) {
  349. throw new Error('min_time cannot be greater than max_time');
  350. }
  351. this.timebounds = { minTime, maxTime };
  352. return this;
  353. }
  354. /**
  355. * If you want to prepare a transaction which will only be valid within some
  356. * range of ledgers, you can set a ledgerbounds precondition.
  357. * Internally this will set the `minLedger` and `maxLedger` preconditions.
  358. *
  359. * @param {number} minLedger The minimum ledger this transaction is valid at
  360. * or after. Cannot be negative. If the value is `0` (the default), the
  361. * transaction is valid immediately.
  362. *
  363. * @param {number} maxLedger The maximum ledger this transaction is valid
  364. * before. Cannot be negative. If the value is `0`, the transaction is
  365. * valid indefinitely.
  366. *
  367. * @returns {TransactionBuilder}
  368. */
  369. setLedgerbounds(minLedger, maxLedger) {
  370. if (this.ledgerbounds !== null) {
  371. throw new Error(
  372. 'LedgerBounds has been already set - setting ledgerbounds would overwrite it.'
  373. );
  374. }
  375. if (minLedger < 0) {
  376. throw new Error('min_ledger cannot be negative');
  377. }
  378. if (maxLedger < 0) {
  379. throw new Error('max_ledger cannot be negative');
  380. }
  381. if (maxLedger > 0 && minLedger > maxLedger) {
  382. throw new Error('min_ledger cannot be greater than max_ledger');
  383. }
  384. this.ledgerbounds = { minLedger, maxLedger };
  385. return this;
  386. }
  387. /**
  388. * If you want to prepare a transaction which will be valid only while the
  389. * account sequence number is
  390. *
  391. * minAccountSequence <= sourceAccountSequence < tx.seqNum
  392. *
  393. * Note that after execution the account's sequence number is always raised to
  394. * `tx.seqNum`. Internally this will set the `minAccountSequence`
  395. * precondition.
  396. *
  397. * @param {string} minAccountSequence The minimum source account sequence
  398. * number this transaction is valid for. If the value is `0` (the
  399. * default), the transaction is valid when `sourceAccount's sequence
  400. * number == tx.seqNum- 1`.
  401. *
  402. * @returns {TransactionBuilder}
  403. */
  404. setMinAccountSequence(minAccountSequence) {
  405. if (this.minAccountSequence !== null) {
  406. throw new Error(
  407. 'min_account_sequence has been already set - setting min_account_sequence would overwrite it.'
  408. );
  409. }
  410. this.minAccountSequence = minAccountSequence;
  411. return this;
  412. }
  413. /**
  414. * For the transaction to be valid, the current ledger time must be at least
  415. * `minAccountSequenceAge` greater than sourceAccount's `sequenceTime`.
  416. * Internally this will set the `minAccountSequenceAge` precondition.
  417. *
  418. * @param {number} durationInSeconds The minimum amount of time between
  419. * source account sequence time and the ledger time when this transaction
  420. * will become valid. If the value is `0`, the transaction is unrestricted
  421. * by the account sequence age. Cannot be negative.
  422. *
  423. * @returns {TransactionBuilder}
  424. */
  425. setMinAccountSequenceAge(durationInSeconds) {
  426. if (typeof durationInSeconds !== 'number') {
  427. throw new Error('min_account_sequence_age must be a number');
  428. }
  429. if (this.minAccountSequenceAge !== null) {
  430. throw new Error(
  431. 'min_account_sequence_age has been already set - setting min_account_sequence_age would overwrite it.'
  432. );
  433. }
  434. if (durationInSeconds < 0) {
  435. throw new Error('min_account_sequence_age cannot be negative');
  436. }
  437. this.minAccountSequenceAge = durationInSeconds;
  438. return this;
  439. }
  440. /**
  441. * For the transaction to be valid, the current ledger number must be at least
  442. * `minAccountSequenceLedgerGap` greater than sourceAccount's ledger sequence.
  443. * Internally this will set the `minAccountSequenceLedgerGap` precondition.
  444. *
  445. * @param {number} gap The minimum number of ledgers between source account
  446. * sequence and the ledger number when this transaction will become valid.
  447. * If the value is `0`, the transaction is unrestricted by the account
  448. * sequence ledger. Cannot be negative.
  449. *
  450. * @returns {TransactionBuilder}
  451. */
  452. setMinAccountSequenceLedgerGap(gap) {
  453. if (this.minAccountSequenceLedgerGap !== null) {
  454. throw new Error(
  455. 'min_account_sequence_ledger_gap has been already set - setting min_account_sequence_ledger_gap would overwrite it.'
  456. );
  457. }
  458. if (gap < 0) {
  459. throw new Error('min_account_sequence_ledger_gap cannot be negative');
  460. }
  461. this.minAccountSequenceLedgerGap = gap;
  462. return this;
  463. }
  464. /**
  465. * For the transaction to be valid, there must be a signature corresponding to
  466. * every Signer in this array, even if the signature is not otherwise required
  467. * by the sourceAccount or operations. Internally this will set the
  468. * `extraSigners` precondition.
  469. *
  470. * @param {string[]} extraSigners required extra signers (as {@link StrKey}s)
  471. *
  472. * @returns {TransactionBuilder}
  473. */
  474. setExtraSigners(extraSigners) {
  475. if (!Array.isArray(extraSigners)) {
  476. throw new Error('extra_signers must be an array of strings.');
  477. }
  478. if (this.extraSigners !== null) {
  479. throw new Error(
  480. 'extra_signers has been already set - setting extra_signers would overwrite it.'
  481. );
  482. }
  483. if (extraSigners.length > 2) {
  484. throw new Error('extra_signers cannot be longer than 2 elements.');
  485. }
  486. this.extraSigners = [...extraSigners];
  487. return this;
  488. }
  489. /**
  490. * Set network nassphrase for the Transaction that will be built.
  491. *
  492. * @param {string} networkPassphrase passphrase of the target Stellar
  493. * network (e.g. "Public Global Stellar Network ; September 2015").
  494. *
  495. * @returns {TransactionBuilder}
  496. */
  497. setNetworkPassphrase(networkPassphrase) {
  498. this.networkPassphrase = networkPassphrase;
  499. return this;
  500. }
  501. /**
  502. * Sets the transaction's internal Soroban transaction data (resources,
  503. * footprint, etc.).
  504. *
  505. * For non-contract(non-Soroban) transactions, this setting has no effect. In
  506. * the case of Soroban transactions, this is either an instance of
  507. * {@link xdr.SorobanTransactionData} or a base64-encoded string of said
  508. * structure. This is usually obtained from the simulation response based on a
  509. * transaction with a Soroban operation (e.g.
  510. * {@link Operation.invokeHostFunction}, providing necessary resource
  511. * and storage footprint estimations for contract invocation.
  512. *
  513. * @param {xdr.SorobanTransactionData | string} sorobanData the
  514. * {@link xdr.SorobanTransactionData} as a raw xdr object or a base64
  515. * string to be decoded
  516. *
  517. * @returns {TransactionBuilder}
  518. * @see {SorobanDataBuilder}
  519. */
  520. setSorobanData(sorobanData) {
  521. this.sorobanData = new SorobanDataBuilder(sorobanData).build();
  522. return this;
  523. }
  524. /**
  525. * This will build the transaction.
  526. * It will also increment the source account's sequence number by 1.
  527. * @returns {Transaction} This method will return the built {@link Transaction}.
  528. */
  529. build() {
  530. const sequenceNumber = new BigNumber(this.source.sequenceNumber()).plus(1);
  531. const fee = new BigNumber(this.baseFee)
  532. .times(this.operations.length)
  533. .toNumber();
  534. const attrs = {
  535. fee,
  536. seqNum: xdr.SequenceNumber.fromString(sequenceNumber.toString()),
  537. memo: this.memo ? this.memo.toXDRObject() : null
  538. };
  539. if (
  540. this.timebounds === null ||
  541. typeof this.timebounds.minTime === 'undefined' ||
  542. typeof this.timebounds.maxTime === 'undefined'
  543. ) {
  544. throw new Error(
  545. 'TimeBounds has to be set or you must call setTimeout(TimeoutInfinite).'
  546. );
  547. }
  548. if (isValidDate(this.timebounds.minTime)) {
  549. this.timebounds.minTime = this.timebounds.minTime.getTime() / 1000;
  550. }
  551. if (isValidDate(this.timebounds.maxTime)) {
  552. this.timebounds.maxTime = this.timebounds.maxTime.getTime() / 1000;
  553. }
  554. this.timebounds.minTime = UnsignedHyper.fromString(
  555. this.timebounds.minTime.toString()
  556. );
  557. this.timebounds.maxTime = UnsignedHyper.fromString(
  558. this.timebounds.maxTime.toString()
  559. );
  560. const timeBounds = new xdr.TimeBounds(this.timebounds);
  561. if (this.hasV2Preconditions()) {
  562. let ledgerBounds = null;
  563. if (this.ledgerbounds !== null) {
  564. ledgerBounds = new xdr.LedgerBounds(this.ledgerbounds);
  565. }
  566. let minSeqNum = this.minAccountSequence || '0';
  567. minSeqNum = xdr.SequenceNumber.fromString(minSeqNum);
  568. const minSeqAge = UnsignedHyper.fromString(
  569. this.minAccountSequenceAge !== null
  570. ? this.minAccountSequenceAge.toString()
  571. : '0'
  572. );
  573. const minSeqLedgerGap = this.minAccountSequenceLedgerGap || 0;
  574. const extraSigners =
  575. this.extraSigners !== null
  576. ? this.extraSigners.map(SignerKey.decodeAddress)
  577. : [];
  578. attrs.cond = xdr.Preconditions.precondV2(
  579. new xdr.PreconditionsV2({
  580. timeBounds,
  581. ledgerBounds,
  582. minSeqNum,
  583. minSeqAge,
  584. minSeqLedgerGap,
  585. extraSigners
  586. })
  587. );
  588. } else {
  589. attrs.cond = xdr.Preconditions.precondTime(timeBounds);
  590. }
  591. attrs.sourceAccount = decodeAddressToMuxedAccount(this.source.accountId());
  592. // TODO - remove this workaround for TransactionExt ts constructor
  593. // and use the typescript generated static factory method once fixed
  594. // https://github.com/stellar/dts-xdr/issues/5
  595. if (this.sorobanData) {
  596. // @ts-ignore
  597. attrs.ext = new xdr.TransactionExt(1, this.sorobanData);
  598. } else {
  599. // @ts-ignore
  600. attrs.ext = new xdr.TransactionExt(0, xdr.Void);
  601. }
  602. const xtx = new xdr.Transaction(attrs);
  603. xtx.operations(this.operations);
  604. const txEnvelope = new xdr.TransactionEnvelope.envelopeTypeTx(
  605. new xdr.TransactionV1Envelope({ tx: xtx })
  606. );
  607. const tx = new Transaction(txEnvelope, this.networkPassphrase);
  608. this.source.incrementSequenceNumber();
  609. return tx;
  610. }
  611. hasV2Preconditions() {
  612. return (
  613. this.ledgerbounds !== null ||
  614. this.minAccountSequence !== null ||
  615. this.minAccountSequenceAge !== null ||
  616. this.minAccountSequenceLedgerGap !== null ||
  617. (this.extraSigners !== null && this.extraSigners.length > 0)
  618. );
  619. }
  620. /**
  621. * Builds a {@link FeeBumpTransaction}, enabling you to resubmit an existing
  622. * transaction with a higher fee.
  623. *
  624. * @param {Keypair|string} feeSource - account paying for the transaction,
  625. * in the form of either a Keypair (only the public key is used) or
  626. * an account ID (in G... or M... form, but refer to `withMuxing`)
  627. * @param {string} baseFee - max fee willing to pay per operation
  628. * in inner transaction (**in stroops**)
  629. * @param {Transaction} innerTx - {@link Transaction} to be bumped by
  630. * the fee bump transaction
  631. * @param {string} networkPassphrase - passphrase of the target
  632. * Stellar network (e.g. "Public Global Stellar Network ; September 2015",
  633. * see {@link Networks})
  634. *
  635. * @todo Alongside the next major version bump, this type signature can be
  636. * changed to be less awkward: accept a MuxedAccount as the `feeSource`
  637. * rather than a keypair or string.
  638. *
  639. * @note Your fee-bump amount should be >= 10x the original fee.
  640. * @see https://developers.stellar.org/docs/glossary/fee-bumps/#replace-by-fee
  641. *
  642. * @returns {FeeBumpTransaction}
  643. */
  644. static buildFeeBumpTransaction(
  645. feeSource,
  646. baseFee,
  647. innerTx,
  648. networkPassphrase
  649. ) {
  650. const innerOps = innerTx.operations.length;
  651. const innerBaseFeeRate = new BigNumber(innerTx.fee).div(innerOps);
  652. const base = new BigNumber(baseFee);
  653. // The fee rate for fee bump is at least the fee rate of the inner transaction
  654. if (base.lt(innerBaseFeeRate)) {
  655. throw new Error(
  656. `Invalid baseFee, it should be at least ${innerBaseFeeRate} stroops.`
  657. );
  658. }
  659. const minBaseFee = new BigNumber(BASE_FEE);
  660. // The fee rate is at least the minimum fee
  661. if (base.lt(minBaseFee)) {
  662. throw new Error(
  663. `Invalid baseFee, it should be at least ${minBaseFee} stroops.`
  664. );
  665. }
  666. let innerTxEnvelope = innerTx.toEnvelope();
  667. if (innerTxEnvelope.switch() === xdr.EnvelopeType.envelopeTypeTxV0()) {
  668. const v0Tx = innerTxEnvelope.v0().tx();
  669. const v1Tx = new xdr.Transaction({
  670. sourceAccount: new xdr.MuxedAccount.keyTypeEd25519(
  671. v0Tx.sourceAccountEd25519()
  672. ),
  673. fee: v0Tx.fee(),
  674. seqNum: v0Tx.seqNum(),
  675. cond: xdr.Preconditions.precondTime(v0Tx.timeBounds()),
  676. memo: v0Tx.memo(),
  677. operations: v0Tx.operations(),
  678. ext: new xdr.TransactionExt(0)
  679. });
  680. innerTxEnvelope = new xdr.TransactionEnvelope.envelopeTypeTx(
  681. new xdr.TransactionV1Envelope({
  682. tx: v1Tx,
  683. signatures: innerTxEnvelope.v0().signatures()
  684. })
  685. );
  686. }
  687. let feeSourceAccount;
  688. if (typeof feeSource === 'string') {
  689. feeSourceAccount = decodeAddressToMuxedAccount(feeSource);
  690. } else {
  691. feeSourceAccount = feeSource.xdrMuxedAccount();
  692. }
  693. const tx = new xdr.FeeBumpTransaction({
  694. feeSource: feeSourceAccount,
  695. fee: xdr.Int64.fromString(base.times(innerOps + 1).toString()),
  696. innerTx: xdr.FeeBumpTransactionInnerTx.envelopeTypeTx(
  697. innerTxEnvelope.v1()
  698. ),
  699. ext: new xdr.FeeBumpTransactionExt(0)
  700. });
  701. const feeBumpTxEnvelope = new xdr.FeeBumpTransactionEnvelope({
  702. tx,
  703. signatures: []
  704. });
  705. const envelope = new xdr.TransactionEnvelope.envelopeTypeTxFeeBump(
  706. feeBumpTxEnvelope
  707. );
  708. return new FeeBumpTransaction(envelope, networkPassphrase);
  709. }
  710. /**
  711. * Build a {@link Transaction} or {@link FeeBumpTransaction} from an
  712. * xdr.TransactionEnvelope.
  713. *
  714. * @param {string|xdr.TransactionEnvelope} envelope - The transaction envelope
  715. * object or base64 encoded string.
  716. * @param {string} networkPassphrase - The network passphrase of the target
  717. * Stellar network (e.g. "Public Global Stellar Network ; September
  718. * 2015"), see {@link Networks}.
  719. *
  720. * @returns {Transaction|FeeBumpTransaction}
  721. */
  722. static fromXDR(envelope, networkPassphrase) {
  723. if (typeof envelope === 'string') {
  724. envelope = xdr.TransactionEnvelope.fromXDR(envelope, 'base64');
  725. }
  726. if (envelope.switch() === xdr.EnvelopeType.envelopeTypeTxFeeBump()) {
  727. return new FeeBumpTransaction(envelope, networkPassphrase);
  728. }
  729. return new Transaction(envelope, networkPassphrase);
  730. }
  731. }
  732. /**
  733. * Checks whether a provided object is a valid Date.
  734. * @argument {Date} d date object
  735. * @returns {boolean}
  736. */
  737. export function isValidDate(d) {
  738. // isnan is okay here because it correctly checks for invalid date objects
  739. // eslint-disable-next-line no-restricted-globals
  740. return d instanceof Date && !isNaN(d);
  741. }