@0xsequence/relayer 0.40.6 → 0.41.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.
@@ -229,50 +229,95 @@ class ProviderRelayer extends BaseRelayer {
229
229
  return encodeNonce(space, nonce);
230
230
  }
231
231
 
232
- async wait(metaTxnId, timeout) {
232
+ async wait(metaTxnId, timeout, delay = this.waitPollRate, maxFails = 5) {
233
+ var _this2 = this;
234
+
233
235
  if (typeof metaTxnId !== 'string') {
234
- logger.info("computing id", metaTxnId.config, metaTxnId.context, metaTxnId.chainId, ...metaTxnId.transactions);
235
- return this.wait(computeMetaTxnHash(addressOf(metaTxnId.config, metaTxnId.context), metaTxnId.chainId, ...metaTxnId.transactions), timeout);
236
- } // Transactions can only get executed on nonce change
237
- // get all nonce changes and look for metaTxnIds in between logs
236
+ logger.info('computing id', metaTxnId.config, metaTxnId.context, metaTxnId.chainId, ...metaTxnId.transactions);
237
+ metaTxnId = computeMetaTxnHash(addressOf(metaTxnId.config, metaTxnId.context), metaTxnId.chainId, ...metaTxnId.transactions);
238
+ }
238
239
 
240
+ let timedOut = false;
239
241
 
240
- const timeoutTime = new Date().getTime() + timeout;
241
- let lastBlock = this.fromBlockLog;
242
+ const retry = async function retry(f, errorMessage) {
243
+ let fails = 0;
242
244
 
243
- if (lastBlock < 0) {
244
- const block = await this.provider.getBlockNumber();
245
- lastBlock = block + lastBlock;
246
- }
245
+ while (!timedOut) {
246
+ try {
247
+ return await f();
248
+ } catch (error) {
249
+ fails++;
247
250
 
248
- const normalMetaTxnId = metaTxnId.replace('0x', '');
251
+ if (maxFails !== undefined && fails >= maxFails) {
252
+ logger.error(`giving up after ${fails} failed attempts${errorMessage ? `: ${errorMessage}` : ''}`, error);
253
+ throw error;
254
+ } else {
255
+ logger.warn(`attempt #${fails} failed${errorMessage ? `: ${errorMessage}` : ''}`, error);
256
+ }
257
+ }
249
258
 
250
- while (new Date().getTime() < timeoutTime) {
251
- const block = await this.provider.getBlockNumber();
252
- const logs = await this.provider.getLogs({
253
- fromBlock: Math.max(0, lastBlock - this.deltaBlocksLog),
254
- toBlock: block,
255
- // Nonce change event topic
256
- topics: ['0x1f180c27086c7a39ea2a7b25239d1ab92348f07ca7bb59d1438fcf527568f881']
257
- });
258
- lastBlock = block; // Get receipts of all transactions
259
+ if (delay > 0) {
260
+ await new Promise(resolve => setTimeout(resolve, delay));
261
+ }
262
+ }
259
263
 
260
- const txs = await Promise.all(logs.map(l => this.provider.getTransactionReceipt(l.transactionHash))); // Find a transaction with a TxExecuted log
264
+ throw new Error(`timed out after ${fails} failed attempts${errorMessage ? `: ${errorMessage}` : ''}`);
265
+ };
261
266
 
262
- const found = txs.find(tx => tx.logs.find(l => l.topics.length === 0 && l.data.replace('0x', '') === normalMetaTxnId || l.topics.length === 1 && // TxFailed event topic
263
- l.topics[0] === "0x3dbd1590ea96dd3253a91f24e64e3a502e1225d602a5731357bc12643070ccd7" && l.data.length >= 64 && l.data.replace('0x', '').startsWith(normalMetaTxnId))); // If found return that
267
+ const waitReceipt = async function waitReceipt() {
268
+ // Transactions can only get executed on nonce change
269
+ // get all nonce changes and look for metaTxnIds in between logs
270
+ let lastBlock = _this2.fromBlockLog;
264
271
 
265
- if (found) {
266
- return _extends({
267
- receipt: found
268
- }, await this.provider.getTransaction(found.transactionHash));
269
- } // Otherwise wait and try again
272
+ if (lastBlock < 0) {
273
+ const block = await retry(() => _this2.provider.getBlockNumber(), 'unable to get latest block number');
274
+ lastBlock = block + lastBlock;
275
+ }
276
+
277
+ if (typeof metaTxnId !== 'string') {
278
+ throw new Error('impossible');
279
+ }
270
280
 
281
+ const normalMetaTxnId = metaTxnId.replace('0x', '');
271
282
 
272
- await new Promise(r => setTimeout(r, this.waitPollRate));
273
- }
283
+ while (!timedOut) {
284
+ const block = await retry(() => _this2.provider.getBlockNumber(), 'unable to get latest block number');
285
+ const logs = await retry(() => _this2.provider.getLogs({
286
+ fromBlock: Math.max(0, lastBlock - _this2.deltaBlocksLog),
287
+ toBlock: block,
288
+ // Nonce change event topic
289
+ topics: ['0x1f180c27086c7a39ea2a7b25239d1ab92348f07ca7bb59d1438fcf527568f881']
290
+ }), `unable to get NonceChange logs for blocks ${Math.max(0, lastBlock - _this2.deltaBlocksLog)} to ${block}`);
291
+ lastBlock = block; // Get receipts of all transactions
292
+
293
+ const txs = await Promise.all(logs.map(l => retry(() => _this2.provider.getTransactionReceipt(l.transactionHash), `unable to get receipt for transaction ${l.transactionHash}`))); // Find a transaction with a TxExecuted log
294
+
295
+ const found = txs.find(tx => tx.logs.find(l => l.topics.length === 0 && l.data.replace('0x', '') === normalMetaTxnId || l.topics.length === 1 && // TxFailed event topic
296
+ l.topics[0] === '0x3dbd1590ea96dd3253a91f24e64e3a502e1225d602a5731357bc12643070ccd7' && l.data.length >= 64 && l.data.replace('0x', '').startsWith(normalMetaTxnId))); // If found return that
297
+
298
+ if (found) {
299
+ return _extends({
300
+ receipt: found
301
+ }, await retry(() => _this2.provider.getTransaction(found.transactionHash), `unable to get transaction ${found.transactionHash}`));
302
+ } // Otherwise wait and try again
274
303
 
275
- throw new Error(`Timeout waiting for transaction receipt ${metaTxnId}`);
304
+
305
+ if (!timedOut) {
306
+ await new Promise(r => setTimeout(r, delay));
307
+ }
308
+ }
309
+
310
+ throw new Error(`Timeout waiting for transaction receipt ${metaTxnId}`);
311
+ };
312
+
313
+ if (timeout !== undefined) {
314
+ return Promise.race([waitReceipt(), new Promise((_, reject) => setTimeout(() => {
315
+ timedOut = true;
316
+ reject(`Timeout waiting for transaction receipt ${metaTxnId}`);
317
+ }, timeout))]);
318
+ } else {
319
+ return waitReceipt();
320
+ }
276
321
  }
