@aztec/sequencer-client 0.0.1-commit.1142ef1 → 0.0.1-commit.125b3452
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.
- package/dest/client/sequencer-client.d.ts +24 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +101 -16
- package/dest/config.d.ts +25 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +49 -30
- package/dest/global_variable_builder/global_builder.d.ts +2 -4
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +7 -6
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/config.d.ts +35 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +106 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +26 -8
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +349 -49
- package/dest/sequencer/checkpoint_proposal_job.d.ts +33 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +217 -66
- package/dest/sequencer/checkpoint_voter.d.ts +3 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +34 -10
- package/dest/sequencer/index.d.ts +1 -2
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -1
- package/dest/sequencer/metrics.d.ts +17 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +111 -30
- package/dest/sequencer/sequencer.d.ts +42 -19
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +107 -50
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +27 -19
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +67 -38
- package/dest/test/utils.d.ts +8 -8
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +12 -11
- package/package.json +29 -28
- package/src/client/sequencer-client.ts +135 -18
- package/src/config.ts +65 -41
- package/src/global_variable_builder/global_builder.ts +6 -5
- package/src/index.ts +1 -6
- package/src/publisher/config.ts +121 -43
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +38 -6
- package/src/publisher/sequencer-publisher-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +344 -61
- package/src/sequencer/checkpoint_proposal_job.ts +306 -81
- package/src/sequencer/checkpoint_voter.ts +32 -7
- package/src/sequencer/index.ts +0 -1
- package/src/sequencer/metrics.ts +124 -32
- package/src/sequencer/sequencer.ts +131 -53
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +122 -78
- package/src/test/utils.ts +24 -14
- package/dest/sequencer/block_builder.d.ts +0 -26
- package/dest/sequencer/block_builder.d.ts.map +0 -1
- package/dest/sequencer/block_builder.js +0 -129
- package/src/sequencer/block_builder.ts +0 -216
|
@@ -4,6 +4,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
|
|
|
4
4
|
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
6
|
type EmpireSlashingProposerContract,
|
|
7
|
+
FeeAssetPriceOracle,
|
|
7
8
|
type GovernanceProposerContract,
|
|
8
9
|
type IEmpireBase,
|
|
9
10
|
MULTI_CALL_3_ADDRESS,
|
|
@@ -18,19 +19,23 @@ import {
|
|
|
18
19
|
type L1BlobInputs,
|
|
19
20
|
type L1TxConfig,
|
|
20
21
|
type L1TxRequest,
|
|
22
|
+
type L1TxUtils,
|
|
23
|
+
MAX_L1_TX_LIMIT,
|
|
21
24
|
type TransactionStats,
|
|
22
25
|
WEI_CONST,
|
|
23
26
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
24
|
-
import
|
|
25
|
-
import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
27
|
+
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
26
28
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
27
29
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
28
30
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
31
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
29
32
|
import { pick } from '@aztec/foundation/collection';
|
|
30
33
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
34
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
31
35
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
32
36
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
33
37
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
38
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
34
39
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
35
40
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
36
41
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
@@ -42,9 +47,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
|
42
47
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
43
48
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
44
49
|
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
import {
|
|
51
|
+
type Hex,
|
|
52
|
+
type StateOverride,
|
|
53
|
+
type TransactionReceipt,
|
|
54
|
+
type TypedDataDefinition,
|
|
55
|
+
encodeFunctionData,
|
|
56
|
+
keccak256,
|
|
57
|
+
multicall3Abi,
|
|
58
|
+
toHex,
|
|
59
|
+
} from 'viem';
|
|
60
|
+
|
|
61
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
62
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
48
63
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
49
64
|
|
|
50
65
|
/** Arguments to the process method of the rollup contract */
|
|
@@ -59,6 +74,8 @@ type L1ProcessArgs = {
|
|
|
59
74
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
60
75
|
/** Attestations and signers signature */
|
|
61
76
|
attestationsAndSignersSignature: Signature;
|
|
77
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
78
|
+
feeAssetPriceModifier: bigint;
|
|
62
79
|
};
|
|
63
80
|
|
|
64
81
|
export const Actions = [
|
|
@@ -104,6 +121,7 @@ export class SequencerPublisher {
|
|
|
104
121
|
private interrupted = false;
|
|
105
122
|
private metrics: SequencerPublisherMetrics;
|
|
106
123
|
public epochCache: EpochCache;
|
|
124
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
107
125
|
|
|
108
126
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
109
127
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -111,6 +129,7 @@ export class SequencerPublisher {
|
|
|
111
129
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
112
130
|
|
|
113
131
|
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
132
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
114
133
|
|
|
115
134
|
protected log: Logger;
|
|
116
135
|
protected ethereumSlotDuration: bigint;
|
|
@@ -120,12 +139,14 @@ export class SequencerPublisher {
|
|
|
120
139
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
121
140
|
private proposerAddressForSimulation?: EthAddress;
|
|
122
141
|
|
|
142
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
143
|
+
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
144
|
+
|
|
123
145
|
/** L1 fee analyzer for fisherman mode */
|
|
124
146
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
|
|
147
|
+
|
|
148
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
149
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
129
150
|
|
|
130
151
|
// A CALL to a cold address is 2700 gas
|
|
131
152
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
@@ -133,7 +154,7 @@ export class SequencerPublisher {
|
|
|
133
154
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
134
155
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
135
156
|
|
|
136
|
-
public l1TxUtils:
|
|
157
|
+
public l1TxUtils: L1TxUtils;
|
|
137
158
|
public rollupContract: RollupContract;
|
|
138
159
|
public govProposerContract: GovernanceProposerContract;
|
|
139
160
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
@@ -144,11 +165,12 @@ export class SequencerPublisher {
|
|
|
144
165
|
protected requests: RequestWithExpiry[] = [];
|
|
145
166
|
|
|
146
167
|
constructor(
|
|
147
|
-
private config:
|
|
168
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
169
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
148
170
|
deps: {
|
|
149
171
|
telemetry?: TelemetryClient;
|
|
150
172
|
blobClient: BlobClientInterface;
|
|
151
|
-
l1TxUtils:
|
|
173
|
+
l1TxUtils: L1TxUtils;
|
|
152
174
|
rollupContract: RollupContract;
|
|
153
175
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
154
176
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -158,6 +180,7 @@ export class SequencerPublisher {
|
|
|
158
180
|
metrics: SequencerPublisherMetrics;
|
|
159
181
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
160
182
|
log?: Logger;
|
|
183
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
161
184
|
},
|
|
162
185
|
) {
|
|
163
186
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
@@ -171,6 +194,7 @@ export class SequencerPublisher {
|
|
|
171
194
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
172
195
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
173
196
|
this.l1TxUtils = deps.l1TxUtils;
|
|
197
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
174
198
|
|
|
175
199
|
this.rollupContract = deps.rollupContract;
|
|
176
200
|
|
|
@@ -192,12 +216,52 @@ export class SequencerPublisher {
|
|
|
192
216
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
193
217
|
);
|
|
194
218
|
}
|
|
219
|
+
|
|
220
|
+
// Initialize fee asset price oracle
|
|
221
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
222
|
+
this.l1TxUtils.client,
|
|
223
|
+
this.rollupContract,
|
|
224
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
228
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
233
|
+
* Does nothing if no store is configured.
|
|
234
|
+
*/
|
|
235
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
236
|
+
if (!this.failedTxStore) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const tx: FailedL1Tx = {
|
|
241
|
+
...failedTx,
|
|
242
|
+
timestamp: Date.now(),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Fire and forget - don't block on backup
|
|
246
|
+
void this.failedTxStore
|
|
247
|
+
.then(store => store?.saveFailedTx(tx))
|
|
248
|
+
.catch(err => {
|
|
249
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
250
|
+
});
|
|
195
251
|
}
|
|
196
252
|
|
|
197
253
|
public getRollupContract(): RollupContract {
|
|
198
254
|
return this.rollupContract;
|
|
199
255
|
}
|
|
200
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Gets the fee asset price modifier from the oracle.
|
|
259
|
+
* Returns 0n if the oracle query fails.
|
|
260
|
+
*/
|
|
261
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
262
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
263
|
+
}
|
|
264
|
+
|
|
201
265
|
public getSenderAddress() {
|
|
202
266
|
return this.l1TxUtils.getSenderAddress();
|
|
203
267
|
}
|
|
@@ -273,7 +337,7 @@ export class SequencerPublisher {
|
|
|
273
337
|
// Start the analysis
|
|
274
338
|
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
275
339
|
l2SlotNumber,
|
|
276
|
-
gasLimit > 0n ? gasLimit :
|
|
340
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
277
341
|
l1Requests,
|
|
278
342
|
blobConfig,
|
|
279
343
|
onComplete,
|
|
@@ -346,7 +410,16 @@ export class SequencerPublisher {
|
|
|
346
410
|
|
|
347
411
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
348
412
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
349
|
-
|
|
413
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
414
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
415
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
416
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
417
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
418
|
+
requested: gasLimit,
|
|
419
|
+
capped: maxGas,
|
|
420
|
+
});
|
|
421
|
+
gasLimit = maxGas;
|
|
422
|
+
}
|
|
350
423
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
351
424
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
352
425
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -356,19 +429,36 @@ export class SequencerPublisher {
|
|
|
356
429
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
357
430
|
|
|
358
431
|
try {
|
|
432
|
+
// Capture context for failed tx backup before sending
|
|
433
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
434
|
+
const multicallData = encodeFunctionData({
|
|
435
|
+
abi: multicall3Abi,
|
|
436
|
+
functionName: 'aggregate3',
|
|
437
|
+
args: [
|
|
438
|
+
validRequests.map(r => ({
|
|
439
|
+
target: r.request.to!,
|
|
440
|
+
callData: r.request.data!,
|
|
441
|
+
allowFailure: true,
|
|
442
|
+
})),
|
|
443
|
+
],
|
|
444
|
+
});
|
|
445
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
446
|
+
|
|
447
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
448
|
+
|
|
359
449
|
this.log.debug('Forwarding transactions', {
|
|
360
450
|
validRequests: validRequests.map(request => request.action),
|
|
361
451
|
txConfig,
|
|
362
452
|
});
|
|
363
|
-
const result = await
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
453
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
454
|
+
if (result === undefined) {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
458
|
+
validRequests,
|
|
459
|
+
result,
|
|
460
|
+
txContext,
|
|
370
461
|
);
|
|
371
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
372
462
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
373
463
|
} catch (err) {
|
|
374
464
|
const viemError = formatViemError(err);
|
|
@@ -386,16 +476,88 @@ export class SequencerPublisher {
|
|
|
386
476
|
}
|
|
387
477
|
}
|
|
388
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
481
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
482
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
483
|
+
*/
|
|
484
|
+
private async forwardWithPublisherRotation(
|
|
485
|
+
validRequests: RequestWithExpiry[],
|
|
486
|
+
txConfig: RequestWithExpiry['gasConfig'],
|
|
487
|
+
blobConfig: L1BlobInputs | undefined,
|
|
488
|
+
) {
|
|
489
|
+
const triedAddresses: EthAddress[] = [];
|
|
490
|
+
let currentPublisher = this.l1TxUtils;
|
|
491
|
+
|
|
492
|
+
while (true) {
|
|
493
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
494
|
+
try {
|
|
495
|
+
const result = await Multicall3.forward(
|
|
496
|
+
validRequests.map(r => r.request),
|
|
497
|
+
currentPublisher,
|
|
498
|
+
txConfig,
|
|
499
|
+
blobConfig,
|
|
500
|
+
this.rollupContract.address,
|
|
501
|
+
this.log,
|
|
502
|
+
);
|
|
503
|
+
this.l1TxUtils = currentPublisher;
|
|
504
|
+
return result;
|
|
505
|
+
} catch (err) {
|
|
506
|
+
if (err instanceof TimeoutError) {
|
|
507
|
+
throw err;
|
|
508
|
+
}
|
|
509
|
+
const viemError = formatViemError(err);
|
|
510
|
+
if (!this.getNextPublisher) {
|
|
511
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
this.log.warn(
|
|
515
|
+
`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
|
|
516
|
+
viemError,
|
|
517
|
+
);
|
|
518
|
+
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
519
|
+
if (!nextPublisher) {
|
|
520
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
currentPublisher = nextPublisher;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
389
528
|
private callbackBundledTransactions(
|
|
390
529
|
requests: RequestWithExpiry[],
|
|
391
|
-
result
|
|
530
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
531
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
392
532
|
) {
|
|
393
533
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
394
534
|
if (result instanceof FormattedViemError) {
|
|
395
535
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
536
|
+
this.backupFailedTx({
|
|
537
|
+
id: keccak256(txContext.multicallData),
|
|
538
|
+
failureType: 'send-error',
|
|
539
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
540
|
+
blobData: txContext.blobData,
|
|
541
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
542
|
+
error: { message: result.message, name: result.name },
|
|
543
|
+
context: {
|
|
544
|
+
actions: requests.map(r => r.action),
|
|
545
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
546
|
+
sender: this.getSenderAddress().toString(),
|
|
547
|
+
},
|
|
548
|
+
});
|
|
396
549
|
return { failedActions: requests.map(r => r.action) };
|
|
397
550
|
} else {
|
|
398
|
-
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
551
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
552
|
+
result,
|
|
553
|
+
requests: requests.map(r => ({
|
|
554
|
+
...r,
|
|
555
|
+
// Avoid logging large blob data
|
|
556
|
+
blobConfig: r.blobConfig
|
|
557
|
+
? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
|
|
558
|
+
: undefined,
|
|
559
|
+
})),
|
|
560
|
+
});
|
|
399
561
|
const successfulActions: Action[] = [];
|
|
400
562
|
const failedActions: Action[] = [];
|
|
401
563
|
for (const request of requests) {
|
|
@@ -405,6 +567,30 @@ export class SequencerPublisher {
|
|
|
405
567
|
failedActions.push(request.action);
|
|
406
568
|
}
|
|
407
569
|
}
|
|
570
|
+
// Single backup for the whole reverted tx
|
|
571
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
572
|
+
this.backupFailedTx({
|
|
573
|
+
id: result.receipt.transactionHash,
|
|
574
|
+
failureType: 'revert',
|
|
575
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
576
|
+
blobData: txContext.blobData,
|
|
577
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
578
|
+
receipt: {
|
|
579
|
+
transactionHash: result.receipt.transactionHash,
|
|
580
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
581
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
582
|
+
status: 'reverted',
|
|
583
|
+
},
|
|
584
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
585
|
+
context: {
|
|
586
|
+
actions: failedActions,
|
|
587
|
+
requests: requests
|
|
588
|
+
.filter(r => failedActions.includes(r.action))
|
|
589
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
590
|
+
sender: this.getSenderAddress().toString(),
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
408
594
|
return { successfulActions, failedActions };
|
|
409
595
|
}
|
|
410
596
|
}
|
|
@@ -516,8 +702,15 @@ export class SequencerPublisher {
|
|
|
516
702
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
517
703
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
518
704
|
|
|
705
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
706
|
+
|
|
519
707
|
try {
|
|
520
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
708
|
+
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
709
|
+
request,
|
|
710
|
+
undefined,
|
|
711
|
+
undefined,
|
|
712
|
+
mergeAbis([request.abi ?? [], ErrorsAbi]),
|
|
713
|
+
);
|
|
521
714
|
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
522
715
|
...logData,
|
|
523
716
|
request,
|
|
@@ -536,7 +729,7 @@ export class SequencerPublisher {
|
|
|
536
729
|
|
|
537
730
|
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
538
731
|
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
539
|
-
if (viemError.message?.includes('
|
|
732
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
540
733
|
this.log.verbose(
|
|
541
734
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
542
735
|
{ ...logData, request, error: viemError.message },
|
|
@@ -562,6 +755,18 @@ export class SequencerPublisher {
|
|
|
562
755
|
|
|
563
756
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
564
757
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
758
|
+
this.backupFailedTx({
|
|
759
|
+
id: keccak256(request.data!),
|
|
760
|
+
failureType: 'simulation',
|
|
761
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
762
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
763
|
+
error: { message: viemError.message, name: viemError.name },
|
|
764
|
+
context: {
|
|
765
|
+
actions: [`invalidate-${reason}`],
|
|
766
|
+
checkpointNumber,
|
|
767
|
+
sender: this.getSenderAddress().toString(),
|
|
768
|
+
},
|
|
769
|
+
});
|
|
565
770
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
566
771
|
}
|
|
567
772
|
}
|
|
@@ -607,24 +812,8 @@ export class SequencerPublisher {
|
|
|
607
812
|
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
608
813
|
): Promise<bigint> {
|
|
609
814
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
610
|
-
|
|
611
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
612
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
613
|
-
// so that the committee is recalculated correctly
|
|
614
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
615
|
-
// if (ignoreSignatures) {
|
|
616
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
617
|
-
// if (!committee) {
|
|
618
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
619
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
620
|
-
// }
|
|
621
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
622
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
623
|
-
// );
|
|
624
|
-
// }
|
|
625
|
-
|
|
626
815
|
const blobFields = checkpoint.toBlobFields();
|
|
627
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
816
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
628
817
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
629
818
|
|
|
630
819
|
const args = [
|
|
@@ -632,7 +821,7 @@ export class SequencerPublisher {
|
|
|
632
821
|
header: checkpoint.header.toViem(),
|
|
633
822
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
634
823
|
oracleInput: {
|
|
635
|
-
feeAssetPriceModifier:
|
|
824
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
636
825
|
},
|
|
637
826
|
},
|
|
638
827
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -681,6 +870,32 @@ export class SequencerPublisher {
|
|
|
681
870
|
return false;
|
|
682
871
|
}
|
|
683
872
|
|
|
873
|
+
// Check if payload was already submitted to governance
|
|
874
|
+
const cacheKey = payload.toString();
|
|
875
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
876
|
+
try {
|
|
877
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
878
|
+
const proposed = await retry(
|
|
879
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
880
|
+
'Check if payload was proposed',
|
|
881
|
+
makeBackoff([0, 1, 2]),
|
|
882
|
+
this.log,
|
|
883
|
+
true,
|
|
884
|
+
);
|
|
885
|
+
if (proposed) {
|
|
886
|
+
this.payloadProposedCache.add(cacheKey);
|
|
887
|
+
}
|
|
888
|
+
} catch (err) {
|
|
889
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
895
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
|
|
684
899
|
const cachedLastVote = this.lastActions[signalType];
|
|
685
900
|
this.lastActions[signalType] = slotNumber;
|
|
686
901
|
const action = signalType;
|
|
@@ -699,11 +914,26 @@ export class SequencerPublisher {
|
|
|
699
914
|
lastValidL2Slot: slotNumber,
|
|
700
915
|
});
|
|
701
916
|
|
|
917
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
918
|
+
|
|
702
919
|
try {
|
|
703
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
920
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
704
921
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
705
922
|
} catch (err) {
|
|
706
|
-
|
|
923
|
+
const viemError = formatViemError(err);
|
|
924
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
|
|
925
|
+
this.backupFailedTx({
|
|
926
|
+
id: keccak256(request.data!),
|
|
927
|
+
failureType: 'simulation',
|
|
928
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
929
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
930
|
+
error: { message: viemError.message, name: viemError.name },
|
|
931
|
+
context: {
|
|
932
|
+
actions: [action],
|
|
933
|
+
slot: slotNumber,
|
|
934
|
+
sender: this.getSenderAddress().toString(),
|
|
935
|
+
},
|
|
936
|
+
});
|
|
707
937
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
708
938
|
}
|
|
709
939
|
|
|
@@ -908,14 +1138,15 @@ export class SequencerPublisher {
|
|
|
908
1138
|
const checkpointHeader = checkpoint.header;
|
|
909
1139
|
|
|
910
1140
|
const blobFields = checkpoint.toBlobFields();
|
|
911
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1141
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
912
1142
|
|
|
913
|
-
const proposeTxArgs = {
|
|
1143
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
914
1144
|
header: checkpointHeader,
|
|
915
1145
|
archive: checkpoint.archive.root.toBuffer(),
|
|
916
1146
|
blobs,
|
|
917
1147
|
attestationsAndSigners,
|
|
918
1148
|
attestationsAndSignersSignature,
|
|
1149
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
919
1150
|
};
|
|
920
1151
|
|
|
921
1152
|
let ts: bigint;
|
|
@@ -998,13 +1229,30 @@ export class SequencerPublisher {
|
|
|
998
1229
|
|
|
999
1230
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1000
1231
|
|
|
1232
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1233
|
+
|
|
1001
1234
|
let gasUsed: bigint;
|
|
1235
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1002
1236
|
try {
|
|
1003
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [],
|
|
1237
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
|
|
1004
1238
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1005
1239
|
} catch (err) {
|
|
1006
|
-
const viemError = formatViemError(err);
|
|
1240
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
1007
1241
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1242
|
+
|
|
1243
|
+
this.backupFailedTx({
|
|
1244
|
+
id: keccak256(request.data!),
|
|
1245
|
+
failureType: 'simulation',
|
|
1246
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1247
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1248
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1249
|
+
context: {
|
|
1250
|
+
actions: [action],
|
|
1251
|
+
slot: slotNumber,
|
|
1252
|
+
sender: this.getSenderAddress().toString(),
|
|
1253
|
+
},
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1008
1256
|
return false;
|
|
1009
1257
|
}
|
|
1010
1258
|
|
|
@@ -1012,10 +1260,14 @@ export class SequencerPublisher {
|
|
|
1012
1260
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
1013
1261
|
logData.gasLimit = gasLimit;
|
|
1014
1262
|
|
|
1263
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1264
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1265
|
+
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1266
|
+
|
|
1015
1267
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1016
1268
|
this.addRequest({
|
|
1017
1269
|
action,
|
|
1018
|
-
request,
|
|
1270
|
+
request: requestWithAbi,
|
|
1019
1271
|
gasConfig: { gasLimit },
|
|
1020
1272
|
lastValidL2Slot: slotNumber,
|
|
1021
1273
|
checkSuccess: (_req, result) => {
|
|
@@ -1084,9 +1336,27 @@ export class SequencerPublisher {
|
|
|
1084
1336
|
kzg,
|
|
1085
1337
|
},
|
|
1086
1338
|
)
|
|
1087
|
-
.catch(err => {
|
|
1088
|
-
const
|
|
1089
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1339
|
+
.catch(async err => {
|
|
1340
|
+
const viemError = formatViemError(err);
|
|
1341
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1342
|
+
const validateBlobsData = encodeFunctionData({
|
|
1343
|
+
abi: RollupAbi,
|
|
1344
|
+
functionName: 'validateBlobs',
|
|
1345
|
+
args: [blobInput],
|
|
1346
|
+
});
|
|
1347
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1348
|
+
this.backupFailedTx({
|
|
1349
|
+
id: keccak256(validateBlobsData),
|
|
1350
|
+
failureType: 'simulation',
|
|
1351
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1352
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1353
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1354
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1355
|
+
context: {
|
|
1356
|
+
actions: ['validate-blobs'],
|
|
1357
|
+
sender: this.getSenderAddress().toString(),
|
|
1358
|
+
},
|
|
1359
|
+
});
|
|
1090
1360
|
throw new Error('Failed to validate blobs');
|
|
1091
1361
|
});
|
|
1092
1362
|
}
|
|
@@ -1097,8 +1367,7 @@ export class SequencerPublisher {
|
|
|
1097
1367
|
header: encodedData.header.toViem(),
|
|
1098
1368
|
archive: toHex(encodedData.archive),
|
|
1099
1369
|
oracleInput: {
|
|
1100
|
-
|
|
1101
|
-
feeAssetPriceModifier: 0n,
|
|
1370
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1102
1371
|
},
|
|
1103
1372
|
},
|
|
1104
1373
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1124,7 +1393,7 @@ export class SequencerPublisher {
|
|
|
1124
1393
|
readonly header: ViemHeader;
|
|
1125
1394
|
readonly archive: `0x${string}`;
|
|
1126
1395
|
readonly oracleInput: {
|
|
1127
|
-
readonly feeAssetPriceModifier:
|
|
1396
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1128
1397
|
};
|
|
1129
1398
|
},
|
|
1130
1399
|
ViemCommitteeAttestations,
|
|
@@ -1166,25 +1435,27 @@ export class SequencerPublisher {
|
|
|
1166
1435
|
});
|
|
1167
1436
|
}
|
|
1168
1437
|
|
|
1438
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1439
|
+
|
|
1169
1440
|
const simulationResult = await this.l1TxUtils
|
|
1170
1441
|
.simulate(
|
|
1171
1442
|
{
|
|
1172
1443
|
to: this.rollupContract.address,
|
|
1173
1444
|
data: rollupData,
|
|
1174
|
-
gas:
|
|
1445
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1175
1446
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1176
1447
|
},
|
|
1177
1448
|
{
|
|
1178
1449
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1179
1450
|
time: timestamp + 1n,
|
|
1180
1451
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1181
|
-
gasLimit:
|
|
1452
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1182
1453
|
},
|
|
1183
1454
|
stateOverrides,
|
|
1184
1455
|
RollupAbi,
|
|
1185
1456
|
{
|
|
1186
1457
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1187
|
-
fallbackGasEstimate:
|
|
1458
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1188
1459
|
},
|
|
1189
1460
|
)
|
|
1190
1461
|
.catch(err => {
|
|
@@ -1194,11 +1465,23 @@ export class SequencerPublisher {
|
|
|
1194
1465
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1195
1466
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1196
1467
|
return {
|
|
1197
|
-
gasUsed:
|
|
1468
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1198
1469
|
logs: [],
|
|
1199
1470
|
};
|
|
1200
1471
|
}
|
|
1201
1472
|
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1473
|
+
this.backupFailedTx({
|
|
1474
|
+
id: keccak256(rollupData),
|
|
1475
|
+
failureType: 'simulation',
|
|
1476
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1477
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1478
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1479
|
+
context: {
|
|
1480
|
+
actions: ['propose'],
|
|
1481
|
+
slot: Number(args[0].header.slotNumber),
|
|
1482
|
+
sender: this.getSenderAddress().toString(),
|
|
1483
|
+
},
|
|
1484
|
+
});
|
|
1202
1485
|
throw err;
|
|
1203
1486
|
});
|
|
1204
1487
|
|