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