277
322
 
278
323
  }
@@ -321,7 +366,7 @@ class LocalRelayer extends ProviderRelayer {
321
366
  this.txnOptions = transactionRequest;
322
367
  }
323
368
 
324
- async relay(signedTxs, quote) {
369
+ async relay(signedTxs, quote, waitForReceipt = true) {
325
370
  if (quote !== undefined) {
326
371
  logger.warn(`LocalRelayer doesn't accept fee quotes`);
327
372
  }
@@ -340,10 +385,18 @@ class LocalRelayer extends ProviderRelayer {
340
385
  // const gasLimit = signedTxs.transactions.reduce((sum, tx) => sum.add(tx.gasLimit), ethers.BigNumber.from(0))
341
386
  // txRequest.gasLimit = gasLimit
342
387
 
343
- return this.signer.sendTransaction(_extends({
388
+ const responsePromise = this.signer.sendTransaction(_extends({
344
389
  to,
345
390
  data
346
391
  }, this.txnOptions));
392
+
393
+ if (waitForReceipt) {
394
+ const response = await responsePromise;
395
+ response.receipt = await response.wait();
396
+ return response;
397
+ } else {
398
+ return responsePromise;
399
+ }
347
400
  }
348
401
 
349
402
  }
@@ -618,7 +671,8 @@ var relayer_gen = /*#__PURE__*/Object.freeze({
618
671
  Relayer: Relayer
619
672
  });
620
673
 
621
- const FAILED_STATUSES = [ETHTxnStatus.FAILED, ETHTxnStatus.PARTIALLY_FAILED, ETHTxnStatus.DROPPED];
674
+ const FINAL_STATUSES = [ETHTxnStatus.DROPPED, ETHTxnStatus.SUCCEEDED, ETHTxnStatus.PARTIALLY_FAILED, ETHTxnStatus.FAILED];
675
+ const FAILED_STATUSES = [ETHTxnStatus.DROPPED, ETHTxnStatus.PARTIALLY_FAILED, ETHTxnStatus.FAILED];
622
676
  function isRpcRelayerOptions(obj) {
623
677
  return obj.url !== undefined && typeof obj.url === 'string';
624
678
  }
@@ -629,26 +683,50 @@ class RpcRelayer extends BaseRelayer {
629
683
  this.service = new Relayer(options.url, fetchPonyfill().fetch);
630
684
  }
631
685
 
632
- async waitReceipt(metaTxnHash, wait = 1000) {
633
- if (typeof metaTxnHash !== 'string') {
634
- logger.info('computing id', metaTxnHash.config, metaTxnHash.context, metaTxnHash.chainId, ...metaTxnHash.transactions);
635
- return this.waitReceipt(computeMetaTxnHash(addressOf(metaTxnHash.config, metaTxnHash.context), metaTxnHash.chainId, ...metaTxnHash.transactions));
686
+ async waitReceipt(metaTxnId, delay = 1000, maxFails = 5, isCancelled) {
687
+ if (typeof metaTxnId !== 'string') {
688
+ logger.info('computing id', metaTxnId.config, metaTxnId.context, metaTxnId.chainId, ...metaTxnId.transactions);
689
+ metaTxnId = computeMetaTxnHash(addressOf(metaTxnId.config, metaTxnId.context), metaTxnId.chainId, ...metaTxnId.transactions);
636
690
  }
637
691
 
638
- logger.info(`[rpc-relayer/waitReceipt] waiting for ${metaTxnHash}`);
639
- let result = await this.service.getMetaTxnReceipt({
640
- metaTxID: metaTxnHash
641
- }); // TODO: remove check for 'UNKNOWN' status when 'QUEUED' status is supported
642
- // TODO: fix backend to not return literal 'null' txnReceipt
692
+ logger.info(`[rpc-relayer/waitReceipt] waiting for ${metaTxnId}`);
693
+ let fails = 0;
643
694
 
644
- while (!result.receipt || !result.receipt.txnReceipt || result.receipt.txnReceipt === 'null' || result.receipt.status === 'UNKNOWN' || result.receipt.status === 'QUEUED' || result.receipt.status === 'SENT') {
645
- await new Promise(r => setTimeout(r, wait));
646
- result = await this.service.getMetaTxnReceipt({
647
- metaTxID: metaTxnHash
648
- });
695
+ while (isCancelled === undefined || !isCancelled()) {
696
+ try {
697
+ const {
698
+ receipt
699
+ } = await this.service.getMetaTxnReceipt({
700
+ metaTxID: metaTxnId
701
+ });
702
+
703
+ if (!receipt) {
704
+ throw new Error('missing expected receipt');
705
+ }
706
+
707
+ if (!receipt.txnReceipt) {
708
+ throw new Error('missing expected transaction receipt');
709
+ }
710
+
711
+ if (FINAL_STATUSES.includes(receipt.status)) {
712
+ return {
713
+ receipt
714
+ };
715
+ }
716
+ } catch (e) {
717
+ fails++;
718
+
719
+ if (fails === maxFails) {
720
+ throw e;
721
+ }
722
+ }
723
+
724
+ if (isCancelled === undefined || !isCancelled()) {
725
+ await new Promise(resolve => setTimeout(resolve, delay));
726
+ }
649
727
  }
650
728
 
651
- return result;
729
+ throw new Error(`Cancelled waiting for transaction receipt ${metaTxnId}`);
652
730
  }
653
731
 
654
732
  async simulate(wallet, ...transactions) {
@@ -773,7 +851,9 @@ class RpcRelayer extends BaseRelayer {
773
851
  return nonce;
774
852
  }
775
853
 
776
- async relay(signedTxs, quote) {
854
+ async relay(signedTxs, quote, waitForReceipt = true) {
855
+ var _this = this;
856
+
777
857
  logger.info(`[rpc-relayer/relay] relaying signed meta-transactions ${JSON.stringify(signedTxs)} with quote ${JSON.stringify(quote)}`);
778
858
  let typecheckedQuote;
779
859
 
@@ -806,15 +886,50 @@ class RpcRelayer extends BaseRelayer {
806
886
  quote: typecheckedQuote
807
887
  });
808
888
  logger.info(`[rpc-relayer/relay] got relay result ${JSON.stringify(metaTxn)}`);
809
- return this.wait(metaTxn.txnHash);
889
+
890
+ if (waitForReceipt) {
891
+ return this.wait(metaTxn.txnHash);
892
+ } else {
893
+ const response = {
894
+ hash: metaTxn.txnHash,
895
+ confirmations: 0,
896
+ from: walletAddress,
897
+ wait: _confirmations => Promise.reject(new Error('impossible'))
898
+ };
899
+
900
+ const wait = async function wait(confirmations) {
901
+ var _waitResponse$receipt;
902
+
903
+ if (!_this.provider) {
904
+ throw new Error('cannot wait for receipt, relayer has no provider set');
905
+ }
906
+
907
+ const waitResponse = await _this.wait(metaTxn.txnHash);
908
+ const transactionHash = (_waitResponse$receipt = waitResponse.receipt) == null ? void 0 : _waitResponse$receipt.transactionHash;
909
+
910
+ if (!transactionHash) {
911
+ throw new Error('cannot wait for receipt, unknown native transaction hash');
912
+ }
913
+
914
+ Object.assign(response, waitResponse);
915
+ return _this.provider.waitForTransaction(transactionHash, confirmations);
916
+ };
917
+
918
+ response.wait = wait;
919
+ return response;
920
+ }
810
921
  }
811
922
 
812
- async wait(metaTxnHash, wait = 1000) {
813
- var _this = this;
923
+ async wait(metaTxnId, timeout, delay = 1000, maxFails = 5) {
924
+ var _this2 = this;
814
925
 
926
+ let timedOut = false;
815
927
  const {
816
928
  receipt
817
- } = await this.waitReceipt(metaTxnHash, wait);
929
+ } = await (timeout !== undefined ? Promise.race([this.waitReceipt(metaTxnId, delay, maxFails, () => timedOut), new Promise((_, reject) => setTimeout(() => {
930
+ timedOut = true;
931
+ reject(`Timeout waiting for transaction receipt ${metaTxnId}`);
932
+ }, timeout))]) : this.waitReceipt(metaTxnId, delay, maxFails));
818
933
 
819
934
  if (!receipt.txnReceipt || FAILED_STATUSES.includes(receipt.status)) {
820
935
  throw new MetaTransactionResponseException(receipt);
@@ -825,13 +940,13 @@ class RpcRelayer extends BaseRelayer {
825
940
  blockHash: txReceipt.blockHash,
826
941
  blockNumber: ethers.BigNumber.from(txReceipt.blockNumber).toNumber(),
827
942
  confirmations: 1,
828
- from: typeof metaTxnHash === 'string' ? undefined : addressOf(metaTxnHash.config, metaTxnHash.context),
943
+ from: typeof metaTxnId === 'string' ? undefined : addressOf(metaTxnId.config, metaTxnId.context),
829
944
  hash: txReceipt.transactionHash,
830
945
  raw: receipt.txnReceipt,
831
946
  receipt: txReceipt,
832
947
  // extended type which is Sequence-specific. Contains the decoded metaTxReceipt
833
948
  wait: async function (confirmations) {
834
- return _this.provider.waitForTransaction(txReceipt.transactionHash, confirmations);
949
+ return _this2.provider.waitForTransaction(txReceipt.transactionHash, confirmations);
835
950
  }
836
951
  };
837
952
  }
@@ -12,8 +12,8 @@ export interface Relayer {
12
12
  }>;
13
13
  gasRefundOptions(config: WalletConfig, context: WalletContext, ...transactions: Transaction[]): Promise<FeeOption[]>;
14
14
  getNonce(config: WalletConfig, context: WalletContext, space?: ethers.BigNumberish, blockTag?: providers.BlockTag): Promise<ethers.BigNumberish>;
15
- relay(signedTxs: SignedTransactions, quote?: FeeQuote): Promise<TransactionResponse>;
16
- wait(metaTxnId: string | SignedTransactions, timeout: number): Promise<TransactionResponse>;
15
+ relay(signedTxs: SignedTransactions, quote?: FeeQuote, waitForReceipt?: boolean): Promise<TransactionResponse>;
16
+ wait(metaTxnId: string | SignedTransactions, timeout?: number, delay?: number, maxFails?: number): Promise<TransactionResponse>;
17
17
  }
18
18
  export * from './local-relayer';
19
19
  export * from './base-relayer';
@@ -1,4 +1,4 @@
1
- import { TransactionRequest } from '@ethersproject/providers';
1
+ import { TransactionReceipt, TransactionRequest } from '@ethersproject/providers';
2
2
  import { Signer as AbstractSigner } from 'ethers';
3
3
  import { SignedTransactions, Transaction, TransactionResponse } from '@0xsequence/transactions';
4
4
  import { WalletContext } from '@0xsequence/network';
@@ -19,5 +19,5 @@ export declare class LocalRelayer extends ProviderRelayer implements Relayer {
19
19
  }>;
20
20
  gasRefundOptions(config: WalletConfig, context: WalletContext, ...transactions: Transaction[]): Promise<FeeOption[]>;
21
21
  setTransactionOptions(transactionRequest: TransactionRequest): void;
22
- relay(signedTxs: SignedTransactions, quote?: FeeQuote): Promise<TransactionResponse>;
22
+ relay(signedTxs: SignedTransactions, quote?: FeeQuote, waitForReceipt?: boolean): Promise<TransactionResponse<TransactionReceipt>>;
23
23
  }
@@ -25,11 +25,11 @@ export declare abstract class ProviderRelayer extends BaseRelayer implements Rel
25
25
  quote?: FeeQuote;
26
26
  }>;
27
27
  abstract gasRefundOptions(config: WalletConfig, context: WalletContext, ...transactions: Transaction[]): Promise<FeeOption[]>;
28
- abstract relay(signedTxs: SignedTransactions, quote?: FeeQuote): Promise<TransactionResponse>;
28
+ abstract relay(signedTxs: SignedTransactions, quote?: FeeQuote, waitForReceipt?: boolean): Promise<TransactionResponse>;
29
29
  simulate(wallet: string, ...transactions: Transaction[]): Promise<SimulateResult[]>;
30
30
  estimateGasLimits(config: WalletConfig, context: WalletContext, ...transactions: Transaction[]): Promise<Transaction[]>;
31
31
  getNonce(config: WalletConfig, context: WalletContext, space?: ethers.BigNumberish, blockTag?: BlockTag): Promise<ethers.BigNumberish>;
32
- wait(metaTxnId: string | SignedTransactions, timeout: number): Promise<providers.TransactionResponse & {
32
+ wait(metaTxnId: string | SignedTransactions, timeout?: number, delay?: number, maxFails?: number): Promise<providers.TransactionResponse & {
33
33
  receipt: providers.TransactionReceipt;
34
34
  }>;
35
35
  }
@@ -13,7 +13,7 @@ export declare function isRpcRelayerOptions(obj: any): obj is RpcRelayerOptions;
13
13
  export declare class RpcRelayer extends BaseRelayer implements Relayer {
14
14
  private readonly service;
15
15
  constructor(options: RpcRelayerOptions);
16
- waitReceipt(metaTxnHash: string | SignedTransactions, wait?: number): Promise<proto.GetMetaTxnReceiptReturn>;
16
+ waitReceipt(metaTxnId: string | SignedTransactions, delay?: number, maxFails?: number, isCancelled?: () => boolean): Promise<proto.GetMetaTxnReceiptReturn>;
17
17
  simulate(wallet: string, ...transactions: Transaction[]): Promise<SimulateResult[]>;
18
18
  estimateGasLimits(config: WalletConfig, context: WalletContext, ...transactions: Transaction[]): Promise<Transaction[]>;
19
19
  getFeeOptions(config: WalletConfig, context: WalletContext, ...transactions: Transaction[]): Promise<{
@@ -22,8 +22,8 @@ export declare class RpcRelayer extends BaseRelayer implements Relayer {
22
22
  }>;
23
23
  gasRefundOptions(config: WalletConfig, context: WalletContext, ...transactions: Transaction[]): Promise<FeeOption[]>;
24
24
  getNonce(config: WalletConfig, context: WalletContext, space?: ethers.BigNumberish): Promise<ethers.BigNumberish>;
25
- relay(signedTxs: SignedTransactions, quote?: FeeQuote): Promise<TransactionResponse>;
26
- wait(metaTxnHash: string | SignedTransactions, wait?: number): Promise<TransactionResponse<RelayerTxReceipt>>;
25
+ relay(signedTxs: SignedTransactions, quote?: FeeQuote, waitForReceipt?: boolean): Promise<TransactionResponse<RelayerTxReceipt>>;
26
+ wait(metaTxnId: string | SignedTransactions, timeout?: number, delay?: number, maxFails?: number): Promise<TransactionResponse<RelayerTxReceipt>>;
27
27
  }
28
28
  export declare type RelayerTxReceipt = {
29
29
  blockHash: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xsequence/relayer",
3
- "version": "0.40.6",
3
+ "version": "0.41.0",
4
4
  "description": "relayer sub-package for Sequence",
5
5
  "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/relayer",
6
6
  "source": "src/index.ts",
@@ -17,10 +17,10 @@
17
17
  "typecheck": "tsc --noEmit"
18
18
  },
19
19
  "dependencies": {
20
- "@0xsequence/abi": "^0.40.6",
21
- "@0xsequence/config": "^0.40.6",
22
- "@0xsequence/transactions": "^0.40.6",
23
- "@0xsequence/utils": "^0.40.6",
20
+ "@0xsequence/abi": "^0.41.0",
21
+ "@0xsequence/config": "^0.41.0",
22
+ "@0xsequence/transactions": "^0.41.0",
23
+ "@0xsequence/utils": "^0.41.0",
24
24
  "@ethersproject/providers": "^5.5.1",
25
25
  "ethers": "^5.5.2",
26
26
  "fetch-ponyfill": "^7.1.0"
package/src/index.ts CHANGED
@@ -39,10 +39,14 @@ export interface Relayer {
39
39
 
40
40
  // relayer will submit the transaction(s) to the network and return the transaction response.
41
41
  // The quote should be the one returned from getFeeOptions, if any.
42
- relay(signedTxs: SignedTransactions, quote?: FeeQuote): Promise<TransactionResponse>
42
+ // waitForReceipt must default to true.
43
+ relay(signedTxs: SignedTransactions, quote?: FeeQuote, waitForReceipt?: boolean): Promise<TransactionResponse>
43
44
 
44
45
  // wait for transaction confirmation
45
- wait(metaTxnId: string | SignedTransactions, timeout: number): Promise<TransactionResponse>
46
+ // timeout is the maximum time to wait for the transaction response
47
+ // delay is the polling interval, i.e. the time to wait between requests
48
+ // maxFails is the maximum number of hard failures to tolerate before giving up
49
+ wait(metaTxnId: string | SignedTransactions, timeout?: number, delay?: number, maxFails?: number): Promise<TransactionResponse>
46
50
  }
47
51
 
48
52
  export * from './local-relayer'
@@ -1,4 +1,4 @@
1
- import { TransactionRequest } from '@ethersproject/providers'
1
+ import { TransactionReceipt, TransactionRequest } from '@ethersproject/providers'
2
2
  import { Signer as AbstractSigner, ethers } from 'ethers'
3
3
  import { walletContracts } from '@0xsequence/abi'
4
4
  import { SignedTransactions, Transaction, sequenceTxAbiEncode, TransactionResponse } from '@0xsequence/transactions'
@@ -57,7 +57,7 @@ export class LocalRelayer extends ProviderRelayer implements Relayer {
57
57
  this.txnOptions = transactionRequest
58
58
  }
59
59
 
60
- async relay(signedTxs: SignedTransactions, quote?: FeeQuote): Promise<TransactionResponse> {
60
+ async relay(signedTxs: SignedTransactions, quote?: FeeQuote, waitForReceipt: boolean = true): Promise<TransactionResponse<TransactionReceipt>> {
61
61
  if (quote !== undefined) {
62
62
  logger.warn(`LocalRelayer doesn't accept fee quotes`)
63
63
  }
@@ -80,6 +80,14 @@ export class LocalRelayer extends ProviderRelayer implements Relayer {
80
80
  // const gasLimit = signedTxs.transactions.reduce((sum, tx) => sum.add(tx.gasLimit), ethers.BigNumber.from(0))
81
81
  // txRequest.gasLimit = gasLimit
82
82
 
83
- return this.signer.sendTransaction({ to, data, ...this.txnOptions })
83
+ const responsePromise = this.signer.sendTransaction({ to, data, ...this.txnOptions })
84
+
85
+ if (waitForReceipt) {
86
+ const response: TransactionResponse = await responsePromise
87
+ response.receipt = await response.wait()
88
+ return response
89
+ } else {
90
+ return responsePromise
91
+ }
84
92
  }
85
93
  }
@@ -54,7 +54,7 @@ export abstract class ProviderRelayer extends BaseRelayer implements Relayer {
54
54
  ...transactions: Transaction[]
55
55
  ): Promise<FeeOption[]>
56
56
 
57
- abstract relay(signedTxs: SignedTransactions, quote?: FeeQuote): Promise<TransactionResponse>
57
+ abstract relay(signedTxs: SignedTransactions, quote?: FeeQuote, waitForReceipt?: boolean): Promise<TransactionResponse>
58
58
 
59
59
  async simulate(wallet: string, ...transactions: Transaction[]): Promise<SimulateResult[]> {
60
60
  return (await Promise.all(transactions.map(async tx => {
@@ -128,66 +128,132 @@ export abstract class ProviderRelayer extends BaseRelayer implements Relayer {
128
128
  return encodeNonce(space, nonce)
129
129
  }
130
130
 
131
- async wait(metaTxnId: string | SignedTransactions, timeout: number): Promise<providers.TransactionResponse & { receipt: providers.TransactionReceipt }> {
131
+ async wait(
132
+ metaTxnId: string | SignedTransactions,
133
+ timeout?: number,
134
+ delay: number = this.waitPollRate,
135
+ maxFails: number = 5
136
+ ): Promise<providers.TransactionResponse & { receipt: providers.TransactionReceipt }> {
132
137
  if (typeof metaTxnId !== 'string') {
133
- logger.info("computing id", metaTxnId.config, metaTxnId.context, metaTxnId.chainId, ...metaTxnId.transactions)
134
- return this.wait(
135
- computeMetaTxnHash(addressOf(metaTxnId.config, metaTxnId.context), metaTxnId.chainId, ...metaTxnId.transactions),
136
- timeout
137
- )
138
+ logger.info('computing id', metaTxnId.config, metaTxnId.context, metaTxnId.chainId, ...metaTxnId.transactions)
139
+
140
+ metaTxnId = computeMetaTxnHash(addressOf(metaTxnId.config, metaTxnId.context), metaTxnId.chainId, ...metaTxnId.transactions)
138
141
  }
139
-
140
- // Transactions can only get executed on nonce change
141
- // get all nonce changes and look for metaTxnIds in between logs
142
- const timeoutTime = new Date().getTime() + timeout
143
- let lastBlock: number = this.fromBlockLog
144
-
145
- if (lastBlock < 0) {
146
- const block = await this.provider.getBlockNumber()
147
- lastBlock = block + lastBlock
142
+
143
+ let timedOut = false
144
+
145
+ const retry = async <T>(f: () => Promise<T>, errorMessage: string): Promise<T> => {
146
+ let fails = 0
147
+
148
+ while (!timedOut) {
149
+ try {
150
+ return await f()
151
+ } catch (error) {
152
+ fails++
153
+
154
+ if (maxFails !== undefined && fails >= maxFails) {
155
+ logger.error(`giving up after ${fails} failed attempts${errorMessage ? `: ${errorMessage}` : ''}`, error)
156
+ throw error
157
+ } else {
158
+ logger.warn(`attempt #${fails} failed${errorMessage ? `: ${errorMessage}` : ''}`, error)
159
+ }
160
+ }
161
+
162
+ if (delay > 0) {
163
+ await new Promise(resolve => setTimeout(resolve, delay))
164
+ }
165
+ }
166
+
167
+ throw new Error(`timed out after ${fails} failed attempts${errorMessage ? `: ${errorMessage}` : ''}`)
148
168
  }
149
169
 
150
- const normalMetaTxnId = metaTxnId.replace('0x', '')
170
+ const waitReceipt = async (): Promise<providers.TransactionResponse & { receipt: providers.TransactionReceipt }> => {
171
+ // Transactions can only get executed on nonce change
172
+ // get all nonce changes and look for metaTxnIds in between logs
173
+ let lastBlock: number = this.fromBlockLog
151
174
 
152
- while (new Date().getTime() < timeoutTime) {
153
- const block = await this.provider.getBlockNumber()
154
- const logs = await this.provider.getLogs({
155
- fromBlock: Math.max(0, lastBlock - this.deltaBlocksLog),
156
- toBlock: block,
157
- // Nonce change event topic
158
- topics: ['0x1f180c27086c7a39ea2a7b25239d1ab92348f07ca7bb59d1438fcf527568f881']
159
- })
175
+ if (lastBlock < 0) {
176
+ const block = await retry(() => this.provider.getBlockNumber(), 'unable to get latest block number')
177
+ lastBlock = block + lastBlock
178
+ }
160
179
 
161
- lastBlock = block
162
-
163
- // Get receipts of all transactions
164
- const txs = await Promise.all(logs.map((l) => this.provider.getTransactionReceipt(l.transactionHash)))
165
-
166
- // Find a transaction with a TxExecuted log
167
- const found = txs.find((tx) => tx.logs.find((l) => (
168
- (
169
- l.topics.length === 0 &&
170
- l.data.replace('0x', '') === normalMetaTxnId
171
- ) || (
172
- l.topics.length === 1 &&
173
- // TxFailed event topic
174
- l.topics[0] === "0x3dbd1590ea96dd3253a91f24e64e3a502e1225d602a5731357bc12643070ccd7" &&
175
- l.data.length >= 64 && l.data.replace('0x', '').startsWith(normalMetaTxnId)
180
+ if (typeof metaTxnId !== 'string') {
181
+ throw new Error('impossible')
182
+ }
183
+
184
+ const normalMetaTxnId = metaTxnId.replace('0x', '')
185
+
186
+ while (!timedOut) {
187
+ const block = await retry(() => this.provider.getBlockNumber(), 'unable to get latest block number')
188
+
189
+ const logs = await retry(
190
+ () =>
191
+ this.provider.getLogs({
192
+ fromBlock: Math.max(0, lastBlock - this.deltaBlocksLog),
193
+ toBlock: block,
194
+ // Nonce change event topic
195
+ topics: ['0x1f180c27086c7a39ea2a7b25239d1ab92348f07ca7bb59d1438fcf527568f881']
196
+ }),
197
+ `unable to get NonceChange logs for blocks ${Math.max(0, lastBlock - this.deltaBlocksLog)} to ${block}`
198
+ )
199
+
200
+ lastBlock = block
201
+
202
+ // Get receipts of all transactions
203
+ const txs = await Promise.all(
204
+ logs.map(l =>
205
+ retry(
206
+ () => this.provider.getTransactionReceipt(l.transactionHash),
207
+ `unable to get receipt for transaction ${l.transactionHash}`
208
+ )
209
+ )
176
210
  )
177
- )))
178
211
 
179
- // If found return that
180
- if (found) {
181
- return {
182
- receipt: found,
183
- ...await this.provider.getTransaction(found.transactionHash)
212
+ // Find a transaction with a TxExecuted log
213
+ const found = txs.find(tx =>
214
+ tx.logs.find(
215
+ l =>
216
+ (l.topics.length === 0 && l.data.replace('0x', '') === normalMetaTxnId) ||
217
+ (l.topics.length === 1 &&
218
+ // TxFailed event topic
219
+ l.topics[0] === '0x3dbd1590ea96dd3253a91f24e64e3a502e1225d602a5731357bc12643070ccd7' &&
220
+ l.data.length >= 64 &&
221
+ l.data.replace('0x', '').startsWith(normalMetaTxnId))
222
+ )
223
+ )
224
+
225
+ // If found return that
226
+ if (found) {
227
+ return {
228
+ receipt: found,
229
+ ...(await retry(
230
+ () => this.provider.getTransaction(found.transactionHash),
231
+ `unable to get transaction ${found.transactionHash}`
232
+ ))
233
+ }
234
+ }
235
+
236
+ // Otherwise wait and try again
237
+ if (!timedOut) {
238
+ await new Promise(r => setTimeout(r, delay))
184
239
  }
185
240
  }
186
241
 
187
- // Otherwise wait and try again
188
- await new Promise(r => setTimeout(r, this.waitPollRate))
242
+ throw new Error(`Timeout waiting for transaction receipt ${metaTxnId}`)
189
243
  }
190
244
 
191
- throw new Error(`Timeout waiting for transaction receipt ${metaTxnId}`)
245
+ if (timeout !== undefined) {
246
+ return Promise.race([
247
+ waitReceipt(),
248
+ new Promise<providers.TransactionResponse & { receipt: providers.TransactionReceipt }>((_, reject) =>
249
+ setTimeout(() => {
250
+ timedOut = true
251
+ reject(`Timeout waiting for transaction receipt ${metaTxnId}`)
252
+ }, timeout)
253
+ )
254
+ ])
255
+ } else {
256
+ return waitReceipt()
257
+ }
192
258
  }
193
259
  }