@pushchain/core 5.1.15 → 5.1.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.
@@ -17,28 +17,9 @@ const chain_1 = require("../../constants/chain");
17
17
  const enums_1 = require("../../constants/enums");
18
18
  const progress_hook_types_1 = require("../../progress-hook/progress-hook.types");
19
19
  const context_1 = require("./context");
20
- // ============================================================================
21
- // PushChainExecutionError
22
- // ============================================================================
23
- /**
24
- * Typed error for Route 1 Push Chain tx failures. Thrown by
25
- * `extractPcTxAndTransform` when the final pcTx commits with `status === 'FAILED'`,
26
- * and surfaced as `SEND_TX_199_02` on the live stream. The readonly `code`
27
- * discriminator lets callers classify via `instanceof PushChainExecutionError`
28
- * instead of sniffing error messages.
29
- *
30
- * Carries the origin-chain gateway tx hash. There is no successful Push Chain
31
- * tx to reference on this path — by definition the pcTx committed `FAILED`.
32
- */
33
- class PushChainExecutionError extends Error {
34
- constructor(message, opts = {}) {
35
- super(message);
36
- this.code = 'PUSH_CHAIN_EXECUTION_FAILED';
37
- this.name = 'PushChainExecutionError';
38
- this.gatewayTxHash = opts.gatewayTxHash;
39
- }
40
- }
41
- exports.PushChainExecutionError = PushChainExecutionError;
20
+ const error_decoder_1 = require("./error-decoder");
21
+ const errors_1 = require("./errors");
22
+ Object.defineProperty(exports, "PushChainExecutionError", { enumerable: true, get: function () { return errors_1.PushChainExecutionError; } });
42
23
  // ============================================================================
43
24
  // sendPushTx
44
25
  // ============================================================================
@@ -167,7 +148,7 @@ const SEQ_RETRY_BASE_DELAY_MS = 400;
167
148
  */
168
149
  function sendUniversalTx(ctx, isUEADeployed, feeLockTxHash, universalPayload, verificationData, eventBuffer, transformFn, resignFn) {
169
150
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
170
- var _a, _b, _c, _d;
151
+ var _a, _b, _c, _d, _e;
171
152
  const { chain, address } = ctx.universalSigner.account;
172
153
  const { vm, chainId } = chain_1.CHAIN_INFO[chain];
173
154
  const universalAccountId = {
@@ -241,24 +222,55 @@ function sendUniversalTx(ctx, isUEADeployed, feeLockTxHash, universalPayload, ve
241
222
  var _a;
242
223
  return (_a = e.attributes) === null || _a === void 0 ? void 0 : _a.filter((attr) => attr.key === 'ethereumTxHash').map((attr) => attr.value);
243
224
  })) !== null && _b !== void 0 ? _b : [];
225
+ // Decode known revert selectors (e.g. `ret 0xacfdb444`) from the cosmos
226
+ // rawLog so the consumer-facing error AND the diagnostic failureSummary
227
+ // log carry an actionable hint instead of raw 4-byte hex. Mirrors the
228
+ // wiring in `extractPcTxAndTransform` for the post-success `pcTx FAILED`
229
+ // path. Also feeds the structured `decodedError` payload through to the
230
+ // terminal hook (199-02 / 299-02 / 399-02 / 999-02).
231
+ let augmentedMessage = (_c = tx.rawLog) !== null && _c !== void 0 ? _c : '';
232
+ let decodedForPayload;
233
+ const decoded = (0, error_decoder_1.decodeRevert)(augmentedMessage);
234
+ if ((decoded === null || decoded === void 0 ? void 0 : decoded.kind) === 'custom') {
235
+ augmentedMessage = `${augmentedMessage} [${decoded.name}: ${decoded.hint}]`;
236
+ decodedForPayload = {
237
+ name: decoded.name,
238
+ hint: decoded.hint,
239
+ selector: decoded.selector,
240
+ };
241
+ }
242
+ else if ((decoded === null || decoded === void 0 ? void 0 : decoded.kind) === 'string') {
243
+ augmentedMessage = `${augmentedMessage} [Error(string): "${decoded.decoded}"]`;
244
+ decodedForPayload = {
245
+ decoded: decoded.decoded,
246
+ selector: decoded.selector,
247
+ };
248
+ }
249
+ else if ((decoded === null || decoded === void 0 ? void 0 : decoded.kind) === 'unknown') {
250
+ (0, context_1.printLog)(ctx, `[error-decoder] unknown selector ${decoded.selector} — consider adding to KNOWN_ERROR_SELECTORS`);
251
+ }
244
252
  const failureSummary = [
245
253
  `PUSH CHAIN TRANSACTION FAILED`,
246
254
  `Cosmos TX Hash: ${tx.transactionHash}`,
247
255
  `Block Height: ${tx.height}, TX Code: ${tx.code} (error)`,
248
256
  `Gas Used: ${tx.gasUsed}, Gas Wanted: ${tx.gasWanted}`,
249
257
  ...(failedEthTxHashes.length > 0 ? [`Ethereum TX Hash(es): ${failedEthTxHashes.join(', ')}`] : []),
250
- `Error: ${tx.rawLog}`,
258
+ `Error: ${augmentedMessage}`,
251
259
  ].join(' | ');
