@pushchain/core 6.0.15 → 6.0.16

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.
@@ -15,6 +15,8 @@
15
15
  * `executeFn` callback that replaces the former `this.execute()` call.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.getRequiredNativeBalanceForOutbound = getRequiredNativeBalanceForOutbound;
19
+ exports.shouldSkipFeeLockingForOutbound = shouldSkipFeeLockingForOutbound;
18
20
  exports.resolveR2DestinationFundsToken = resolveR2DestinationFundsToken;
19
21
  exports.executeMultiChain = executeMultiChain;
20
22
  exports.executeUoaToCea = executeUoaToCea;
@@ -46,6 +48,13 @@ const payload_builders_1 = require("../payload-builders");
46
48
  const selectors_1 = require("../../constants/selectors");
47
49
  const erc20_evm_1 = require("../../constants/abi/erc20.evm");
48
50
  const push_chain_1 = require("../../push-chain/push-chain");
51
+ function getRequiredNativeBalanceForOutbound(nativeValueForGas, gasReserve) {
52
+ return nativeValueForGas + gasReserve;
53
+ }
54
+ function shouldSkipFeeLockingForOutbound(opts) {
55
+ return (opts.isUEADeployed &&
56
+ opts.balance >= getRequiredNativeBalanceForOutbound(opts.nativeValueForGas, opts.gasReserve));
57
+ }
49
58
  // =============================================================================
50
59
  // Route 2 (UEA → CEA) phase helpers
51
60
  // =============================================================================
@@ -524,6 +533,13 @@ function executeUoaToCea(ctx, params, executeFn) {
524
533
  args: [ueaAddress],
525
534
  });
526
535
  }
536
+ const requiredNativeBalanceR2Evm = getRequiredNativeBalanceForOutbound(nativeValueForGas, EVM_GAS_RESERVE);
537
+ const skipFeeLockingR2Evm = shouldSkipFeeLockingForOutbound({
538
+ isUEADeployed,
539
+ balance: effectiveBalance,
540
+ nativeValueForGas,
541
+ gasReserve: EVM_GAS_RESERVE,
542
+ });
527
543
  (0, preflight_1.runPreflight)({
528
544
  ctx,
529
545
  ueaAddress,
@@ -557,7 +573,7 @@ function executeUoaToCea(ctx, params, executeFn) {
557
573
  isDeployed: isUEADeployed,
558
574
  nonce: ueaNonce,
559
575
  balance: ueaBalance,
560
- }, _skipFeeLocking: isUEADeployed }, (!isUEADeployed ? { _minimumDepositUsd: ROUTE2_MINIMUM_DEPOSIT_USD } : {}));
576
+ }, _skipFeeLocking: skipFeeLockingR2Evm, _requiredFundsOverride: requiredNativeBalanceR2Evm }, (!skipFeeLockingR2Evm ? { _minimumDepositUsd: ROUTE2_MINIMUM_DEPOSIT_USD } : {}));
561
577
  // Signature request — user is prompted to sign the universal payload.
562
578
  // executeFn is monolithic (sign → broadcast), so we emit the logical
563
579
  // post-sign / pre-broadcast markers as a burst after it returns so
