@0xsequence/account 1.2.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -126,21 +126,23 @@ class AccountSigner {
126
126
  }
127
127
  throw new Error('No fee option available - not enough balance');
128
128
  }
129
- async sendTransaction(txsPromise) {
129
+ async sendTransaction(txsPromise, options) {
130
130
  var _this$options$stubSig, _this$options2, _this$options$selectF, _this$options3, _this$options4;
131
131
  const txs = isDeferrable(txsPromise) ? await ethers.ethers.utils.resolveProperties(txsPromise) : txsPromise;
132
132
  const prepare = await this.account.prepareTransactions({
133
133
  txs,
134
134
  chainId: this.chainId,
135
- stubSignatureOverrides: (_this$options$stubSig = (_this$options2 = this.options) == null ? void 0 : _this$options2.stubSignatureOverrides) != null ? _this$options$stubSig : new Map()
135
+ stubSignatureOverrides: (_this$options$stubSig = (_this$options2 = this.options) == null ? void 0 : _this$options2.stubSignatureOverrides) != null ? _this$options$stubSig : new Map(),
136
+ simulateForFeeOptions: options == null ? void 0 : options.simulateForFeeOptions
136
137
  });
137
138
  const selectMethod = (_this$options$selectF = (_this$options3 = this.options) == null ? void 0 : _this$options3.selectFee) != null ? _this$options$selectF : this.defaultSelectFee.bind(this);
138
139
  const feeOption = await selectMethod(txs, prepare.feeOptions);
139
140
  const finalTransactions = [...prepare.transactions, ...encodeGasRefundTransaction(feeOption)];
140
141
  return this.account.sendTransaction(finalTransactions, this.chainId, prepare.feeQuote, undefined, undefined, (_this$options4 = this.options) != null && _this$options4.nonceSpace ? {
141
142
  nonceSpace: this.options.nonceSpace
142
- } : undefined);
143
+ } : undefined); // Will always have a transaction response
143
144
  }
145
+
144
146
  getBalance(blockTag) {
145
147
  return this.provider.getBalance(this.account.address, blockTag);
146
148
  }
@@ -409,6 +411,19 @@ class Account {
409
411
  throw new Error(`Wallet ${this.address} is not fully migrated`);
410
412
  }
411
413
  }
414
+ async predecorateSignedTransactions(status, chainId) {
415
+ // Request signed predecorate transactions from child wallets
416
+ const bundles = await this.orchestrator.predecorateSignedTransactions({
417
+ chainId
418
+ });
419
+ // Get signed predecorate transaction
420
+ const predecorated = await this.predecorateTransactions([], status, chainId);
421
+ if (core.commons.transaction.fromTransactionish(this.address, predecorated).length > 0) {
422
+ // Sign it
423
+ bundles.push(await this.signTransactions(predecorated, chainId));
424
+ }
425
+ return bundles;
426
+ }
412
427
  async predecorateTransactions(txs, status, chainId) {
413
428
  // if onchain wallet config is not up to date
414
429
  // then we should append an extra transaction that updates it
@@ -420,34 +435,49 @@ class Account {
420
435
  }
421
436
  return txs;
422
437
  }