252
260
  (0, context_1.printLog)(ctx, failureSummary);
253
- throw new Error(tx.rawLog);
261
+ // Throw a typed error so the orchestrator's outer catch can lift the
262
+ // structured `decodedError` straight onto the terminal-hook payload.
263
+ throw new errors_1.PushChainExecutionError(augmentedMessage, {
264
+ decodedError: decodedForPayload,
265
+ });
254
266
  }
255
267
  if (!tx || tx.code !== 0) {
256
268
  throw new Error(`sendUniversalTx — unreachable: loop exited without success or throw`);
257
269
  }
258
- const ethTxHashes = (_d = (_c = tx.events) === null || _c === void 0 ? void 0 : _c.filter((e) => e.type === 'ethereum_tx').flatMap((e) => {
270
+ const ethTxHashes = (_e = (_d = tx.events) === null || _d === void 0 ? void 0 : _d.filter((e) => e.type === 'ethereum_tx').flatMap((e) => {
259
271
  var _a;
260
272
  return (_a = e.attributes) === null || _a === void 0 ? void 0 : _a.filter((attr) => attr.key === 'ethereumTxHash').map((attr) => attr.value);
261
- })) !== null && _d !== void 0 ? _d : [];
273
+ })) !== null && _e !== void 0 ? _e : [];
262
274
  if (ethTxHashes.length === 0) {
263
275
  throw new Error('No ethereumTxHash found in transaction events');
264
276
  }