@@ -614,6 +630,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
614
630
  if (hasSvmExecute) {
615
631
  (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — encoded execute payload: ${(svmPayload.length - 2) / 2} bytes`);
616
632
  }
633
+ (0, payload_builders_1.assertSvmPayloadWithinRelayLimit)(svmPayload, `Route 2 SVM ${targetChain}`);
617
634
  // --- Determine PRC-20 token and burn amount (chain-local, pre-203) ---
618
635
  const { prc20Token, burnAmount } = resolveR2Prc20TokenSvm(params, targetChain, ctx.pushNetwork, hasSvmExecute);
619
636
  if (params.value && params.value > BigInt(0)) {
@@ -756,6 +773,13 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
756
773
  args: [ueaAddress],
757
774
  });
758
775
  }
776
+ const requiredNativeBalanceR2Svm = getRequiredNativeBalanceForOutbound(nativeValueForGas, SVM_GAS_RESERVE);
777
+ const skipFeeLockingR2Svm = shouldSkipFeeLockingForOutbound({
778
+ isUEADeployed,
779
+ balance: ueaBalance,
780
+ nativeValueForGas,
781
+ gasReserve: SVM_GAS_RESERVE,
782
+ });
759
783
  (0, preflight_1.runPreflight)({
760
784
  ctx,
761
785
  ueaAddress,
@@ -791,7 +815,8 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
791
815
  nonce: ueaNonce,
792
816
  balance: ueaBalance,
793
817
  },
794
- _skipFeeLocking: isUEADeployed, // skip fee-locking only if UEA is already deployed
818
+ _skipFeeLocking: skipFeeLockingR2Svm,
819
+ _requiredFundsOverride: requiredNativeBalanceR2Svm,
795
820
  };
796
821
  // Signature request — user is prompted to sign the universal payload.
797
822
  // Emit spec-ordered burst after executeFn returns (sign → verify → broadcast).
@@ -1077,6 +1102,13 @@ function executeCeaToPush(ctx, params, executeFn) {
1077
1102
  // payload-only (UEA never holds the source-chain PRC-20; see plan §9 #4).
1078
1103
  // Native UPC check still applies for the gas swap. It warns by default and
1079
1104
  // throws only when options.enforceGasCheck=true.
1105
+ const requiredNativeBalanceR3Evm = getRequiredNativeBalanceForOutbound(nativeValueForGas, OUTBOUND_GAS_RESERVE_R3);
1106
+ const skipFeeLockingR3Evm = shouldSkipFeeLockingForOutbound({
1107
+ isUEADeployed,
1108
+ balance: effectiveBalance,
1109
+ nativeValueForGas,
1110
+ gasReserve: OUTBOUND_GAS_RESERVE_R3,
1111
+ });
1080
1112
  (0, preflight_1.runPreflight)({
1081
1113
  ctx,
1082
1114
  ueaAddress,
@@ -1104,7 +1136,7 @@ function executeCeaToPush(ctx, params, executeFn) {
1104
1136
  isDeployed: isUEADeployed,
1105
1137
  nonce: ueaNonce,
1106
1138
  balance: ueaBalance,
1107
- }, _skipFeeLocking: isUEADeployed }, (!isUEADeployed ? { _minimumDepositUsd: ROUTE3_MINIMUM_DEPOSIT_USD } : {}));
1139
+ }, _skipFeeLocking: skipFeeLockingR3Evm, _requiredFundsOverride: requiredNativeBalanceR3Evm }, (!skipFeeLockingR3Evm ? { _minimumDepositUsd: ROUTE3_MINIMUM_DEPOSIT_USD } : {}));
1108
1140
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_01);
1109
1141
  let response;
1110
1142
  try {
@@ -1190,6 +1222,7 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1190
1222
  extraPayload: buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, ueaNonce + BigInt(1)),
1191
1223
  revertRecipientHex: ceaPdaHex,
1192
1224
  });
1225
+ (0, payload_builders_1.assertSvmPayloadWithinRelayLimit)(svmPayload, `Route 3 SVM ${sourceChain}`);
1193
1226
  (0, context_1.printLog)(ctx, `executeCeaToPushSvm — drainAmount: ${drainAmount.toString()}, payload length: ${(svmPayload.length - 2) / 2} bytes`);
1194
1227
  const burnAmount = BigInt(0);
1195
1228
  let effectiveGasLimit = (_a = params.gasLimit) !== null && _a !== void 0 ? _a : BigInt(0);
@@ -1285,6 +1318,13 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1285
1318
  }
1286
1319
  // SVM warn-threshold telemetry (no truncation — pre-flight handles drain protection).
1287
1320
  (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R3_SVM');
1321
+ const requiredNativeBalanceR3Svm = getRequiredNativeBalanceForOutbound(nativeValueForGas, OUTBOUND_GAS_RESERVE_R3_SVM);
1322
+ const skipFeeLockingR3Svm = shouldSkipFeeLockingForOutbound({
1323
+ isUEADeployed,
1324
+ balance: currentBalance,
1325
+ nativeValueForGas,
1326
+ gasReserve: OUTBOUND_GAS_RESERVE_R3_SVM,
1327
+ });
1288
1328
  // Pre-flight (R3 SVM): no PRC-20 burn check. The drain amount lives in the
1289
1329
  // embedded Solana instruction and is withdrawn from the external CEA.
1290
1330
  (0, preflight_1.runPreflight)({
@@ -1312,8 +1352,8 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1312
1352
  const executeParams = Object.assign({ to: ueaAddress, value: multicallNativeValue, data: pushChainMulticalls, _ueaStatus: {
1313
1353
  isDeployed: isUEADeployed,
1314
1354
  nonce: ueaNonce,
1315
- balance: ueaBalance,
1316
- }, _skipFeeLocking: isUEADeployed }, (!isUEADeployed ? { _minimumDepositUsd: utils_1.Utils.helpers.parseUnits('10', 8) } : {}));
1355
+ balance: currentBalance,
1356
+ }, _skipFeeLocking: skipFeeLockingR3Svm, _requiredFundsOverride: requiredNativeBalanceR3Svm }, (!skipFeeLockingR3Svm ? { _minimumDepositUsd: utils_1.Utils.helpers.parseUnits('10', 8) } : {}));
1317
1357
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_01);
1318
1358
  let response;
1319
1359
  try {
@@ -1391,6 +1431,7 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1391
1431
  to: target,
1392
1432
  senderUea: ueaAddress,
1393
1433
  });
1434
+ (0, payload_builders_1.assertSvmPayloadWithinRelayLimit)(payload, `Route 2 SVM ${target.chain}`);
1394
1435
  let prc20Token = selectors_1.ZERO_ADDRESS;
1395
1436
  let burnAmount = BigInt(0);
1396
1437
  if ((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) {
@@ -1450,6 +1491,7 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1450
1491
  extraPayload: buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, nonce + BigInt(1)),
1451
1492
  revertRecipientHex: ceaPdaHex2,
1452
1493
  });
1494
+ (0, payload_builders_1.assertSvmPayloadWithinRelayLimit)(svmPayload, `Route 3 SVM ${sourceChain}`);
1453
1495
  // Payload-only outbound: drain amount lives inside ixData.
1454
1496
  const burnAmount = BigInt(0);
1455
1497
  const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, (_h = params.gasLimit) !== null && _h !== void 0 ? _h : BigInt(0), svmPayload, ueaAddress, (_j = params.maxPCForGas) !== null && _j !== void 0 ? _j : BigInt(0));
@@ -25,15 +25,25 @@ function buildDomainSeparator(vm, chainId, version, verifyingContract) {
25
25
  const domainTypeHash = (0, viem_1.keccak256)((0, viem_1.toBytes)(vm === enums_1.VM.EVM
26
26
  ? 'EIP712Domain(string version,uint256 chainId,address verifyingContract)'
27
27
  : 'EIP712Domain_SVM(string version,string chainId,address verifyingContract)'));
28
+ // NOTE (SVM chainId encoding): the UEA_SVM currently deployed on Donut
29
+ // abi-encodes `_universalAccountId.chainId` as a RAW string in its
30
+ // domainSeparator() (pre-EIP-712-fix). We must match that byte-for-byte or the
31
+ // ed25519 signature is over the wrong digest and `executeUniversalTx` reverts
32
+ // with `InvalidSVMSignature` (0xa764e90c). The newer UEA_SVM (core-contracts
33
+ // commits 5d97ff4 "hash SVM domain separator chainId per EIP-712" + 62d47cc
34
+ // "bind … via EIP-712 salt") keccak-hashes chainId AND adds a `bytes32 salt =
35
+ // bytes32(block.chainid)`. When Donut is upgraded to that version, change the
36
+ // SVM branch back to `keccak256(toBytes(chainId))` and append the salt field.
37
+ // Verified against on-chain getUniversalPayloadHash on UEA 0xad977a75 (Donut).
28
38
  return (0, viem_1.keccak256)((0, viem_1.encodeAbiParameters)([
29
39
  { name: 'typeHash', type: 'bytes32' },
30
40
  { name: 'version', type: 'bytes32' },
31
- { name: 'chainId', type: vm === enums_1.VM.EVM ? 'uint256' : 'bytes32' },
41
+ { name: 'chainId', type: vm === enums_1.VM.EVM ? 'uint256' : 'string' },
32
42
  { name: 'verifyingContract', type: 'address' },
33
43
  ], [
34
44
  domainTypeHash,
35
45
  (0, viem_1.keccak256)((0, viem_1.toBytes)(version)),
36
- vm === enums_1.VM.EVM ? BigInt(chainId) : (0, viem_1.keccak256)((0, viem_1.toBytes)(chainId)),
46
+ vm === enums_1.VM.EVM ? BigInt(chainId) : chainId,
37
47
  verifyingContract,
38
48
  ]));
39
49
  }
@@ -339,8 +339,11 @@ class Orchestrator {
339
339
  */
340
340
  createCascadedBuilder(preparedTxs, options) {
341
341
  const perCallHook = options === null || options === void 0 ? void 0 : options.progressHook;
342
+ const cascadeOptions = (options === null || options === void 0 ? void 0 : options.enforceGasCheck) === true
343
+ ? { enforceGasCheck: true }
344
+ : undefined;
342
345
  if (!perCallHook) {
343
- return (0, internals_1.createCascadedBuilder)(this.ctx, preparedTxs, this._getCascadeCallbacks(), undefined);
346
+ return (0, internals_1.createCascadedBuilder)(this.ctx, preparedTxs, this._getCascadeCallbacks(), undefined, cascadeOptions);
344
347
  }
345
348
  // Wrap ctx.progressHook for the synchronous send() phase — captures every
346
349
  // fireProgressHook(ctx, ...) call during pre-flight, composeCascade, and
@@ -353,7 +356,7 @@ class Orchestrator {
353
356
  // `dispatchCascadeProgressEvent` path delivers events to BOTH that hook
354
357
  // and ctx.progressHook (the init-time hook, restored by then) with
355
358
  // reference-dedup.
356
- const innerBuilder = (0, internals_1.createCascadedBuilder)(this.ctx, preparedTxs, this._getCascadeCallbacks(), perCallHook);
359
+ const innerBuilder = (0, internals_1.createCascadedBuilder)(this.ctx, preparedTxs, this._getCascadeCallbacks(), perCallHook, cascadeOptions);
357
360
  return {
358
361
  send: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
359
362
  const originalHook = this.progressHook;
@@ -140,11 +140,19 @@ export type ExecuteParams = {
140
140
  };
141
141
  /**
142
142
  * Internal: Skip fee locking for outbound flows (UEA→CEA).
143
- * Outbound txs execute entirely on Push Chain and don't need
144
- * external-chain fee locking.
143
+ * Outbound txs can skip external-chain fee locking only when the executing
144
+ * Push account already has enough native PC for the outbound gas swap plus
145
+ * Push-side execution reserve.
145
146
  * @internal
146
147
  */
147
148
  _skipFeeLocking?: boolean;
149
+ /**
150
+ * Internal: Minimum native PC balance required before standard execution may
151
+ * skip fee locking. Route handlers use this to include their own outbound
152
+ * gas-swap reserve in executeStandardPayload's fee-locking decision.
153
+ * @internal
154
+ */
155
+ _requiredFundsOverride?: bigint;
148
156
  /**
149
157
  * Internal: Minimum fee-locking deposit in USD (8 decimals).
150
158
  * Overrides the default $1 floor when the caller needs a larger deposit
@@ -717,6 +725,12 @@ export interface CascadeTrackOptions {
717
725
  eventHook?: (event: import('../progress-hook/progress-hook.types').ProgressEvent) => void;
718
726
  }
719
727
  export interface CascadeExecutionOptions {
728
+ /**
729
+ * Enforce SDK pre-flight gas/balance checks across the whole cascade.
730
+ * - false/omitted: individual prepared-hop settings decide enforcement
731
+ * - true: shortfalls throw InsufficientUEABalanceError before broadcast
732
+ */
733
+ enforceGasCheck?: boolean;
720
734
  /** Per-call progress callback. */
721
735
  progressHook?: (event: import('../progress-hook/progress-hook.types').ProgressEvent) => void;
722
736
  }
@@ -157,6 +157,14 @@ export declare function isSvmChain(chain: CHAIN): boolean;
157
157
  * Must be exactly 32 bytes (0x + 64 hex chars = 66 characters total).
158
158
  */
159
159
  export declare function isValidSolanaHexAddress(address: string): boolean;
160
+ /**
161
+ * Conservative upper bound for SVM outbound instruction data. Solana's packet
162
+ * limit is 1232 bytes before the transaction envelope, accounts, blockhash,
163
+ * and signatures, so SDK-built instruction data must stay well below that.
164
+ */
165
+ export declare const SVM_RELAYABLE_PAYLOAD_MAX_BYTES = 1000;
166
+ export declare function getHexPayloadByteLength(payload: `0x${string}`): number;
167
+ export declare function assertSvmPayloadWithinRelayLimit(payload: `0x${string}`, context: string, maxBytes?: number): void;
160
168
  /**
161
169
  * Encode SVM execute payload for CPI execution on Solana.
162
170
  *
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MULTICALL_TUPLE_TYPE = void 0;
3
+ exports.SVM_RELAYABLE_PAYLOAD_MAX_BYTES = exports.MULTICALL_TUPLE_TYPE = void 0;
4
4
  exports.buildExecuteMulticall = buildExecuteMulticall;
5
5
  exports.buildCeaMulticallPayload = buildCeaMulticallPayload;
6
6
  exports.buildInboundUniversalPayload = buildInboundUniversalPayload;
@@ -14,6 +14,8 @@ exports.buildMigrationPayload = buildMigrationPayload;
14
14
  exports.buildOutboundApprovalAndCall = buildOutboundApprovalAndCall;
15
15
  exports.isSvmChain = isSvmChain;
16
16
  exports.isValidSolanaHexAddress = isValidSolanaHexAddress;
17
+ exports.getHexPayloadByteLength = getHexPayloadByteLength;
18
+ exports.assertSvmPayloadWithinRelayLimit = assertSvmPayloadWithinRelayLimit;
17
19
  exports.encodeSvmExecutePayload = encodeSvmExecutePayload;
18
20
  exports.encodeSvmCeaToUeaPayload = encodeSvmCeaToUeaPayload;
19
21
  const viem_1 = require("viem");
@@ -413,6 +415,24 @@ function isSvmChain(chain) {
413
415
  function isValidSolanaHexAddress(address) {
414
416
  return /^0x[a-fA-F0-9]{64}$/.test(address);
415
417
  }
418
+ /**
419
+ * Conservative upper bound for SVM outbound instruction data. Solana's packet
420
+ * limit is 1232 bytes before the transaction envelope, accounts, blockhash,
421
+ * and signatures, so SDK-built instruction data must stay well below that.
422
+ */
423
+ exports.SVM_RELAYABLE_PAYLOAD_MAX_BYTES = 1000;
424
+ function getHexPayloadByteLength(payload) {
425
+ return Math.max(0, (payload.length - 2) / 2);
426
+ }
427
+ function assertSvmPayloadWithinRelayLimit(payload, context, maxBytes = exports.SVM_RELAYABLE_PAYLOAD_MAX_BYTES) {
428
+ const payloadBytes = getHexPayloadByteLength(payload);
429
+ if (payloadBytes <= maxBytes)
430
+ return;
431
+ throw new Error(`SVM outbound payload for ${context} is ${payloadBytes} bytes, ` +
432
+ `exceeding relay-safe limit ${maxBytes} bytes. Solana cannot carry ` +
433
+ `this much instruction data in one transaction; split the cascade into ` +
434
+ `separate transactions or reduce the nested payload.`);
435
+ }
416
436
  /** Convert 0x-prefixed hex string to Uint8Array */
417
437
  function hexToBytes(hex) {
418
438
  const h = hex.startsWith('0x') ? hex.slice(2) : hex;
@@ -15,12 +15,36 @@ function buildSvmPayloadFromParams(params) {
15
15
  hasExecute: false,
16
16
  };
17
17
  }
18
- const resolved = (0, resolve_1.resolveSvmCall)({
19
- programAddress: params.to.address,
20
- data: (0, viem_1.hexToBytes)(params.data),
21
- senderUea: params.senderUea,
22
- targetChain: params.to.chain,
23
- });
18
+ // `data` is the EVM-oriented calldata field. On an SVM hop it is only a real
19
+ // instruction when it resolves against a registered Anchor IDL (8-byte
20
+ // discriminator + matching instruction). EVM calldata (e.g. a 4-byte selector
21
+ // like `increment()`) is NOT an SVM instruction for the target program — treat
22
+ // it as "no execute" (empty svmPayload), matching the documented behaviour that
23
+ // the `data` field is ignored for SVM chains, rather than throwing mid-prepare.
24
+ //
25
+ // We deliberately only swallow "the data isn't a valid instruction for this
26
+ // program's IDL" (too short / no discriminator match). A "no IDL found" error
27
+ // still throws — that means the caller targeted a program whose IDL was never
28
+ // registered, which is a real configuration mistake they need to see.
29
+ let resolved;
30
+ try {
31
+ resolved = (0, resolve_1.resolveSvmCall)({
32
+ programAddress: params.to.address,
33
+ data: (0, viem_1.hexToBytes)(params.data),
34
+ senderUea: params.senderUea,
35
+ targetChain: params.to.chain,
36
+ });
37
+ }
38
+ catch (err) {
39
+ if (err instanceof Error && /no IDL found/i.test(err.message)) {
40
+ throw err;
41
+ }
42
+ return {
43
+ svmPayload: '0x',
44
+ targetBytes: params.to.address,
45
+ hasExecute: false,
46
+ };
47
+ }
24
48
  const svmPayload = (0, payload_builders_1.encodeSvmExecutePayload)({
25
49
  targetProgram: resolved.targetProgram,
26
50
  accounts: resolved.accounts,
@@ -95,6 +95,10 @@ export declare class PushChain {
95
95
  * Accepts an array of PreparedUniversalTx (from prepareTransaction).
96
96
  *
97
97
  * @param txs - Prepared transactions to execute as a cascade.
98
+ * @param options.enforceGasCheck - Defaults to false. When true, the
99
+ * cascade stops on pre-flight gas/balance shortfall instead of warning
100
+ * and proceeding. This applies to every prepared transaction in the
101
+ * cascade without having to set the flag on each prepared hop.
98
102
  * @param options.progressHook - Optional per-call progress callback.
99
103
  * Additive with the init-time `progressHook` passed to `initialize`:
100
104
  * both hooks receive every `ProgressEvent` emitted during this call
@@ -43,7 +43,25 @@ class PushClient extends evm_client_1.EvmClient {
43
43
  return tslib_1.__awaiter(this, arguments, void 0, function* (operation, operationName = 'operation') {
44
44
  const rpcUrls = this.pushChainInfo.tendermintRpc;
45
45
  let lastError = null;
46
- for (let attempt = 0; attempt < rpcUrls.length; attempt++) {
46
+ // Cycle the RPC URLs, but also RETRY transient failures (5xx / network /
47
+ // "HTTP request failed") with backoff — otherwise a single 502 on a
48
+ // single-endpoint chain (e.g. Donut) aborts a tx mid status-poll. Total
49
+ // attempts = urls × (RETRIES_PER_URL + 1).
50
+ const RETRIES_PER_URL = 4;
51
+ const totalAttempts = rpcUrls.length * (RETRIES_PER_URL + 1);
52
+ const isTransient = (e) => {
53
+ var _a;
54
+ const m = `${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : ''}`.toLowerCase();
55
+ return (/\b(502|503|504|429)\b/.test(m) ||
56
+ m.includes('http request failed') ||
57
+ m.includes('server error') ||
58
+ m.includes('timeout') ||
59
+ m.includes('timed out') ||
60
+ m.includes('econnreset') ||
61
+ m.includes('socket hang up') ||
62
+ m.includes('fetch failed'));
63
+ };
64
+ for (let attempt = 0; attempt < totalAttempts; attempt++) {
47
65
  const rpcIndex = (this.currentRpcIndex + attempt) % rpcUrls.length;
48
66
  const rpcUrl = rpcUrls[rpcIndex];
49
67
  try {
@@ -55,10 +73,16 @@ class PushClient extends evm_client_1.EvmClient {
55
73
  }
56
74
  catch (error) {
57
75
  lastError = error;
58
- if (attempt === rpcUrls.length - 1) {
76
+ if (attempt === totalAttempts - 1) {
59
77
  break;
60
78
  }
61
- yield new Promise((resolve) => setTimeout(resolve, 100));
79
+ // Non-transient errors: move to the next URL quickly. Transient ones:
80
+ // back off (200ms → 400 → 800 …, capped) so a flaky RPC can recover.
81
+ const cycle = Math.floor(attempt / rpcUrls.length);
82
+ const delay = isTransient(lastError)
83
+ ? Math.min(2000, 200 * Math.pow(2, cycle))
84
+ : 100;
85
+ yield new Promise((resolve) => setTimeout(resolve, delay));
62
86
  }
63
87
  }
64
88
  throw new Error(`All RPC endpoints failed for ${operationName}. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
@@ -14,7 +14,10 @@ const viem_1 = require("viem");
14
14
  */
15
15
  class EvmClient {
16
16
  constructor({ rpcUrls, chain }) {
17
- const transports = rpcUrls.map((rpcUrl) => (0, viem_1.http)(rpcUrl));
17
+ // retryCount/retryDelay make reads (incl. contract reads + Push status
18
+ // queries via PushClient) resilient to transient RPC 5xx (e.g. 502s under
19
+ // load), which otherwise abort a tx mid-flight. viem retries with backoff.
20
+ const transports = rpcUrls.map((rpcUrl) => (0, viem_1.http)(rpcUrl, { retryCount: 5, retryDelay: 500 }));
18
21
  this.publicClient = (0, viem_1.createPublicClient)({
19
22
  chain,
20
23
  transport: (0, viem_1.fallback)(transports),