423
- decorateTransactions(bundle, status) {
424
- const bootstrapBundle = this.buildBootstrapTransactions(status, bundle.chainId);
425
- if (bootstrapBundle.transactions.length === 0) {
426
- return bundle;
438
+ async decorateTransactions(bundles, status, chainId) {
439
+ var _chainId;
440
+ if (!Array.isArray(bundles)) {
441
+ // Recurse with array
442
+ return this.decorateTransactions([bundles], status, chainId);
427
443
  }
444
+
445
+ // Default to chainId of first bundle when not supplied
446
+ chainId = (_chainId = chainId) != null ? _chainId : bundles[0].chainId;
447
+ const bootstrapBundle = await this.buildBootstrapTransactions(status, chainId);
448
+ const hasBootstrapTxs = bootstrapBundle.transactions.length > 0;
449
+ if (!hasBootstrapTxs && bundles.length === 1) {
450
+ return bundles[0];
451
+ }
452
+
453
+ // Intent defaults to first bundle when no bootstrap transaction
454
+ const {
455
+ entrypoint
456
+ } = hasBootstrapTxs ? bootstrapBundle : bundles[0];
428
457
  const decoratedBundle = {
429
- entrypoint: bootstrapBundle.entrypoint,
430
- chainId: bundle.chainId,
431
- intent: bundle.intent,
432
- transactions: [...bootstrapBundle.transactions, {
458
+ entrypoint,
459
+ chainId,
460
+ // Intent of the first bundle is used
461
+ intent: bundles[0].intent,
462
+ transactions: [...bootstrapBundle.transactions, ...bundles.map(bundle => ({
433
463
  to: bundle.entrypoint,
434
464
  data: core.commons.transaction.encodeBundleExecData(bundle),
435
465
  gasLimit: 0,
436
466
  delegateCall: false,
437
467
  revertOnError: true,
438
468
  value: 0
439
- }]
469
+ }))]
440
470
  };
441
471
 
442
472
  // Re-compute the meta-transaction id to use the guest module subdigest
443
473
  if (!status.onChain.deployed) {
444
- decoratedBundle.intent.id = core.commons.transaction.subdigestOfGuestModuleTransactions(this.contexts[this.version].guestModule, decoratedBundle.chainId, decoratedBundle.transactions);
474
+ decoratedBundle.intent.id = core.commons.transaction.subdigestOfGuestModuleTransactions(this.contexts[this.version].guestModule, chainId, decoratedBundle.transactions);
445
475
  }
446
476
  return decoratedBundle;
447
477
  }
448
- decorateSignature(signature, status) {
478
+ async decorateSignature(signature, status) {
449
479
  if (!status.presignedConfigurations || status.presignedConfigurations.length === 0) {
450
- return new Promise(resolve => resolve(signature));
480
+ return signature;
451
481
  }
452
482
  const coder = this.coders.signature;
453
483
  const chain = status.presignedConfigurations.map(c => c.signature);
@@ -466,14 +496,13 @@ class Account {
466
496
  signatures
467
497
  });
468
498
  }
469
- async signDigest(digest, chainId, decorate = true, cantValidateBehavior = 'ignore') {
499
+ async signDigest(digest, chainId, decorate = true, cantValidateBehavior = 'ignore', metadata) {
470
500
  // If we are signing a digest for chainId zero then we can never be fully migrated
471
501
  // because Sequence v1 doesn't allow for signing a message on "all chains"
472
502
 
473
503
  // So we ignore the state on "chain zero" and instead use one of the states of the networks
474
504
  // wallet-webapp should ensure the wallet is as migrated as possible, trying to mimic
475
505
  // the behaviour of being migrated on all chains
476
-
477
506
  const chainRef = ethers.ethers.constants.Zero.eq(chainId) ? this.networks[0].chainId : chainId;
478
507
  const status = await this.status(chainRef);
479
508
  this.mustBeFullyMigrated(status);
@@ -484,7 +513,7 @@ class Account {
484
513
  throw new Error('Wallet cannot validate onchain');
485
514
  }
486
515
  const wallet = this.walletForStatus(chainId, status);
487
- const signature = await wallet.signDigest(digest);
516
+ const signature = await wallet.signDigest(digest, metadata);
488
517
  const decorated = decorate ? this.decorateSignature(signature, status) : signature;
489
518
 
490
519
  // If the wallet can't validate onchain then we
@@ -503,8 +532,8 @@ class Account {
503
532
  }
504
533
  return decorated;
505
534
  }
506
- buildEIP6492Signature(signature, status, chainId) {
507
- const bootstrapBundle = this.buildBootstrapTransactions(status, chainId);
535
+ async buildEIP6492Signature(signature, status, chainId) {
536
+ const bootstrapBundle = await this.buildBootstrapTransactions(status, chainId);
508
537
  if (bootstrapBundle.transactions.length === 0) {
509
538
  throw new Error('Cannot build EIP-6492 signature without bootstrap transactions');
510
539
  }
@@ -560,8 +589,12 @@ class Account {
560
589
  * by any of the migrations.
561
590
  *
562
591
  */
563
- buildBootstrapTransactions(status, chainId) {
564
- const transactions = [];
592
+ async buildBootstrapTransactions(status, chainId) {
593
+ var _bundle$transactions;
594
+ const bundle = await this.orchestrator.buildDeployTransaction({
595
+ chainId
596
+ });
597
+ const transactions = (_bundle$transactions = bundle == null ? void 0 : bundle.transactions) != null ? _bundle$transactions : [];
565
598
 
566
599
  // Add wallet deployment if needed
567
600
  if (!status.onChain.deployed) {
@@ -570,6 +603,7 @@ class Account {
570
603
  const deployTransaction = wallet.Wallet.buildDeployTransaction(status.original.context, status.original.imageHash);
571
604
  transactions.push(...deployTransaction.transactions);
572
605
  }
606
+ transactions.length;
573
607
 
574
608
  // Get pending migrations
575
609
  transactions.push(...status.signedMigrations.map(m => ({
@@ -619,9 +653,20 @@ class Account {
619
653
  const status = pstatus || (await this.status(chainId));
620
654
  this.mustBeFullyMigrated(status);
621
655
  const wallet = this.walletForStatus(chainId, status);
656
+ const metadata = {
657
+ address: this.address,
658
+ digest: '',
659
+ // Set in wallet.signTransactions
660
+ chainId,
661
+ config: {
662
+ version: this.version
663
+ },
664
+ decorate: true,
665
+ cantValidateBehavior: 'ignore'
666
+ };
622
667
  const signed = await wallet.signTransactions(txs, (options == null ? void 0 : options.nonceSpace) && {
623
668
  space: options == null ? void 0 : options.nonceSpace
624
- });
669
+ }, metadata);
625
670
  return _extends({}, signed, {
626
671
  signature: await this.decorateSignature(signed.signature, status)
627
672
  });
@@ -700,9 +745,12 @@ class Account {
700
745
  };
701
746
  }
702
747
  async sendSignedTransactions(signedBundle, chainId, quote, pstatus, callback) {
703
- const status = pstatus || (await this.status(signedBundle.chainId));
748
+ if (!Array.isArray(signedBundle)) {
749
+ return this.sendSignedTransactions([signedBundle], chainId, quote, pstatus, callback);
750
+ }
751
+ const status = pstatus || (await this.status(signedBundle[0].chainId));
704
752
  this.mustBeFullyMigrated(status);
705
- const decoratedBundle = this.decorateTransactions(signedBundle, status);
753
+ const decoratedBundle = await this.decorateTransactions(signedBundle, status);
706
754
  callback == null ? void 0 : callback(decoratedBundle);
707
755
  return this.relayer(chainId).relay(decoratedBundle, quote);
708
756
  }
@@ -710,7 +758,7 @@ class Account {
710
758
  const wallet = this.walletForStatus(chainId, status || (await this.status(chainId)));
711
759
  return wallet.fillGasLimits(txs);
712
760
  }
713
- async gasRefundQuotes(txs, chainId, stubSignatureOverrides, status) {
761
+ async gasRefundQuotes(txs, chainId, stubSignatureOverrides, status, options) {
714
762
  const wstatus = status || (await this.status(chainId));
715
763
  const wallet = this.walletForStatus(chainId, wstatus);
716
764
  const predecorated = await this.predecorateTransactions(txs, wstatus, chainId);
@@ -735,9 +783,9 @@ class Account {
735
783
  nonce: 0 // The relayer also ignored the nonce
736
784
  };
737
785
 
738
- const decoratedBundle = this.decorateTransactions(signedBundle, wstatus);
786
+ const decoratedBundle = await this.decorateTransactions(signedBundle, wstatus);
739
787
  const data = core.commons.transaction.encodeBundleExecData(decoratedBundle);
740
- const res = await this.relayer(chainId).getFeeOptionsRaw(decoratedBundle.entrypoint, data);
788
+ const res = await this.relayer(chainId).getFeeOptionsRaw(decoratedBundle.entrypoint, data, options);
741
789
  return _extends({}, res, {
742
790
  decorated: decoratedBundle
743
791
  });
@@ -745,7 +793,9 @@ class Account {
745
793
  async prepareTransactions(args) {
746
794
  const status = await this.status(args.chainId);
747
795
  const transactions = await this.fillGasLimits(args.txs, args.chainId, status);
748
- const gasRefundQuote = await this.gasRefundQuotes(transactions, args.chainId, args.stubSignatureOverrides, status);
796
+ const gasRefundQuote = await this.gasRefundQuotes(transactions, args.chainId, args.stubSignatureOverrides, status, {
797
+ simulate: args.simulateForFeeOptions
798
+ });
749
799
  const flatDecorated = core.commons.transaction.unwind(this.address, gasRefundQuote.decorated.transactions);
750
800
  return {
751
801
  transactions,
@@ -757,9 +807,21 @@ class Account {
757
807
  async sendTransaction(txs, chainId, quote, skipPreDecorate = false, callback, options) {
758
808
  const status = await this.status(chainId);
759
809
  const predecorated = skipPreDecorate ? txs : await this.predecorateTransactions(txs, status, chainId);
760
- const signed = await this.signTransactions(predecorated, chainId, undefined, options);
761
- // TODO: is it safe to pass status again here?
762
- return this.sendSignedTransactions(signed, chainId, quote, undefined, callback);
810
+ const hasTxs = core.commons.transaction.fromTransactionish(this.address, predecorated).length > 0;
811
+ const signed = hasTxs ? await this.signTransactions(predecorated, chainId, undefined, options) : undefined;
812
+ const childBundles = await this.orchestrator.predecorateSignedTransactions({
813
+ chainId
814
+ });
815
+ const bundles = [];
816
+ if (signed !== undefined && signed.transactions.length > 0) {
817
+ bundles.push(signed);
818
+ }
819
+ bundles.push(...childBundles.filter(b => b.transactions.length > 0));
820
+ if (bundles.length === 0) {
821
+ // Nothing to send?
822
+ return;
823
+ }
824
+ return this.sendSignedTransactions(bundles, chainId, quote, undefined, callback);
763
825
  }
764
826
  async signTypedData(domain, types, message, chainId, cantValidateBehavior = 'ignore') {
765
827
  const digest = utils.encodeTypedDataDigest({
@@ -126,21 +126,23 @@ class AccountSigner {
126
126
  }
127
127
  throw new Error('No fee option available - not enough balance');
128
128
  }
129
- async sendTransaction(txsPromise) {
129
+ async sendTransaction(txsPromise, options) {
130
130
  var _this$options$stubSig, _this$options2, _this$options$selectF, _this$options3, _this$options4;
131
131
  const txs = isDeferrable(txsPromise) ? await ethers.ethers.utils.resolveProperties(txsPromise) : txsPromise;
132
132
  const prepare = await this.account.prepareTransactions({
133
133
  txs,
134
134
  chainId: this.chainId,
135
- stubSignatureOverrides: (_this$options$stubSig = (_this$options2 = this.options) == null ? void 0 : _this$options2.stubSignatureOverrides) != null ? _this$options$stubSig : new Map()
135
+ stubSignatureOverrides: (_this$options$stubSig = (_this$options2 = this.options) == null ? void 0 : _this$options2.stubSignatureOverrides) != null ? _this$options$stubSig : new Map(),
136
+ simulateForFeeOptions: options == null ? void 0 : options.simulateForFeeOptions
136
137
  });
137
138
  const selectMethod = (_this$options$selectF = (_this$options3 = this.options) == null ? void 0 : _this$options3.selectFee) != null ? _this$options$selectF : this.defaultSelectFee.bind(this);
138
139
  const feeOption = await selectMethod(txs, prepare.feeOptions);
139
140
  const finalTransactions = [...prepare.transactions, ...encodeGasRefundTransaction(feeOption)];
140
141
  return this.account.sendTransaction(finalTransactions, this.chainId, prepare.feeQuote, undefined, undefined, (_this$options4 = this.options) != null && _this$options4.nonceSpace ? {
141
142
  nonceSpace: this.options.nonceSpace
142
- } : undefined);
143
+ } : undefined); // Will always have a transaction response
143
144
  }
145
+
144
146
  getBalance(blockTag) {
145
147
  return this.provider.getBalance(this.account.address, blockTag);
146
148
  }
@@ -409,6 +411,19 @@ class Account {
409
411
  throw new Error(`Wallet ${this.address} is not fully migrated`);
410
412
  }
411
413
  }
414
+ async predecorateSignedTransactions(status, chainId) {
415
+ // Request signed predecorate transactions from child wallets
416
+ const bundles = await this.orchestrator.predecorateSignedTransactions({
417
+ chainId
418
+ });
419
+ // Get signed predecorate transaction
420
+ const predecorated = await this.predecorateTransactions([], status, chainId);
421
+ if (core.commons.transaction.fromTransactionish(this.address, predecorated).length > 0) {
422
+ // Sign it
423
+ bundles.push(await this.signTransactions(predecorated, chainId));
424
+ }
425
+ return bundles;
426
+ }
412
427
  async predecorateTransactions(txs, status, chainId) {
413
428
  // if onchain wallet config is not up to date
414
429
  // then we should append an extra transaction that updates it
@@ -420,34 +435,49 @@ class Account {
420
435
  }
421
436
  return txs;
422
437
  }
423
- decorateTransactions(bundle, status) {
424
- const bootstrapBundle = this.buildBootstrapTransactions(status, bundle.chainId);
425
- if (bootstrapBundle.transactions.length === 0) {
426
- return bundle;
438
+ async decorateTransactions(bundles, status, chainId) {
439
+ var _chainId;
440
+ if (!Array.isArray(bundles)) {
441
+ // Recurse with array
442
+ return this.decorateTransactions([bundles], status, chainId);
427
443
  }
444
+
445
+ // Default to chainId of first bundle when not supplied
446
+ chainId = (_chainId = chainId) != null ? _chainId : bundles[0].chainId;
447
+ const bootstrapBundle = await this.buildBootstrapTransactions(status, chainId);
448
+ const hasBootstrapTxs = bootstrapBundle.transactions.length > 0;
449
+ if (!hasBootstrapTxs && bundles.length === 1) {
450
+ return bundles[0];
451
+ }
452
+
453
+ // Intent defaults to first bundle when no bootstrap transaction
454
+ const {
455
+ entrypoint
456
+ } = hasBootstrapTxs ? bootstrapBundle : bundles[0];
428
457
  const decoratedBundle = {
429
- entrypoint: bootstrapBundle.entrypoint,
430
- chainId: bundle.chainId,
431
- intent: bundle.intent,
432
- transactions: [...bootstrapBundle.transactions, {
458
+ entrypoint,
459
+ chainId,
460
+ // Intent of the first bundle is used
461
+ intent: bundles[0].intent,
462
+ transactions: [...bootstrapBundle.transactions, ...bundles.map(bundle => ({
433
463
  to: bundle.entrypoint,
434
464
  data: core.commons.transaction.encodeBundleExecData(bundle),
435
465
  gasLimit: 0,
436
466
  delegateCall: false,
437
467
  revertOnError: true,
438
468
  value: 0
439
- }]
469
+ }))]
440
470
  };
441
471
 
442
472
  // Re-compute the meta-transaction id to use the guest module subdigest
443
473
  if (!status.onChain.deployed) {
444
- decoratedBundle.intent.id = core.commons.transaction.subdigestOfGuestModuleTransactions(this.contexts[this.version].guestModule, decoratedBundle.chainId, decoratedBundle.transactions);
474
+ decoratedBundle.intent.id = core.commons.transaction.subdigestOfGuestModuleTransactions(this.contexts[this.version].guestModule, chainId, decoratedBundle.transactions);
445
475
  }
446
476
  return decoratedBundle;
447
477
  }
448
- decorateSignature(signature, status) {
478
+ async decorateSignature(signature, status) {
449
479
  if (!status.presignedConfigurations || status.presignedConfigurations.length === 0) {
450
- return new Promise(resolve => resolve(signature));
480
+ return signature;
451
481
  }
452
482
  const coder = this.coders.signature;
453
483
  const chain = status.presignedConfigurations.map(c => c.signature);
@@ -466,14 +496,13 @@ class Account {
466
496
  signatures
467
497
  });
468
498
  }
469
- async signDigest(digest, chainId, decorate = true, cantValidateBehavior = 'ignore') {
499
+ async signDigest(digest, chainId, decorate = true, cantValidateBehavior = 'ignore', metadata) {
470
500
  // If we are signing a digest for chainId zero then we can never be fully migrated
471
501
  // because Sequence v1 doesn't allow for signing a message on "all chains"
472
502
 
473
503
  // So we ignore the state on "chain zero" and instead use one of the states of the networks
474
504
  // wallet-webapp should ensure the wallet is as migrated as possible, trying to mimic
475
505
  // the behaviour of being migrated on all chains
476
-
477
506
  const chainRef = ethers.ethers.constants.Zero.eq(chainId) ? this.networks[0].chainId : chainId;
478
507
  const status = await this.status(chainRef);
479
508
  this.mustBeFullyMigrated(status);
@@ -484,7 +513,7 @@ class Account {
484
513
  throw new Error('Wallet cannot validate onchain');
485
514
  }
486
515
  const wallet = this.walletForStatus(chainId, status);
487
- const signature = await wallet.signDigest(digest);
516
+ const signature = await wallet.signDigest(digest, metadata);
488
517
  const decorated = decorate ? this.decorateSignature(signature, status) : signature;
489
518
 
490
519
  // If the wallet can't validate onchain then we
@@ -503,8 +532,8 @@ class Account {
503
532
  }
504
533
  return decorated;
505
534
  }
506
- buildEIP6492Signature(signature, status, chainId) {
507
- const bootstrapBundle = this.buildBootstrapTransactions(status, chainId);
535
+ async buildEIP6492Signature(signature, status, chainId) {
536
+ const bootstrapBundle = await this.buildBootstrapTransactions(status, chainId);
508
537
  if (bootstrapBundle.transactions.length === 0) {
509
538
  throw new Error('Cannot build EIP-6492 signature without bootstrap transactions');
510
539
  }
@@ -560,8 +589,12 @@ class Account {
560
589
  * by any of the migrations.
561
590
  *
562
591
  */
563
- buildBootstrapTransactions(status, chainId) {
564
- const transactions = [];
592
+ async buildBootstrapTransactions(status, chainId) {
593
+ var _bundle$transactions;
594
+ const bundle = await this.orchestrator.buildDeployTransaction({
595
+ chainId
596
+ });
597
+ const transactions = (_bundle$transactions = bundle == null ? void 0 : bundle.transactions) != null ? _bundle$transactions : [];
565
598
 
566
599
  // Add wallet deployment if needed
567
600
  if (!status.onChain.deployed) {
@@ -570,6 +603,7 @@ class Account {
570
603
  const deployTransaction = wallet.Wallet.buildDeployTransaction(status.original.context, status.original.imageHash);
571
604
  transactions.push(...deployTransaction.transactions);
572
605
  }
606
+ transactions.length;
573
607
 
574
608
  // Get pending migrations
575
609
  transactions.push(...status.signedMigrations.map(m => ({
@@ -619,9 +653,20 @@ class Account {
619
653
  const status = pstatus || (await this.status(chainId));
620
654
  this.mustBeFullyMigrated(status);
621
655
  const wallet = this.walletForStatus(chainId, status);
656
+ const metadata = {
657
+ address: this.address,
658
+ digest: '',
659
+ // Set in wallet.signTransactions
660
+ chainId,
661
+ config: {
662
+ version: this.version
663
+ },
664
+ decorate: true,
665
+ cantValidateBehavior: 'ignore'
666
+ };
622
667
  const signed = await wallet.signTransactions(txs, (options == null ? void 0 : options.nonceSpace) && {
623
668
  space: options == null ? void 0 : options.nonceSpace
624
- });
669
+ }, metadata);
625
670
  return _extends({}, signed, {
626
671
  signature: await this.decorateSignature(signed.signature, status)
627
672
  });
@@ -700,9 +745,12 @@ class Account {
700
745
  };
701
746
  }
702
747
  async sendSignedTransactions(signedBundle, chainId, quote, pstatus, callback) {
703
- const status = pstatus || (await this.status(signedBundle.chainId));
748
+ if (!Array.isArray(signedBundle)) {
749
+ return this.sendSignedTransactions([signedBundle], chainId, quote, pstatus, callback);
750
+ }
751
+ const status = pstatus || (await this.status(signedBundle[0].chainId));
704
752
  this.mustBeFullyMigrated(status);
705
- const decoratedBundle = this.decorateTransactions(signedBundle, status);
753
+ const decoratedBundle = await this.decorateTransactions(signedBundle, status);
706
754
  callback == null ? void 0 : callback(decoratedBundle);
707
755
  return this.relayer(chainId).relay(decoratedBundle, quote);
708
756
  }
@@ -710,7 +758,7 @@ class Account {
710
758
  const wallet = this.walletForStatus(chainId, status || (await this.status(chainId)));
711
759
  return wallet.fillGasLimits(txs);
712
760
  }
713
- async gasRefundQuotes(txs, chainId, stubSignatureOverrides, status) {
761
+ async gasRefundQuotes(txs, chainId, stubSignatureOverrides, status, options) {
714
762
  const wstatus = status || (await this.status(chainId));
715
763
  const wallet = this.walletForStatus(chainId, wstatus);
716
764
  const predecorated = await this.predecorateTransactions(txs, wstatus, chainId);
@@ -735,9 +783,9 @@ class Account {
735
783
  nonce: 0 // The relayer also ignored the nonce
736
784
  };
737
785
 
738
- const decoratedBundle = this.decorateTransactions(signedBundle, wstatus);
786
+ const decoratedBundle = await this.decorateTransactions(signedBundle, wstatus);
739
787
  const data = core.commons.transaction.encodeBundleExecData(decoratedBundle);
740
- const res = await this.relayer(chainId).getFeeOptionsRaw(decoratedBundle.entrypoint, data);
788
+ const res = await this.relayer(chainId).getFeeOptionsRaw(decoratedBundle.entrypoint, data, options);
741
789
  return _extends({}, res, {
742
790
  decorated: decoratedBundle
743
791
  });
@@ -745,7 +793,9 @@ class Account {
745
793
  async prepareTransactions(args) {
746
794
  const status = await this.status(args.chainId);
747
795
  const transactions = await this.fillGasLimits(args.txs, args.chainId, status);
748
- const gasRefundQuote = await this.gasRefundQuotes(transactions, args.chainId, args.stubSignatureOverrides, status);
796
+ const gasRefundQuote = await this.gasRefundQuotes(transactions, args.chainId, args.stubSignatureOverrides, status, {
797
+ simulate: args.simulateForFeeOptions
798
+ });
749
799
  const flatDecorated = core.commons.transaction.unwind(this.address, gasRefundQuote.decorated.transactions);
750
800
  return {
751
801
  transactions,
@@ -757,9 +807,21 @@ class Account {
757
807
  async sendTransaction(txs, chainId, quote, skipPreDecorate = false, callback, options) {
758
808
  const status = await this.status(chainId);
759
809
  const predecorated = skipPreDecorate ? txs : await this.predecorateTransactions(txs, status, chainId);
760
- const signed = await this.signTransactions(predecorated, chainId, undefined, options);
761
- // TODO: is it safe to pass status again here?
762
- return this.sendSignedTransactions(signed, chainId, quote, undefined, callback);
810
+ const hasTxs = core.commons.transaction.fromTransactionish(this.address, predecorated).length > 0;
811
+ const signed = hasTxs ? await this.signTransactions(predecorated, chainId, undefined, options) : undefined;
812
+ const childBundles = await this.orchestrator.predecorateSignedTransactions({
813
+ chainId
814
+ });
815
+ const bundles = [];
816
+ if (signed !== undefined && signed.transactions.length > 0) {
817
+ bundles.push(signed);
818
+ }
819
+ bundles.push(...childBundles.filter(b => b.transactions.length > 0));
820
+ if (bundles.length === 0) {
821
+ // Nothing to send?
822
+ return;
823
+ }
824
+ return this.sendSignedTransactions(bundles, chainId, quote, undefined, callback);
763
825
  }
764
826
  async signTypedData(domain, types, message, chainId, cantValidateBehavior = 'ignore') {
765
827
  const digest = utils.encodeTypedDataDigest({
@@ -122,21 +122,23 @@ class AccountSigner {
122
122
  }
123
123
  throw new Error('No fee option available - not enough balance');
124
124
  }
125
- async sendTransaction(txsPromise) {
125
+ async sendTransaction(txsPromise, options) {
126
126
  var _this$options$stubSig, _this$options2, _this$options$selectF, _this$options3, _this$options4;
127
127
  const txs = isDeferrable(txsPromise) ? await ethers.utils.resolveProperties(txsPromise) : txsPromise;
128
128
  const prepare = await this.account.prepareTransactions({
129
129
  txs,
130
130
  chainId: this.chainId,
131
- stubSignatureOverrides: (_this$options$stubSig = (_this$options2 = this.options) == null ? void 0 : _this$options2.stubSignatureOverrides) != null ? _this$options$stubSig : new Map()
131
+ stubSignatureOverrides: (_this$options$stubSig = (_this$options2 = this.options) == null ? void 0 : _this$options2.stubSignatureOverrides) != null ? _this$options$stubSig : new Map(),
132
+ simulateForFeeOptions: options == null ? void 0 : options.simulateForFeeOptions
132
133
  });
133
134
  const selectMethod = (_this$options$selectF = (_this$options3 = this.options) == null ? void 0 : _this$options3.selectFee) != null ? _this$options$selectF : this.defaultSelectFee.bind(this);
134
135
  const feeOption = await selectMethod(txs, prepare.feeOptions);
135
136
  const finalTransactions = [...prepare.transactions, ...encodeGasRefundTransaction(feeOption)];
136
137
  return this.account.sendTransaction(finalTransactions, this.chainId, prepare.feeQuote, undefined, undefined, (_this$options4 = this.options) != null && _this$options4.nonceSpace ? {
137
138
  nonceSpace: this.options.nonceSpace
138
- } : undefined);
139
+ } : undefined); // Will always have a transaction response
139
140
  }
141
+
140
142
  getBalance(blockTag) {
141
143
  return this.provider.getBalance(this.account.address, blockTag);
142
144
  }
@@ -405,6 +407,19 @@ class Account {
405
407
  throw new Error(`Wallet ${this.address} is not fully migrated`);
406
408
  }
407
409
  }
410
+ async predecorateSignedTransactions(status, chainId) {
411
+ // Request signed predecorate transactions from child wallets
412
+ const bundles = await this.orchestrator.predecorateSignedTransactions({
413
+ chainId
414
+ });
415
+ // Get signed predecorate transaction
416
+ const predecorated = await this.predecorateTransactions([], status, chainId);
417
+ if (commons.transaction.fromTransactionish(this.address, predecorated).length > 0) {
418
+ // Sign it
419
+ bundles.push(await this.signTransactions(predecorated, chainId));
420
+ }
421
+ return bundles;
422
+ }
408
423
  async predecorateTransactions(txs, status, chainId) {
409
424
  // if onchain wallet config is not up to date
410
425
  // then we should append an extra transaction that updates it
@@ -416,34 +431,49 @@ class Account {
416
431
  }
417
432
  return txs;
418
433
  }
419
- decorateTransactions(bundle, status) {
420
- const bootstrapBundle = this.buildBootstrapTransactions(status, bundle.chainId);
421
- if (bootstrapBundle.transactions.length === 0) {
422
- return bundle;
434
+ async decorateTransactions(bundles, status, chainId) {
435
+ var _chainId;
436
+ if (!Array.isArray(bundles)) {
437
+ // Recurse with array
438
+ return this.decorateTransactions([bundles], status, chainId);
423
439
  }
440
+
441
+ // Default to chainId of first bundle when not supplied
442
+ chainId = (_chainId = chainId) != null ? _chainId : bundles[0].chainId;
443
+ const bootstrapBundle = await this.buildBootstrapTransactions(status, chainId);
444
+ const hasBootstrapTxs = bootstrapBundle.transactions.length > 0;
445
+ if (!hasBootstrapTxs && bundles.length === 1) {
446
+ return bundles[0];
447
+ }
448
+
449
+ // Intent defaults to first bundle when no bootstrap transaction
450
+ const {
451
+ entrypoint
452
+ } = hasBootstrapTxs ? bootstrapBundle : bundles[0];
424
453
  const decoratedBundle = {
425
- entrypoint: bootstrapBundle.entrypoint,
426
- chainId: bundle.chainId,
427
- intent: bundle.intent,
428
- transactions: [...bootstrapBundle.transactions, {
454
+ entrypoint,
455
+ chainId,
456
+ // Intent of the first bundle is used
457
+ intent: bundles[0].intent,
458
+ transactions: [...bootstrapBundle.transactions, ...bundles.map(bundle => ({
429
459
  to: bundle.entrypoint,
430
460
  data: commons.transaction.encodeBundleExecData(bundle),
431
461
  gasLimit: 0,
432
462
  delegateCall: false,
433
463
  revertOnError: true,
434
464
  value: 0
435
- }]
465
+ }))]
436
466
  };
437
467
 
438
468
  // Re-compute the meta-transaction id to use the guest module subdigest
439
469
  if (!status.onChain.deployed) {
440
- decoratedBundle.intent.id = commons.transaction.subdigestOfGuestModuleTransactions(this.contexts[this.version].guestModule, decoratedBundle.chainId, decoratedBundle.transactions);
470
+ decoratedBundle.intent.id = commons.transaction.subdigestOfGuestModuleTransactions(this.contexts[this.version].guestModule, chainId, decoratedBundle.transactions);
441
471
  }
442
472
  return decoratedBundle;
443
473
  }
444
- decorateSignature(signature, status) {
474
+ async decorateSignature(signature, status) {
445
475
  if (!status.presignedConfigurations || status.presignedConfigurations.length === 0) {
446
- return new Promise(resolve => resolve(signature));
476
+ return signature;
447
477
  }
448
478
  const coder = this.coders.signature;
449
479
  const chain = status.presignedConfigurations.map(c => c.signature);
@@ -462,14 +492,13 @@ class Account {
462
492
  signatures
463
493
  });
464
494
  }
465
- async signDigest(digest, chainId, decorate = true, cantValidateBehavior = 'ignore') {
495
+ async signDigest(digest, chainId, decorate = true, cantValidateBehavior = 'ignore', metadata) {
466
496
  // If we are signing a digest for chainId zero then we can never be fully migrated
467
497
  // because Sequence v1 doesn't allow for signing a message on "all chains"
468
498
 
469
499
  // So we ignore the state on "chain zero" and instead use one of the states of the networks
470
500
  // wallet-webapp should ensure the wallet is as migrated as possible, trying to mimic
471
501
  // the behaviour of being migrated on all chains
472
-
473
502
  const chainRef = ethers.constants.Zero.eq(chainId) ? this.networks[0].chainId : chainId;
474
503
  const status = await this.status(chainRef);
475
504
  this.mustBeFullyMigrated(status);
@@ -480,7 +509,7 @@ class Account {
480
509
  throw new Error('Wallet cannot validate onchain');
481
510
  }
482
511
  const wallet = this.walletForStatus(chainId, status);
483
- const signature = await wallet.signDigest(digest);
512
+ const signature = await wallet.signDigest(digest, metadata);
484
513
  const decorated = decorate ? this.decorateSignature(signature, status) : signature;
485
514
 
486
515
  // If the wallet can't validate onchain then we
@@ -499,8 +528,8 @@ class Account {
499
528
  }
500
529
  return decorated;
501
530
  }
502
- buildEIP6492Signature(signature, status, chainId) {
503
- const bootstrapBundle = this.buildBootstrapTransactions(status, chainId);
531
+ async buildEIP6492Signature(signature, status, chainId) {
532
+ const bootstrapBundle = await this.buildBootstrapTransactions(status, chainId);
504
533
  if (bootstrapBundle.transactions.length === 0) {
505
534
  throw new Error('Cannot build EIP-6492 signature without bootstrap transactions');
506
535
  }
@@ -556,8 +585,12 @@ class Account {
556
585
  * by any of the migrations.
557
586
  *
558
587
  */
559
- buildBootstrapTransactions(status, chainId) {
560
- const transactions = [];
588
+ async buildBootstrapTransactions(status, chainId) {
589
+ var _bundle$transactions;
590
+ const bundle = await this.orchestrator.buildDeployTransaction({
591
+ chainId
592
+ });
593
+ const transactions = (_bundle$transactions = bundle == null ? void 0 : bundle.transactions) != null ? _bundle$transactions : [];
561
594
 
562
595
  // Add wallet deployment if needed
563
596
  if (!status.onChain.deployed) {
@@ -566,6 +599,7 @@ class Account {
566
599
  const deployTransaction = Wallet.buildDeployTransaction(status.original.context, status.original.imageHash);
567
600
  transactions.push(...deployTransaction.transactions);
568
601
  }
602
+ transactions.length;
569
603
 
570
604
  // Get pending migrations
571
605
  transactions.push(...status.signedMigrations.map(m => ({
@@ -615,9 +649,20 @@ class Account {
615
649
  const status = pstatus || (await this.status(chainId));
616
650
  this.mustBeFullyMigrated(status);
617
651
  const wallet = this.walletForStatus(chainId, status);
652
+ const metadata = {
653
+ address: this.address,
654
+ digest: '',
655
+ // Set in wallet.signTransactions
656
+ chainId,
657
+ config: {
658
+ version: this.version
659
+ },
660
+ decorate: true,
661
+ cantValidateBehavior: 'ignore'
662
+ };
618
663
  const signed = await wallet.signTransactions(txs, (options == null ? void 0 : options.nonceSpace) && {
619
664
  space: options == null ? void 0 : options.nonceSpace
620
- });
665
+ }, metadata);
621
666
  return _extends({}, signed, {
622
667
  signature: await this.decorateSignature(signed.signature, status)
623
668
  });
@@ -696,9 +741,12 @@ class Account {
696
741
  };
697
742
  }
698
743
  async sendSignedTransactions(signedBundle, chainId, quote, pstatus, callback) {
699
- const status = pstatus || (await this.status(signedBundle.chainId));
744
+ if (!Array.isArray(signedBundle)) {
745
+ return this.sendSignedTransactions([signedBundle], chainId, quote, pstatus, callback);
746
+ }
747
+ const status = pstatus || (await this.status(signedBundle[0].chainId));
700
748
  this.mustBeFullyMigrated(status);
701
- const decoratedBundle = this.decorateTransactions(signedBundle, status);
749
+ const decoratedBundle = await this.decorateTransactions(signedBundle, status);
702
750
  callback == null ? void 0 : callback(decoratedBundle);
703
751
  return this.relayer(chainId).relay(decoratedBundle, quote);
704
752
  }
@@ -706,7 +754,7 @@ class Account {
706
754
  const wallet = this.walletForStatus(chainId, status || (await this.status(chainId)));
707
755
  return wallet.fillGasLimits(txs);
708
756
  }
709
- async gasRefundQuotes(txs, chainId, stubSignatureOverrides, status) {
757
+ async gasRefundQuotes(txs, chainId, stubSignatureOverrides, status, options) {
710
758
  const wstatus = status || (await this.status(chainId));
711
759
  const wallet = this.walletForStatus(chainId, wstatus);
712
760
  const predecorated = await this.predecorateTransactions(txs, wstatus, chainId);
@@ -731,9 +779,9 @@ class Account {
731
779
  nonce: 0 // The relayer also ignored the nonce
732
780
  };
733
781
 
734
- const decoratedBundle = this.decorateTransactions(signedBundle, wstatus);
782
+ const decoratedBundle = await this.decorateTransactions(signedBundle, wstatus);
735
783
  const data = commons.transaction.encodeBundleExecData(decoratedBundle);
736
- const res = await this.relayer(chainId).getFeeOptionsRaw(decoratedBundle.entrypoint, data);
784
+ const res = await this.relayer(chainId).getFeeOptionsRaw(decoratedBundle.entrypoint, data, options);
737
785
  return _extends({}, res, {
738
786
  decorated: decoratedBundle
739
787
  });
@@ -741,7 +789,9 @@ class Account {
741
789
  async prepareTransactions(args) {
742
790
  const status = await this.status(args.chainId);
743
791
  const transactions = await this.fillGasLimits(args.txs, args.chainId, status);
744
- const gasRefundQuote = await this.gasRefundQuotes(transactions, args.chainId, args.stubSignatureOverrides, status);
792
+ const gasRefundQuote = await this.gasRefundQuotes(transactions, args.chainId, args.stubSignatureOverrides, status, {
793
+ simulate: args.simulateForFeeOptions
794
+ });
745
795
  const flatDecorated = commons.transaction.unwind(this.address, gasRefundQuote.decorated.transactions);
746
796
  return {
747
797
  transactions,
@@ -753,9 +803,21 @@ class Account {
753
803
  async sendTransaction(txs, chainId, quote, skipPreDecorate = false, callback, options) {
754
804
  const status = await this.status(chainId);
755
805
  const predecorated = skipPreDecorate ? txs : await this.predecorateTransactions(txs, status, chainId);
756
- const signed = await this.signTransactions(predecorated, chainId, undefined, options);
757
- // TODO: is it safe to pass status again here?
758
- return this.sendSignedTransactions(signed, chainId, quote, undefined, callback);
806
+ const hasTxs = commons.transaction.fromTransactionish(this.address, predecorated).length > 0;
807
+ const signed = hasTxs ? await this.signTransactions(predecorated, chainId, undefined, options) : undefined;
808
+ const childBundles = await this.orchestrator.predecorateSignedTransactions({
809
+ chainId
810
+ });
811
+ const bundles = [];
812
+ if (signed !== undefined && signed.transactions.length > 0) {
813
+ bundles.push(signed);
814
+ }
815
+ bundles.push(...childBundles.filter(b => b.transactions.length > 0));
816
+ if (bundles.length === 0) {
817
+ // Nothing to send?
818
+ return;
819
+ }
820
+ return this.sendSignedTransactions(bundles, chainId, quote, undefined, callback);
759
821
  }
760
822
  async signTypedData(domain, types, message, chainId, cantValidateBehavior = 'ignore') {
761
823
  const digest = encodeTypedDataDigest({
@@ -76,11 +76,12 @@ export declare class Account {
76
76
  walletFor(chainId: ethers.BigNumberish, context: commons.context.WalletContext, config: commons.config.Config, coders: typeof this.coders): Wallet;
77
77
  status(chainId: ethers.BigNumberish, longestPath?: boolean): Promise<AccountStatus>;
78
78
  private mustBeFullyMigrated;
79
+ predecorateSignedTransactions(status: AccountStatus, chainId: ethers.BigNumberish): Promise<commons.transaction.SignedTransactionBundle[]>;
79
80
  predecorateTransactions(txs: commons.transaction.Transactionish, status: AccountStatus, chainId: ethers.BigNumberish): Promise<commons.transaction.Transactionish>;
80
- decorateTransactions(bundle: commons.transaction.IntendedTransactionBundle, status: AccountStatus): commons.transaction.IntendedTransactionBundle;
81
+ decorateTransactions(bundles: commons.transaction.IntendedTransactionBundle | commons.transaction.IntendedTransactionBundle[], status: AccountStatus, chainId?: ethers.BigNumberish): Promise<commons.transaction.IntendedTransactionBundle>;
81
82
  decorateSignature<T extends ethers.BytesLike>(signature: T, status: Partial<Pick<AccountStatus, 'presignedConfigurations'>>): Promise<T | string>;
82
83
  publishWitness(): Promise<void>;
83
- signDigest(digest: ethers.BytesLike, chainId: ethers.BigNumberish, decorate?: boolean, cantValidateBehavior?: 'ignore' | 'eip6492' | 'throw'): Promise<string>;
84
+ signDigest(digest: ethers.BytesLike, chainId: ethers.BigNumberish, decorate?: boolean, cantValidateBehavior?: 'ignore' | 'eip6492' | 'throw', metadata?: object): Promise<string>;
84
85
  private buildEIP6492Signature;
85
86
  editConfig(changes: {
86
87
  add?: commons.config.SimpleSigner[];
@@ -99,7 +100,7 @@ export declare class Account {
99
100
  * by any of the migrations.
100
101
  *
101
102
  */
102
- buildBootstrapTransactions(status: AccountStatus, chainId: ethers.BigNumberish): commons.transaction.IntendedTransactionBundle;
103
+ buildBootstrapTransactions(status: AccountStatus, chainId: ethers.BigNumberish): Promise<commons.transaction.IntendedTransactionBundle>;
103
104
  bootstrapTransactions(chainId: ethers.BigNumberish, prestatus?: AccountStatus): Promise<Omit<commons.transaction.IntendedTransactionBundle, 'chainId'>>;
104
105
  doBootstrap(chainId: ethers.BigNumberish, feeQuote?: FeeQuote, prestatus?: AccountStatus): Promise<commons.transaction.TransactionResponse<any>>;
105
106
  signMessage(message: ethers.BytesLike, chainId: ethers.BigNumberish, cantValidateBehavior?: 'ignore' | 'eip6492' | 'throw'): Promise<string>;
@@ -115,9 +116,11 @@ export declare class Account {
115
116
  migratedAllChains: boolean;
116
117
  failedChains: number[];
117
118
  }>;
118
- sendSignedTransactions(signedBundle: commons.transaction.IntendedTransactionBundle, chainId: ethers.BigNumberish, quote?: FeeQuote, pstatus?: AccountStatus, callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void): Promise<ethers.providers.TransactionResponse>;
119
+ sendSignedTransactions(signedBundle: commons.transaction.IntendedTransactionBundle | commons.transaction.IntendedTransactionBundle[], chainId: ethers.BigNumberish, quote?: FeeQuote, pstatus?: AccountStatus, callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void): Promise<ethers.providers.TransactionResponse>;
119
120
  fillGasLimits(txs: commons.transaction.Transactionish, chainId: ethers.BigNumberish, status?: AccountStatus): Promise<commons.transaction.SimulatedTransaction[]>;
120
- gasRefundQuotes(txs: commons.transaction.Transactionish, chainId: ethers.BigNumberish, stubSignatureOverrides: Map<string, string>, status?: AccountStatus): Promise<{
121
+ gasRefundQuotes(txs: commons.transaction.Transactionish, chainId: ethers.BigNumberish, stubSignatureOverrides: Map<string, string>, status?: AccountStatus, options?: {
122
+ simulate?: boolean;
123
+ }): Promise<{
121
124
  options: FeeOption[];
122
125
  quote?: FeeQuote;
123
126
  decorated: commons.transaction.IntendedTransactionBundle;
@@ -126,10 +129,11 @@ export declare class Account {
126
129
  txs: commons.transaction.Transactionish;
127
130
  chainId: ethers.BigNumberish;
128
131
  stubSignatureOverrides: Map<string, string>;
132
+ simulateForFeeOptions?: boolean;
129
133
  }): Promise<PreparedTransactions>;
130
134
  sendTransaction(txs: commons.transaction.Transactionish, chainId: ethers.BigNumberish, quote?: FeeQuote, skipPreDecorate?: boolean, callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void, options?: {
131
135
  nonceSpace?: ethers.BigNumberish;
132
- }): Promise<ethers.providers.TransactionResponse>;
136
+ }): Promise<ethers.providers.TransactionResponse | undefined>;
133
137
  signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, message: Record<string, any>, chainId: ethers.BigNumberish, cantValidateBehavior?: 'ignore' | 'eip6492' | 'throw'): Promise<string>;
134
138
  getAllSigners(): Promise<{
135
139
  address: string;
@@ -19,7 +19,9 @@ export declare class AccountSigner implements ethers.Signer {
19
19
  getAddress(): Promise<string>;
20
20
  signMessage(message: string | ethers.utils.Bytes): Promise<string>;
21
21
  private defaultSelectFee;
22
- sendTransaction(txsPromise: ethers.utils.Deferrable<ethers.providers.TransactionRequest> | commons.transaction.Transactionish): Promise<ethers.providers.TransactionResponse>;
22
+ sendTransaction(txsPromise: ethers.utils.Deferrable<ethers.providers.TransactionRequest> | commons.transaction.Transactionish, options?: {
23
+ simulateForFeeOptions?: boolean;
24
+ }): Promise<ethers.providers.TransactionResponse>;
23
25
  getBalance(blockTag?: ethers.providers.BlockTag | undefined): Promise<ethers.BigNumber>;
24
26
  call(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>, blockTag?: ethers.providers.BlockTag | undefined): Promise<string>;
25
27
  resolveName(name: string): Promise<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xsequence/account",
3
- "version": "1.2.8",
3
+ "version": "1.3.0",
4
4
  "description": "tools for migrating sequence wallets to new versions",
5
5
  "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/account",
6
6
  "source": "src/index.ts",
@@ -10,19 +10,19 @@
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "ethers": "^5.5.2",
13
- "@0xsequence/core": "1.2.8",
14
- "@0xsequence/migration": "1.2.8",
15
- "@0xsequence/sessions": "1.2.8",
16
- "@0xsequence/relayer": "1.2.8",
17
- "@0xsequence/utils": "1.2.8",
18
- "@0xsequence/network": "1.2.8",
19
- "@0xsequence/wallet": "1.2.8"
13
+ "@0xsequence/core": "1.3.0",
14
+ "@0xsequence/migration": "1.3.0",
15
+ "@0xsequence/network": "1.3.0",
16
+ "@0xsequence/relayer": "1.3.0",
17
+ "@0xsequence/utils": "1.3.0",
18
+ "@0xsequence/sessions": "1.3.0",
19
+ "@0xsequence/wallet": "1.3.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@istanbuljs/nyc-config-typescript": "^1.0.2",
23
23
  "nyc": "^15.1.0",
24
- "@0xsequence/signhub": "1.2.8",
25
- "@0xsequence/tests": "1.2.8"
24
+ "@0xsequence/signhub": "1.3.0",
25
+ "@0xsequence/tests": "1.3.0"
26
26
  },
27
27
  "files": [
28
28
  "src",
package/src/account.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { commons, universal } from '@0xsequence/core'
2
+ import { WalletSignRequestMetadata } from '@0xsequence/core/src/commons'
2
3
  import { migrator, defaults, version } from '@0xsequence/migration'
3
4
  import { ChainId, NetworkConfig } from '@0xsequence/network'
4
5
  import { FeeOption, FeeQuote, isRelayer, Relayer, RpcRelayer } from '@0xsequence/relayer'
@@ -352,6 +353,21 @@ export class Account {
352
353
  }
353
354
  }
354
355
 
356
+ async predecorateSignedTransactions(
357
+ status: AccountStatus,
358
+ chainId: ethers.BigNumberish
359
+ ): Promise<commons.transaction.SignedTransactionBundle[]> {
360
+ // Request signed predecorate transactions from child wallets
361
+ const bundles = await this.orchestrator.predecorateSignedTransactions({ chainId })
362
+ // Get signed predecorate transaction
363
+ const predecorated = await this.predecorateTransactions([], status, chainId)
364
+ if (commons.transaction.fromTransactionish(this.address, predecorated).length > 0) {
365
+ // Sign it
366
+ bundles.push(await this.signTransactions(predecorated, chainId))
367
+ }
368
+ return bundles
369
+ }
370
+
355
371
  async predecorateTransactions(
356
372
  txs: commons.transaction.Transactionish,
357
373
  status: AccountStatus,
@@ -369,29 +385,46 @@ export class Account {
369
385
  return txs
370
386
  }
371
387
 
372
- decorateTransactions(
373
- bundle: commons.transaction.IntendedTransactionBundle,
374
- status: AccountStatus
375
- ): commons.transaction.IntendedTransactionBundle {
376
- const bootstrapBundle = this.buildBootstrapTransactions(status, bundle.chainId)
377
- if (bootstrapBundle.transactions.length === 0) {
378
- return bundle
388
+ async decorateTransactions(
389
+ bundles: commons.transaction.IntendedTransactionBundle | commons.transaction.IntendedTransactionBundle[],
390
+ status: AccountStatus,
391
+ chainId?: ethers.BigNumberish
392
+ ): Promise<commons.transaction.IntendedTransactionBundle> {
393
+ if (!Array.isArray(bundles)) {
394
+ // Recurse with array
395
+ return this.decorateTransactions([bundles], status, chainId)
379
396
  }
380
397
 
398
+ // Default to chainId of first bundle when not supplied
399
+ chainId = chainId ?? bundles[0].chainId
400
+
401
+ const bootstrapBundle = await this.buildBootstrapTransactions(status, chainId)
402
+ const hasBootstrapTxs = bootstrapBundle.transactions.length > 0
403
+
404
+ if (!hasBootstrapTxs && bundles.length === 1) {
405
+ return bundles[0]
406
+ }
407
+
408
+ // Intent defaults to first bundle when no bootstrap transaction
409
+ const { entrypoint } = hasBootstrapTxs ? bootstrapBundle : bundles[0]
410
+
381
411
  const decoratedBundle = {
382
- entrypoint: bootstrapBundle.entrypoint,
383
- chainId: bundle.chainId,
384
- intent: bundle.intent,
412
+ entrypoint,
413
+ chainId,
414
+ // Intent of the first bundle is used
415
+ intent: bundles[0].intent,
385
416
  transactions: [
386
417
  ...bootstrapBundle.transactions,
387
- {
388
- to: bundle.entrypoint,
389
- data: commons.transaction.encodeBundleExecData(bundle),
390
- gasLimit: 0,
391
- delegateCall: false,
392
- revertOnError: true,
393
- value: 0
394
- }
418
+ ...bundles.map(
419
+ (bundle): commons.transaction.Transaction => ({
420
+ to: bundle.entrypoint,
421
+ data: commons.transaction.encodeBundleExecData(bundle),
422
+ gasLimit: 0,
423
+ delegateCall: false,
424
+ revertOnError: true,
425
+ value: 0
426
+ })
427
+ )
395
428
  ]
396
429
  }
397
430
 
@@ -399,7 +432,7 @@ export class Account {
399
432
  if (!status.onChain.deployed) {
400
433
  decoratedBundle.intent.id = commons.transaction.subdigestOfGuestModuleTransactions(
401
434
  this.contexts[this.version].guestModule,
402
- decoratedBundle.chainId,
435
+ chainId,
403
436
  decoratedBundle.transactions
404
437
  )
405
438
  }
@@ -407,12 +440,12 @@ export class Account {
407
440
  return decoratedBundle
408
441
  }
409
442
 
410
- decorateSignature<T extends ethers.BytesLike>(
443
+ async decorateSignature<T extends ethers.BytesLike>(
411
444
  signature: T,
412
445
  status: Partial<Pick<AccountStatus, 'presignedConfigurations'>>
413
446
  ): Promise<T | string> {
414
447
  if (!status.presignedConfigurations || status.presignedConfigurations.length === 0) {
415
- return new Promise(resolve => resolve(signature))
448
+ return signature
416
449
  }
417
450
 
418
451
  const coder = this.coders.signature
@@ -434,7 +467,8 @@ export class Account {
434
467
  digest: ethers.BytesLike,
435
468
  chainId: ethers.BigNumberish,
436
469
  decorate: boolean = true,
437
- cantValidateBehavior: 'ignore' | 'eip6492' | 'throw' = 'ignore'
470
+ cantValidateBehavior: 'ignore' | 'eip6492' | 'throw' = 'ignore',
471
+ metadata?: object
438
472
  ): Promise<string> {
439
473
  // If we are signing a digest for chainId zero then we can never be fully migrated
440
474
  // because Sequence v1 doesn't allow for signing a message on "all chains"
@@ -442,10 +476,8 @@ export class Account {
442
476
  // So we ignore the state on "chain zero" and instead use one of the states of the networks
443
477
  // wallet-webapp should ensure the wallet is as migrated as possible, trying to mimic
444
478
  // the behaviour of being migrated on all chains
445
-
446
479
  const chainRef = ethers.constants.Zero.eq(chainId) ? this.networks[0].chainId : chainId
447
480
  const status = await this.status(chainRef)
448
-
449
481
  this.mustBeFullyMigrated(status)
450
482
 
451
483
  // Check if we can validate onchain and what to do if we can't
@@ -455,7 +487,7 @@ export class Account {
455
487
  }
456
488
 
457
489
  const wallet = this.walletForStatus(chainId, status)
458
- const signature = await wallet.signDigest(digest)
490
+ const signature = await wallet.signDigest(digest, metadata)
459
491
 
460
492
  const decorated = decorate ? this.decorateSignature(signature, status) : signature
461
493
 
@@ -478,8 +510,8 @@ export class Account {
478
510
  return decorated
479
511
  }
480
512
 
481
- private buildEIP6492Signature(signature: string, status: AccountStatus, chainId: ethers.BigNumberish): string {
482
- const bootstrapBundle = this.buildBootstrapTransactions(status, chainId)
513
+ private async buildEIP6492Signature(signature: string, status: AccountStatus, chainId: ethers.BigNumberish): Promise<string> {
514
+ const bootstrapBundle = await this.buildBootstrapTransactions(status, chainId)
483
515
  if (bootstrapBundle.transactions.length === 0) {
484
516
  throw new Error('Cannot build EIP-6492 signature without bootstrap transactions')
485
517
  }
@@ -550,8 +582,12 @@ export class Account {
550
582
  * by any of the migrations.
551
583
  *
552
584
  */
553
- buildBootstrapTransactions(status: AccountStatus, chainId: ethers.BigNumberish): commons.transaction.IntendedTransactionBundle {
554
- const transactions: commons.transaction.Transaction[] = []
585
+ async buildBootstrapTransactions(
586
+ status: AccountStatus,
587
+ chainId: ethers.BigNumberish
588
+ ): Promise<commons.transaction.IntendedTransactionBundle> {
589
+ const bundle = await this.orchestrator.buildDeployTransaction({ chainId })
590
+ const transactions: commons.transaction.Transaction[] = bundle?.transactions ?? []
555
591
 
556
592
  // Add wallet deployment if needed
557
593
  if (!status.onChain.deployed) {
@@ -561,6 +597,7 @@ export class Account {
561
597
 
562
598
  transactions.push(...deployTransaction.transactions)
563
599
  }
600
+ const len = transactions.length
564
601
 
565
602
  // Get pending migrations
566
603
  transactions.push(
@@ -622,7 +659,17 @@ export class Account {
622
659
  this.mustBeFullyMigrated(status)
623
660
 
624
661
  const wallet = this.walletForStatus(chainId, status)
625
- const signed = await wallet.signTransactions(txs, options?.nonceSpace && { space: options?.nonceSpace })
662
+
663
+ const metadata: WalletSignRequestMetadata = {
664
+ address: this.address,
665
+ digest: '', // Set in wallet.signTransactions
666
+ chainId,
667
+ config: { version: this.version },
668
+ decorate: true,
669
+ cantValidateBehavior: 'ignore'
670
+ }
671
+
672
+ const signed = await wallet.signTransactions(txs, options?.nonceSpace && { space: options?.nonceSpace }, metadata)
626
673
 
627
674
  return {
628
675
  ...signed,
@@ -707,16 +754,19 @@ export class Account {
707
754
  }
708
755
 
709
756
  async sendSignedTransactions(
710
- signedBundle: commons.transaction.IntendedTransactionBundle,
757
+ signedBundle: commons.transaction.IntendedTransactionBundle | commons.transaction.IntendedTransactionBundle[],
711
758
  chainId: ethers.BigNumberish,
712
759
  quote?: FeeQuote,
713
760
  pstatus?: AccountStatus,
714
761
  callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void
715
762
  ): Promise<ethers.providers.TransactionResponse> {
716
- const status = pstatus || (await this.status(signedBundle.chainId))
763
+ if (!Array.isArray(signedBundle)) {
764
+ return this.sendSignedTransactions([signedBundle], chainId, quote, pstatus, callback)
765
+ }
766
+ const status = pstatus || (await this.status(signedBundle[0].chainId))
717
767
  this.mustBeFullyMigrated(status)
718
768
 
719
- const decoratedBundle = this.decorateTransactions(signedBundle, status)
769
+ const decoratedBundle = await this.decorateTransactions(signedBundle, status)
720
770
  callback?.(decoratedBundle)
721
771
 
722
772
  return this.relayer(chainId).relay(decoratedBundle, quote)
@@ -735,7 +785,10 @@ export class Account {
735
785
  txs: commons.transaction.Transactionish,
736
786
  chainId: ethers.BigNumberish,
737
787
  stubSignatureOverrides: Map<string, string>,
738
- status?: AccountStatus
788
+ status?: AccountStatus,
789
+ options?: {
790
+ simulate?: boolean
791
+ }
739
792
  ): Promise<{
740
793
  options: FeeOption[]
741
794
  quote?: FeeQuote
@@ -766,9 +819,9 @@ export class Account {
766
819
  nonce: 0 // The relayer also ignored the nonce
767
820
  }
768
821
 
769
- const decoratedBundle = this.decorateTransactions(signedBundle, wstatus)
822
+ const decoratedBundle = await this.decorateTransactions(signedBundle, wstatus)
770
823
  const data = commons.transaction.encodeBundleExecData(decoratedBundle)
771
- const res = await this.relayer(chainId).getFeeOptionsRaw(decoratedBundle.entrypoint, data)
824
+ const res = await this.relayer(chainId).getFeeOptionsRaw(decoratedBundle.entrypoint, data, options)
772
825
  return { ...res, decorated: decoratedBundle }
773
826
  }
774
827
 
@@ -776,11 +829,14 @@ export class Account {
776
829
  txs: commons.transaction.Transactionish
777
830
  chainId: ethers.BigNumberish
778
831
  stubSignatureOverrides: Map<string, string>
832
+ simulateForFeeOptions?: boolean
779
833
  }): Promise<PreparedTransactions> {
780
834
  const status = await this.status(args.chainId)
781
835
 
782
836
  const transactions = await this.fillGasLimits(args.txs, args.chainId, status)
783
- const gasRefundQuote = await this.gasRefundQuotes(transactions, args.chainId, args.stubSignatureOverrides, status)
837
+ const gasRefundQuote = await this.gasRefundQuotes(transactions, args.chainId, args.stubSignatureOverrides, status, {
838
+ simulate: args.simulateForFeeOptions
839
+ })
784
840
  const flatDecorated = commons.transaction.unwind(this.address, gasRefundQuote.decorated.transactions)
785
841
 
786
842
  return {
@@ -800,13 +856,27 @@ export class Account {
800
856
  options?: {
801
857
  nonceSpace?: ethers.BigNumberish
802
858
  }
803
- ): Promise<ethers.providers.TransactionResponse> {
859
+ ): Promise<ethers.providers.TransactionResponse | undefined> {
804
860
  const status = await this.status(chainId)
805
861
 
806
862
  const predecorated = skipPreDecorate ? txs : await this.predecorateTransactions(txs, status, chainId)
807
- const signed = await this.signTransactions(predecorated, chainId, undefined, options)
808
- // TODO: is it safe to pass status again here?
809
- return this.sendSignedTransactions(signed, chainId, quote, undefined, callback)
863
+ const hasTxs = commons.transaction.fromTransactionish(this.address, predecorated).length > 0
864
+ const signed = hasTxs ? await this.signTransactions(predecorated, chainId, undefined, options) : undefined
865
+
866
+ const childBundles = await this.orchestrator.predecorateSignedTransactions({ chainId })
867
+
868
+ const bundles: commons.transaction.SignedTransactionBundle[] = []
869
+ if (signed !== undefined && signed.transactions.length > 0) {
870
+ bundles.push(signed)
871
+ }
872
+ bundles.push(...childBundles.filter(b => b.transactions.length > 0))
873
+
874
+ if (bundles.length === 0) {
875
+ // Nothing to send?
876
+ return
877
+ }
878
+
879
+ return this.sendSignedTransactions(bundles, chainId, quote, undefined, callback)
810
880
  }
811
881
 
812
882
  async signTypedData(
@@ -0,0 +1,85 @@
1
+ import { commons } from '@0xsequence/core'
2
+ import { signers, Status } from '@0xsequence/signhub'
3
+ import { ethers } from 'ethers'
4
+ import { Account } from '../account'
5
+
6
+ export type MetadataWithChainId = {
7
+ chainId: ethers.BigNumberish
8
+ }
9
+
10
+ // Implements a wrapper for using Sequence accounts as nested signers in the signhub orchestrator.
11
+ export class AccountOrchestratorWrapper implements signers.SapientSigner {
12
+ constructor(public account: Account) {}
13
+
14
+ async getAddress(): Promise<string> {
15
+ return this.account.address
16
+ }
17
+
18
+ getChainIdFromMetadata(metadata: object): ethers.BigNumber {
19
+ try {
20
+ const { chainId } = metadata as MetadataWithChainId
21
+ return ethers.BigNumber.from(chainId)
22
+ } catch (err) {
23
+ // Invalid metadata object
24
+ throw new Error('AccountOrchestratorWrapper only supports metadata with chain id')
25
+ }
26
+ }
27
+
28
+ async buildDeployTransaction(metadata: object): Promise<commons.transaction.TransactionBundle | undefined> {
29
+ const chainId = this.getChainIdFromMetadata(metadata)
30
+ const status = await this.account.status(chainId)
31
+ return this.account.buildBootstrapTransactions(status, chainId)
32
+ }
33
+
34
+ async predecorateSignedTransactions(metadata: object): Promise<commons.transaction.SignedTransactionBundle[]> {
35
+ const chainId = this.getChainIdFromMetadata(metadata)
36
+ const status = await this.account.status(chainId)
37
+ return this.account.predecorateSignedTransactions(status, chainId)
38
+ }
39
+
40
+ async decorateTransactions(
41
+ bundle: commons.transaction.IntendedTransactionBundle,
42
+ metadata: object
43
+ ): Promise<commons.transaction.IntendedTransactionBundle> {
44
+ const chainId = this.getChainIdFromMetadata(metadata)
45
+ const status = await this.account.status(chainId)
46
+ return this.account.decorateTransactions(bundle, status)
47
+ }
48
+
49
+ async requestSignature(
50
+ _id: string,
51
+ message: ethers.utils.BytesLike,
52
+ metadata: object,
53
+ callbacks: {
54
+ onSignature: (signature: ethers.utils.BytesLike) => void
55
+ onRejection: (error: string) => void
56
+ onStatus: (situation: string) => void
57
+ }
58
+ ): Promise<boolean> {
59
+ if (!commons.isWalletSignRequestMetadata(metadata)) {
60
+ throw new Error('AccountOrchestratorWrapper only supports wallet metadata requests')
61
+ }
62
+
63
+ const { chainId, decorate } = metadata
64
+ // EIP-6492 not supported on nested signatures
65
+ // Default to throw instead of ignore. Ignoring should be explicit
66
+ const cantValidateBehavior = metadata.cantValidateBehavior ?? 'throw'
67
+
68
+ // For Sequence nested signatures we must use `signDigest` and not `signMessage`
69
+ // otherwise the account will hash the digest and the signature will be invalid.
70
+ try {
71
+ callbacks.onSignature(await this.account.signDigest(message, chainId, decorate, cantValidateBehavior, metadata))
72
+ } catch (err) {
73
+ callbacks.onRejection('Unable to sign account')
74
+ return false
75
+ }
76
+
77
+ return true
78
+ }
79
+
80
+ notifyStatusChange(_i: string, _s: Status, _m: object): void {}
81
+
82
+ suffix(): ethers.utils.BytesLike {
83
+ return [3]
84
+ }
85
+ }
package/src/signer.ts CHANGED
@@ -125,7 +125,10 @@ export class AccountSigner implements ethers.Signer {
125
125
  }
126
126
 
127
127
  async sendTransaction(
128
- txsPromise: ethers.utils.Deferrable<ethers.providers.TransactionRequest> | commons.transaction.Transactionish
128
+ txsPromise: ethers.utils.Deferrable<ethers.providers.TransactionRequest> | commons.transaction.Transactionish,
129
+ options?: {
130
+ simulateForFeeOptions?: boolean
131
+ }
129
132
  ): Promise<ethers.providers.TransactionResponse> {
130
133
  const txs = isDeferrable(txsPromise)
131
134
  ? await ethers.utils.resolveProperties(txsPromise as ethers.utils.Deferrable<ethers.providers.TransactionRequest>)
@@ -134,7 +137,8 @@ export class AccountSigner implements ethers.Signer {
134
137
  const prepare = await this.account.prepareTransactions({
135
138
  txs,
136
139
  chainId: this.chainId,
137
- stubSignatureOverrides: this.options?.stubSignatureOverrides ?? new Map()
140
+ stubSignatureOverrides: this.options?.stubSignatureOverrides ?? new Map(),
141
+ simulateForFeeOptions: options?.simulateForFeeOptions
138
142
  })
139
143
 
140
144
  const selectMethod = this.options?.selectFee ?? this.defaultSelectFee.bind(this)
@@ -153,7 +157,7 @@ export class AccountSigner implements ethers.Signer {
153
157
  nonceSpace: this.options.nonceSpace
154
158
  }
155
159
  : undefined
156
- )
160
+ ) as Promise<ethers.providers.TransactionResponse> // Will always have a transaction response
157
161
  }
158
162
 
159
163
  getBalance(blockTag?: ethers.providers.BlockTag | undefined): Promise<ethers.BigNumber> {