@@ -278,7 +290,7 @@ function sendUniversalTx(ctx, isUEADeployed, feeLockTxHash, universalPayload, ve
278
290
  */
279
291
  function extractPcTxAndTransform(ctx, pushChainUniversalTx, gatewayTxHash, eventBuffer, label, transformFn) {
280
292
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
281
- var _a;
293
+ var _a, _b;
282
294
  if (!((_a = pushChainUniversalTx === null || pushChainUniversalTx === void 0 ? void 0 : pushChainUniversalTx.pcTx) === null || _a === void 0 ? void 0 : _a.length)) {
283
295
  throw new Error(`Failed to retrieve Push Chain transaction status for gateway tx: ${gatewayTxHash}. ` +
284
296
  `The transaction may have failed on Push Chain or not been indexed yet.`);
@@ -291,24 +303,39 @@ function extractPcTxAndTransform(ctx, pushChainUniversalTx, gatewayTxHash, event
291
303
  ? lastPcTransaction
292
304
  : pushChainUniversalTx.pcTx.find((pcTx) => pcTx.status === 'FAILED' && pcTx.errorMsg);
293
305
  const errorDetails = (failedPcTx === null || failedPcTx === void 0 ? void 0 : failedPcTx.errorMsg) ? `: ${failedPcTx.errorMsg}` : '';
294
- // Parse known UEA error selectors for actionable hints
306
+ // Decode known revert selectors and ABI-encoded `Error(string)` reverts
307
+ // into actionable hints. Unknown selectors are logged at INFO so the
308
+ // table can grow from real reverts.
295
309
  let hint = '';
296
- if (failedPcTx === null || failedPcTx === void 0 ? void 0 : failedPcTx.errorMsg) {
297
- if (failedPcTx.errorMsg.includes('0xacfdb444')) {
298
- hint = ' [ExecutionFailed: the subcall to the target contract reverted. ' +
299
- 'Check that the target address can receive native tokens (has receive/fallback) ' +
300
- 'and that calldata matches a valid function signature.]';
301
- }
302
- else if (failedPcTx.errorMsg.includes('0x179a867c')) {
303
- hint = ' [ExpiredDeadline: the transaction deadline has passed.]';
304
- }
310
+ let decodedForPayload;
311
+ const decoded = (0, error_decoder_1.decodeRevert)((_b = failedPcTx === null || failedPcTx === void 0 ? void 0 : failedPcTx.errorMsg) !== null && _b !== void 0 ? _b : '');
312
+ if ((decoded === null || decoded === void 0 ? void 0 : decoded.kind) === 'custom') {
313
+ hint = ` [${decoded.name}: ${decoded.hint}]`;
314
+ decodedForPayload = {
315
+ name: decoded.name,
316
+ hint: decoded.hint,
317
+ selector: decoded.selector,
318
+ };
319
+ }
320
+ else if ((decoded === null || decoded === void 0 ? void 0 : decoded.kind) === 'string') {
321
+ hint = ` [Error(string): "${decoded.decoded}"]`;
322
+ decodedForPayload = {
323
+ decoded: decoded.decoded,
324
+ selector: decoded.selector,
325
+ };
326
+ }
327
+ else if ((decoded === null || decoded === void 0 ? void 0 : decoded.kind) === 'unknown') {
328
+ (0, context_1.printLog)(ctx, `[error-decoder] unknown selector ${decoded.selector} — consider adding to KNOWN_ERROR_SELECTORS`);
305
329
  }
306
330
  const fullMessage = `Push Chain transaction failed for gateway tx: ${gatewayTxHash}${errorDetails}${hint}`;
307
331
  // Live-emit 199-02 before throwing so the terminal failure hook reaches
308
332
  // any caller-registered progress stream (parity with reconstructProgressEvents
309
333
  // which emits 199-02 on replay).
310
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_199_02, fullMessage);
311
- throw new PushChainExecutionError(fullMessage, { gatewayTxHash });
334
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_199_02, fullMessage, decodedForPayload);
335
+ throw new errors_1.PushChainExecutionError(fullMessage, {
336
+ gatewayTxHash,
337
+ decodedError: decodedForPayload,
338
+ });
312
339
  }
313
340
  const tx = yield ctx.pushClient.getTransaction(lastPcTransaction.txHash);
314
341
  return transformFn(tx, eventBuffer);
@@ -31,6 +31,8 @@ const helpers_1 = require("./helpers");
31
31
  const payload_builder_1 = require("./payload-builder");
32
32
  const uea_manager_1 = require("./uea-manager");
33
33
  const gas_calculator_1 = require("./gas-calculator");
34
+ const preflight_1 = require("./preflight");
35
+ const svm_rent_1 = require("./svm-rent");
34
36
  const chain_1 = require("../../constants/chain");
35
37
  const utils_1 = require("../../utils");
36
38
  const price_fetch_1 = require("../../price-fetch/price-fetch");
@@ -279,7 +281,7 @@ function executeMultiChain(ctx, params, executeFn) {
279
281
  */
280
282
  function executeUoaToCea(ctx, params, executeFn) {
281
283
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
282
- var _a, _b, _c;
284
+ var _a, _b, _c, _d;
283
285
  const target = params.to;
284
286
  const targetChain = target.chain;
285
287
  const targetAddress = target.address;
@@ -440,29 +442,39 @@ function executeUoaToCea(ctx, params, executeFn) {
440
442
  (0, context_1.printLog)(ctx, `executeUoaToCea — capping nativeValueForGas at 200 UPC ceiling (was ${nativeValueForGas.toString()})`);
441
443
  nativeValueForGas = EVM_NATIVE_VALUE_SAFETY_CAP;
442
444
  }
443
- let adjustedValue;
444
- if (effectiveBalance >= nativeValueForGas + EVM_GAS_RESERVE) {
445
- adjustedValue = nativeValueForGas;
446
- }
447
- else if (effectiveBalance > EVM_GAS_RESERVE) {
448
- // Balance-starved: send everything we can afford. swapAndBurnGas
449
- // refunds unused; undershoot will revert with a concrete error.
450
- adjustedValue = effectiveBalance - EVM_GAS_RESERVE;
451
- }
452
- else if (effectiveBalance > BigInt(0)) {
453
- // Drained UEA: overshoot would return (false, "") and surface as an
454
- // opaque ExecutionFailed() — clamp to what we have.
455
- adjustedValue = effectiveBalance < nativeValueForGas ? effectiveBalance : nativeValueForGas;
456
- }
457
- else {
458
- throw new Error(`UEA ${ueaAddress} has zero UPC balance; cannot fund outbound gas swap. ` +
459
- `Bridge UPC to the UEA before retrying.`);
445
+ // Pre-flight: throws InsufficientUEABalanceError when UEA cannot cover
446
+ // (nativeValueForGas + gasReserve) or the PRC-20 burn amount. Replaces the
447
+ // legacy silent-clamp that previously submitted under-funded swaps and
448
+ // bubbled up as Uniswap STF reverts. PRC-20 check fires whenever
449
+ // burnAmount > 0 — user must pre-hold the burn token, even when it equals
450
+ // the gas token (the gateway does transferFrom BEFORE the gas swap).
451
+ let prc20BalanceR2Evm;
452
+ if (burnAmount > BigInt(0)) {
453
+ prc20BalanceR2Evm = yield ctx.pushClient.readContract({
454
+ address: prc20Token,
455
+ abi: erc20_evm_1.ERC20_EVM,
456
+ functionName: 'balanceOf',
457
+ args: [ueaAddress],
458
+ });
460
459
  }
461
- if (adjustedValue !== nativeValueForGas) {
462
- (0, context_1.printLog)(ctx, `executeUoaToCea — adjusting nativeValueForGas from ${nativeValueForGas.toString()} ` +
463
- `to ${adjustedValue.toString()} (effective balance: ${effectiveBalance.toString()})`);
464
- nativeValueForGas = adjustedValue;
460
+ const allowUnderfundedR2Evm = ((_d = params.options) === null || _d === void 0 ? void 0 : _d.allowUnderfundedSwap) === true;
461
+ const preflightR2Evm = (0, preflight_1.runPreflight)({
462
+ ctx,
463
+ ueaAddress,
464
+ ueaBalance: effectiveBalance,
465
+ requiredValue: nativeValueForGas,
466
+ gasReserve: EVM_GAS_RESERVE,
467
+ pathTag: 'R2_EVM',
468
+ burnToken: prc20Token,
469
+ burnAmount,
470
+ prc20Balance: prc20BalanceR2Evm,
471
+ allowUnderfundedSwap: allowUnderfundedR2Evm,
472
+ });
473
+ if (!preflightR2Evm.ok) {
474
+ nativeValueForGas = preflightR2Evm.legacyClampedValue;
475
+ (0, context_1.printLog)(ctx, `executeUoaToCea — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
465
476
  }
477
+ // ok=true → nativeValueForGas already covers required + reserve; no adjust needed.
466
478
  // Build outbound multicalls (approve burn + sendUniversalTxOutbound with native value)
467
479
  const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
468
480
  prc20Token,
@@ -475,31 +487,6 @@ function executeUoaToCea(ctx, params, executeFn) {
475
487
  });
476
488
  pushChainMulticalls.push(...outboundMulticalls);
477
489
  (0, context_1.printLog)(ctx, `executeUoaToCea — Push Chain multicall has ${pushChainMulticalls.length} operations`);
478
- // TODO: Enable pre-flight balance checks once outbound flow is stable
479
- // if (burnAmount > BigInt(0)) {
480
- // const prc20Balance = await ctx.pushClient.publicClient.readContract({
481
- // address: prc20Token,
482
- // abi: [{ name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] }],
483
- // functionName: 'balanceOf',
484
- // args: [ueaAddress],
485
- // }) as bigint;
486
- // if (prc20Balance < burnAmount) {
487
- // throw new Error(
488
- // `Insufficient PRC-20 token balance on UEA. ` +
489
- // `Required: ${burnAmount.toString()}, Available: ${prc20Balance.toString()}, ` +
490
- // `Token: ${prc20Token}, UEA: ${ueaAddress}. ` +
491
- // `Please bridge tokens to Push Chain first.`
492
- // );
493
- // }
494
- // }
495
- // const currentUeaBalance = await ctx.pushClient.getBalance(ueaAddress);
496
- // if (currentUeaBalance < nativeValueForGas) {
497
- // throw new Error(
498
- // `Insufficient native balance on UEA for outbound gas. ` +
499
- // `Required: ${nativeValueForGas.toString()} wei, Available: ${currentUeaBalance.toString()} wei, ` +
500
- // `UEA: ${ueaAddress}. Please send UPC to your UEA first.`
501
- // );
502
- // }
503
490
  // Execute through the normal execute() flow
504
491
  // This handles fee-locking on origin chain and executes the multicall from UEA context
505
492
  // Sum native values from multicall entries for proper fee calculation
@@ -549,7 +536,7 @@ function executeUoaToCea(ctx, params, executeFn) {
549
536
  */
550
537
  function executeUoaToCeaSvm(ctx, params, target, executeFn) {
551
538
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
552
- var _a, _b, _c, _d, _e, _f;
539
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
553
540
  const targetChain = target.chain;
554
541
  const targetAddress = target.address; // 0x-prefixed, 32 bytes
555
542
  const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
@@ -582,6 +569,22 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
582
569
  // gasLimit, it'll be derived from the gasFee/gasPrice response so the
583
570
  // on-chain record carries a non-zero compute budget the relay can use.
584
571
  let effectiveGasLimit = (_d = params.gasLimit) !== null && _d !== void 0 ? _d : BigInt(0);
572
+ // Conditional CEA-ATA rent bump: when the SPL outbound's destination ATA
573
+ // does not yet exist on Solana, the gateway needs an extra ~2.04M lamports
574
+ // for ATA-create rent. The on-chain quote does NOT include this, so bump
575
+ // effectiveGasLimit BEFORE queryOutboundGasFee so the returned gasFee
576
+ // already covers the rent.
577
+ const splMintBase58 = burnAmount > BigInt(0)
578
+ ? (_f = (_e = params.funds) === null || _e === void 0 ? void 0 : _e.token) === null || _f === void 0 ? void 0 : _f.address
579
+ : undefined;
580
+ effectiveGasLimit = yield (0, svm_rent_1.maybeBumpForCeaAtaRent)({
581
+ ctx,
582
+ ueaAddress,
583
+ targetChain,
584
+ splMintBase58,
585
+ burnAmount,
586
+ effectiveGasLimit,
587
+ });
585
588
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
586
589
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_01, targetChain);
587
590
  try {
@@ -628,7 +631,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
628
631
  const signerChainSvm = ctx.universalSigner.account.chain;
629
632
  const isNativePushEOASvm = (0, helpers_1.isPushChain)(signerChainSvm);
630
633
  // Skip getCode if accountStatusCache already confirmed deployment
631
- const deployedHintSvm = (_f = (_e = ctx.accountStatusCache) === null || _e === void 0 ? void 0 : _e.uea) === null || _f === void 0 ? void 0 : _f.deployed;
634
+ const deployedHintSvm = (_h = (_g = ctx.accountStatusCache) === null || _g === void 0 ? void 0 : _g.uea) === null || _h === void 0 ? void 0 : _h.deployed;
632
635
  let ueaBalance;
633
636
  let isUEADeployed;
634
637
  if (deployedHintSvm || isNativePushEOASvm) {
@@ -650,37 +653,57 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
650
653
  // refundUnusedGas.
651
654
  //
652
655
  // SVM pools (WPC/pSOL) are thinner than EVM pools so the pool-quote can
653
- // easily exceed the UEA's balance. Apply a balance-aware clamp so the
654
- // outer tx doesn't fail with InsufficientFunds before the swap runs.
655
- // No hard safety cap here — pSOL pool skew means even legitimate swaps
656
- // can need hundreds of PC; capping would trigger Uniswap STF reverts.
656
+ // easily exceed the UEA's balance. Pre-flight (`runPreflight`) handles
657
+ // drain protection: an under-funded quote throws cleanly with
658
+ // `InsufficientUEABalanceError` and no cosmos tx is submitted. The
659
+ // SVM_NATIVE_VALUE_WARN_THRESHOLD below is telemetry-only; it does NOT
660
+ // truncate `nativeValueForGas`. v2 may add a hard cap once telemetry
661
+ // justifies a number — until then the original engineer's pool-skew
662
+ // warning still applies (capping risks false-positive truncation of
663
+ // legitimate quotes).
657
664
  const SVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for outer-tx gas
658
665
  let nativeValueForGas = BigInt(0);
659
666
  if (universalCoreAddress && gasFee > BigInt(0)) {
660
667
  const estimated = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, ueaBalance);
661
668
  nativeValueForGas = (estimated * BigInt(110)) / BigInt(100);
662
669
  (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — nativeValueForGas: pool-quote=${estimated.toString()}, with 10% buffer=${nativeValueForGas.toString()}`);
670
+ (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R2_SVM');
671
+ }
672
+ // Pre-flight: throws InsufficientUEABalanceError when UEA cannot cover
673
+ // (nativeValueForGas + SVM_GAS_RESERVE) or the PRC-20 burn amount.
674
+ // Replaces the legacy silent-clamp that previously submitted under-funded
675
+ // swaps and bubbled up as Uniswap STF reverts.
676
+ // PRC-20 burn-balance pre-check fires whenever `burnAmount > 0`, regardless
677
+ // of whether `prc20Token` happens to equal `gasToken`. The gateway's
678
+ // `sendUniversalTxOutbound` does `transferFrom(user, gateway, burnAmount)`
679
+ // BEFORE the gas swap, so user must pre-hold `burnAmount` of `prc20Token`
680
+ // even when it's the same asset as the gas token (verified empirically:
681
+ // EOA with 77k units of pSOL reverted on-chain when trying to burn 10M).
682
+ let prc20BalanceR2Svm;
683
+ if (burnAmount > BigInt(0)) {
684
+ prc20BalanceR2Svm = yield ctx.pushClient.readContract({
685
+ address: prc20Token,
686
+ abi: erc20_evm_1.ERC20_EVM,
687
+ functionName: 'balanceOf',
688
+ args: [ueaAddress],
689
+ });
663
690
  }
664
- // Balance-aware clamp: if UEA balance can't cover target + gas reserve,
665
- // send only what we can afford. swapAndBurnGas refunds any excess.
666
- let adjustedValueSvm;
667
- if (ueaBalance >= nativeValueForGas + SVM_GAS_RESERVE) {
668
- adjustedValueSvm = nativeValueForGas;
669
- }
670
- else if (ueaBalance > SVM_GAS_RESERVE) {
671
- adjustedValueSvm = ueaBalance - SVM_GAS_RESERVE;
672
- }
673
- else if (ueaBalance > BigInt(0)) {
674
- adjustedValueSvm = ueaBalance < nativeValueForGas ? ueaBalance : nativeValueForGas;
675
- }
676
- else {
677
- throw new Error(`UEA ${ueaAddress} has zero UPC balance; cannot fund outbound gas swap. ` +
678
- `Bridge UPC to the UEA before retrying.`);
679
- }
680
- if (adjustedValueSvm !== nativeValueForGas) {
681
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — adjusting nativeValueForGas from ${nativeValueForGas.toString()} ` +
682
- `to ${adjustedValueSvm.toString()} (UEA balance: ${ueaBalance.toString()})`);
683
- nativeValueForGas = adjustedValueSvm;
691
+ const allowUnderfundedR2Svm = ((_j = params.options) === null || _j === void 0 ? void 0 : _j.allowUnderfundedSwap) === true;
692
+ const preflightR2Svm = (0, preflight_1.runPreflight)({
693
+ ctx,
694
+ ueaAddress,
695
+ ueaBalance,
696
+ requiredValue: nativeValueForGas,
697
+ gasReserve: SVM_GAS_RESERVE,
698
+ pathTag: 'R2_SVM',
699
+ burnToken: prc20Token,
700
+ burnAmount,
701
+ prc20Balance: prc20BalanceR2Svm,
702
+ allowUnderfundedSwap: allowUnderfundedR2Svm,
703
+ });
704
+ if (!preflightR2Svm.ok) {
705
+ nativeValueForGas = preflightR2Svm.legacyClampedValue;
706
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
684
707
  }
685
708
  // --- Build Push Chain multicalls (approve + sendUniversalTxOutbound) ---
686
709
  // Reuse the same builder as EVM — this part is identical
@@ -746,7 +769,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
746
769
  */
747
770
  function executeCeaToPush(ctx, params, executeFn) {
748
771
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
749
- var _a, _b, _c, _d, _e, _f, _g, _h;
772
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
750
773
  // 1. Validate and extract source chain
751
774
  if (!((_a = params.from) === null || _a === void 0 ? void 0 : _a.chain)) {
752
775
  throw new Error('Route 3 (CEA -> Push) requires from.chain to specify the source CEA chain');
@@ -994,31 +1017,42 @@ function executeCeaToPush(ctx, params, executeFn) {
994
1017
  }
995
1018
  }
996
1019
  }
997
- // Balance-aware adjustment matching Route 2 logic:
998
- // - If balance exceeds target + reserve: use target (200 UPC)
999
- // - If balance exceeds reserve: use balance - reserve (maximize within budget)
1000
- // - Otherwise: keep original nativeValueForGas (fallback)
1001
- const EVM_NATIVE_VALUE_TARGET_R3 = BigInt(200e18); // 200 UPC
1002
- let adjustedValue;
1020
+ // Balance-aware *upward* allocation. The previous behaviour silently
1021
+ // clamped to a 200 PC target whenever balance had headroom — which was
1022
+ // a downward clamp when the calibrated value already exceeded 200 PC,
1023
+ // masking under-funded swaps with opaque ExecutionFailed reverts. The
1024
+ // new logic preserves the "consume extra to reduce STF risk" intent for
1025
+ // small calibrated values while never reducing a calibrated value that
1026
+ // happened to land above 200 PC. Down-clamp + zero-balance throws live
1027
+ // in `runPreflight` below.
1028
+ const EVM_NATIVE_VALUE_TARGET_R3 = BigInt(200e18); // 200 UPC target floor
1003
1029
  if (effectiveBalance > EVM_NATIVE_VALUE_TARGET_R3 + OUTBOUND_GAS_RESERVE_R3) {
1004
- adjustedValue = EVM_NATIVE_VALUE_TARGET_R3;
1005
- }
1006
- else if (effectiveBalance > OUTBOUND_GAS_RESERVE_R3) {
1007
- adjustedValue = effectiveBalance - OUTBOUND_GAS_RESERVE_R3;
1008
- }
1009
- else if (effectiveBalance > BigInt(0)) {
1010
- // Drained UEA: overshoot would make the inner .call{value:...} return (false, "")
1011
- // and the UEA reverts with opaque ExecutionFailed(). Clamp to whatever we have so
1012
- // the gateway swap either succeeds or surfaces a concrete revert reason.
1013
- adjustedValue = effectiveBalance < nativeValueForGas ? effectiveBalance : nativeValueForGas;
1014
- }
1015
- else {
1016
- throw new Error(`UEA ${ueaAddress} has zero UPC balance; cannot fund outbound gas swap. ` +
1017
- `Bridge UPC to the UEA before retrying.`);
1030
+ // Balance can cover the target. Bump nativeValueForGas UP to 200 PC,
1031
+ // but never reduce a calibrated value already above 200.
1032
+ const newValue = nativeValueForGas > EVM_NATIVE_VALUE_TARGET_R3
1033
+ ? nativeValueForGas
1034
+ : EVM_NATIVE_VALUE_TARGET_R3;
1035
+ if (newValue !== nativeValueForGas) {
1036
+ (0, context_1.printLog)(ctx, `executeCeaToPush — adjusting nativeValueForGas UP from ${nativeValueForGas.toString()} to ${newValue.toString()} (EVM_NATIVE_VALUE_TARGET_R3 floor)`);
1037
+ nativeValueForGas = newValue;
1038
+ }
1018
1039
  }
1019
- if (adjustedValue !== nativeValueForGas) {
1020
- (0, context_1.printLog)(ctx, `executeCeaToPush — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${adjustedValue.toString()} (effective balance: ${effectiveBalance.toString()})`);
1021
- nativeValueForGas = adjustedValue;
1040
+ // Pre-flight (R3 EVM): no PRC-20 burn check — `burnAmount = 0` makes this
1041
+ // payload-only (UEA never holds the source-chain PRC-20; see plan §9 #4).
1042
+ // Native UPC check still applies for the gas swap.
1043
+ const allowUnderfundedR3Evm = ((_j = params.options) === null || _j === void 0 ? void 0 : _j.allowUnderfundedSwap) === true;
1044
+ const preflightR3Evm = (0, preflight_1.runPreflight)({
1045
+ ctx,
1046
+ ueaAddress,
1047
+ ueaBalance: effectiveBalance,
1048
+ requiredValue: nativeValueForGas,
1049
+ gasReserve: OUTBOUND_GAS_RESERVE_R3,
1050
+ pathTag: 'R3_EVM',
1051
+ allowUnderfundedSwap: allowUnderfundedR3Evm,
1052
+ });
1053
+ if (!preflightR3Evm.ok) {
1054
+ nativeValueForGas = preflightR3Evm.legacyClampedValue;
1055
+ (0, context_1.printLog)(ctx, `executeCeaToPush — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
1022
1056
  }
1023
1057
  // 12. Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1024
1058
  const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
@@ -1084,7 +1118,7 @@ function executeCeaToPush(ctx, params, executeFn) {
1084
1118
  */
1085
1119
  function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1086
1120
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
1087
- var _a, _b, _c, _d;
1121
+ var _a, _b, _c, _d, _e;
1088
1122
  if (typeof params.to !== 'string') {
1089
1123
  throw new Error('Route 3 SVM (CEA -> Push): to must be a Push Chain address (string), not a ChainTarget');
1090
1124
  }
@@ -1136,9 +1170,13 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1136
1170
  revertRecipientHex: ceaPdaHex,
1137
1171
  });
1138
1172
  (0, context_1.printLog)(ctx, `executeCeaToPushSvm — drainAmount: ${drainAmount.toString()}, payload length: ${(svmPayload.length - 2) / 2} bytes`);
1139
- // burnAmount = 1 (minimum for precompile; drain amount lives inside the ixData)
1140
- // The precompile rejects amount=0, so we use BigInt(1) as a workaround.
1141
- const burnAmount = BigInt(1);
1173
+ // burnAmount = 0 (payload-only outbound — drain amount lives inside ixData).
1174
+ // Mirrors the 2026-03-19 cascade fix that removed the 1-wei workaround for
1175
+ // SVM payload-only hops because it caused `InsufficientBalance (0xf4d678b8)`
1176
+ // reverts on fresh UEAs that don't hold the source-chain native PRC-20
1177
+ // (e.g. pSOL). The UGPC upgrade post-2026-03-19 already accepts amount = 0
1178
+ // on the cascade path, and the same precompile is used here.
1179
+ const burnAmount = BigInt(0);
1142
1180
  // Build outbound request: target = gateway program (self-call)
1143
1181
  const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, (_b = params.gasLimit) !== null && _b !== void 0 ? _b : BigInt(0), svmPayload, ueaAddress);
1144
1182
  (0, context_1.printLog)(ctx, `executeCeaToPushSvm — outbound request: target=${outboundReq.target.slice(0, 20)}..., token=${outboundReq.token}`);
@@ -1189,15 +1227,58 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1189
1227
  (0, context_1.printLog)(ctx, `executeCeaToPushSvm — Case C: bumping nativeValueForGas from ${nativeValueForGas} to ${bumped} (overflow=${sizingDecisionR3Svm.overflowNativePc})`);
1190
1228
  nativeValueForGas = bumped;
1191
1229
  }
1192
- // Adjust nativeValueForGas using UEA balance (contract refunds excess)
1193
- // Re-fetch balance to minimize staleness from gas fee query RPC roundtrips
1230
+ // Re-fetch balance to minimize staleness from gas fee query RPC roundtrips.
1194
1231
  const currentBalance = yield ctx.pushClient.getBalance(ueaAddress);
1195
1232
  // Cosmos-EVM tx overhead costs ~1 PC per operation; 3 PC covers approve(s) + buffer.
1196
1233
  const OUTBOUND_GAS_RESERVE_R3_SVM = BigInt(3e18);
1197
- if (currentBalance > OUTBOUND_GAS_RESERVE_R3_SVM && currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM > nativeValueForGas) {
1198
- const adjustedValue = currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM;
1199
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${adjustedValue.toString()} (UEA balance: ${currentBalance.toString()})`);
1200
- nativeValueForGas = adjustedValue;
1234
+ // Upward-allocation ceiling: cap the upward adjust at max(200 PC, 5x calibrated)
1235
+ // so a UEA with a giant balance doesn't submit msg.value=balance for a small
1236
+ // gas swap. The contract refunds excess via swapAndBurnGas, but pushing
1237
+ // hundreds of thousands of PC into a Uniswap pool moves the price for other
1238
+ // users. 200 PC mirrors EVM's safety cap; 5x calibrated covers pool-skew
1239
+ // scenarios where the calibrated value itself is > 40 PC (5x40 = 200).
1240
+ const UPWARD_BASE_CEILING_R3_SVM = BigInt(200) * BigInt(1e18);
1241
+ const calibratedCeiling = nativeValueForGas * BigInt(5);
1242
+ const upwardCeiling = calibratedCeiling > UPWARD_BASE_CEILING_R3_SVM
1243
+ ? calibratedCeiling
1244
+ : UPWARD_BASE_CEILING_R3_SVM;
1245
+ // Upward-only allocation: when balance has headroom over the calibrated
1246
+ // value, consume more of it (up to the ceiling above). Down-clamp +
1247
+ // zero-balance throws live in pre-flight.
1248
+ // Ordering note: pre-flight runs AFTER the upward adjust below; the
1249
+ // semantics are equivalent to running it before — when upward adjust
1250
+ // applies it sets nativeValueForGas to balance-reserve (capped), and the
1251
+ // post-adjust pre-flight check is always satisfied. When upward adjust is
1252
+ // skipped (balance too low), pre-flight runs against the calibrated value
1253
+ // and throws cleanly on shortfall.
1254
+ if (currentBalance > OUTBOUND_GAS_RESERVE_R3_SVM &&
1255
+ currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM > nativeValueForGas) {
1256
+ const balanceDriven = currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM;
1257
+ const adjustedValue = balanceDriven > upwardCeiling ? upwardCeiling : balanceDriven;
1258
+ if (adjustedValue !== nativeValueForGas) {
1259
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${adjustedValue.toString()} ` +
1260
+ `(UEA balance: ${currentBalance.toString()}, upwardCeiling: ${upwardCeiling.toString()})`);
1261
+ nativeValueForGas = adjustedValue;
1262
+ }
1263
+ }
1264
+ // SVM warn-threshold telemetry (no truncation — pre-flight handles drain protection).
1265
+ (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R3_SVM');
1266
+ // Pre-flight (R3 SVM): no PRC-20 burn check — `burnAmount = 1` is a legacy
1267
+ // precompile-validation shim against a token UEAs structurally don't hold
1268
+ // (see plan §9 #4 / §10.2 latent-bug ticket). Native UPC check still applies.
1269
+ const allowUnderfundedR3Svm = ((_e = params.options) === null || _e === void 0 ? void 0 : _e.allowUnderfundedSwap) === true;
1270
+ const preflightR3Svm = (0, preflight_1.runPreflight)({
1271
+ ctx,
1272
+ ueaAddress,
1273
+ ueaBalance: currentBalance,
1274
+ requiredValue: nativeValueForGas,
1275
+ gasReserve: OUTBOUND_GAS_RESERVE_R3_SVM,
1276
+ pathTag: 'R3_SVM',
1277
+ allowUnderfundedSwap: allowUnderfundedR3Svm,
1278
+ });
1279
+ if (!preflightR3Svm.ok) {
1280
+ nativeValueForGas = preflightR3Svm.legacyClampedValue;
1281
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
1201
1282
  }
1202
1283
  // Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1203
1284
  const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Conditional CEA-ATA rent bump for SVM SPL outbounds.
3
+ *
4
+ * The gateway program (svm-gateway) computes per-tx fees with a conditional
5
+ * rent for first-time SPL transfers — see `calculateSplExecuteFees` in
6
+ * `push-chain-gateway-contracts/contracts/svm-gateway/tests/helpers/test-utils.ts`:
7
+ *
8
+ * const ataExists = await ceaAtaExists(connection, ceaAta);
9
+ * const ceaAtaRent = ataExists ? 0n : BigInt(getMinimumBalanceForRentExemption(165));
10
+ * gasFee = executedTxRent + ceaAtaRent + COMPUTE_BUFFER;
11
+ *
12
+ * The gateway's on-chain `getOutboundTxGasAndFees` quote does NOT factor in
13
+ * the ATA rent today, so the SDK has to bump `effectiveGasLimit` itself
14
+ * before calling that quote. Native-SOL outbounds use `calculateSolExecuteFees`
15
+ * which has no conditional rent, so no SDK bump is needed there.
16
+ */
17
+ import { PublicKey } from '@solana/web3.js';
18
+ import type { OrchestratorContext } from './context';
19
+ import type { CHAIN as ChainType } from '../../constants/enums';
20
+ /** ATA rent (165-byte SPL token account) + executed-tx rent (8-byte) + compute buffer.
21
+ * Mirrors getMinimumBalanceForRentExemption(165) ≈ 2_039_280
22
+ * + getMinimumBalanceForRentExemption(8) ≈ 9_200
23
+ * + COMPUTE_BUFFER 100_000.
24
+ * Signature fee (5_000) is already included in the gateway's base quote. */
25
+ export declare const CEA_ATA_RENT_LAMPORTS_BUMP: bigint;
26
+ /** Derive CEA PDA from a 20-byte EVM address. Seeds: ["push_identity", evmBytes]. */
27
+ export declare function deriveSvmCeaPda(evmAddress: `0x${string}`): PublicKey;
28
+ /** Derive a standard SPL Associated Token Account address. */
29
+ export declare function deriveAtaPubkey(owner: PublicKey, mint: PublicKey): PublicKey;
30
+ export interface MaybeBumpInput {
31
+ ctx: OrchestratorContext;
32
+ ueaAddress: `0x${string}`;
33
+ targetChain: ChainType;
34
+ /** SPL mint as base58 (from MoveableToken.address for SVM tokens). undefined ⇒ no SPL. */
35
+ splMintBase58: string | undefined;
36
+ /** Burn amount in token units. 0 ⇒ no SPL transferFrom ⇒ no ATA needed. */
37
+ burnAmount: bigint;
38
+ effectiveGasLimit: bigint;
39
+ }
40
+ /**
41
+ * Returns the (possibly bumped) effectiveGasLimit. Logs the decision via
42
+ * printLog. On any RPC failure or invalid input, logs a warning and returns
43
+ * the input unchanged — better to risk one underflow than to block all SPL
44
+ * outbounds on RPC flake; pre-flight will surface insufficient gas anyway.
45
+ */
46
+ export declare function maybeBumpForCeaAtaRent(input: MaybeBumpInput): Promise<bigint>;