@pushchain/core 5.1.17 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/src/lib/constants/abi/cea.evm.d.ts +52 -0
  3. package/src/lib/constants/abi/cea.evm.js +66 -1
  4. package/src/lib/constants/abi/prc20.evm.d.ts +117 -11
  5. package/src/lib/constants/abi/prc20.evm.js +38 -11
  6. package/src/lib/constants/abi/uea.evm.js +41 -0
  7. package/src/lib/constants/abi/universalGatewayPC.evm.d.ts +117 -1
  8. package/src/lib/constants/abi/universalGatewayPC.evm.js +40 -1
  9. package/src/lib/constants/abi/universalGatewayV0.json +1 -1
  10. package/src/lib/constants/chain.js +14 -14
  11. package/src/lib/constants/index.d.ts +1 -1
  12. package/src/lib/constants/index.js +2 -1
  13. package/src/lib/constants/selectors.d.ts +8 -0
  14. package/src/lib/constants/selectors.js +9 -1
  15. package/src/lib/constants/tokens.js +1 -0
  16. package/src/lib/formatters.d.ts +7 -0
  17. package/src/lib/formatters.js +46 -0
  18. package/src/lib/index.d.ts +2 -0
  19. package/src/lib/index.js +7 -1
  20. package/src/lib/orchestrator/internals/cascade.d.ts +8 -1
  21. package/src/lib/orchestrator/internals/cascade.js +593 -192
  22. package/src/lib/orchestrator/internals/error-decoder.d.ts +2 -6
  23. package/src/lib/orchestrator/internals/error-decoder.js +10 -10
  24. package/src/lib/orchestrator/internals/errors.d.ts +1 -1
  25. package/src/lib/orchestrator/internals/errors.js +8 -5
  26. package/src/lib/orchestrator/internals/execute-funds-only.js +3 -2
  27. package/src/lib/orchestrator/internals/execute-funds-payload.js +2 -1
  28. package/src/lib/orchestrator/internals/execute-standard.js +11 -10
  29. package/src/lib/orchestrator/internals/gas-calculator.d.ts +1 -0
  30. package/src/lib/orchestrator/internals/gas-calculator.js +91 -22
  31. package/src/lib/orchestrator/internals/helpers.d.ts +2 -1
  32. package/src/lib/orchestrator/internals/helpers.js +14 -11
  33. package/src/lib/orchestrator/internals/inbound-tracker.js +4 -2
  34. package/src/lib/orchestrator/internals/outbound-sync.d.ts +1 -0
  35. package/src/lib/orchestrator/internals/outbound-sync.js +140 -71
  36. package/src/lib/orchestrator/internals/pc-usd-oracle.d.ts +1 -1
  37. package/src/lib/orchestrator/internals/pc-usd-oracle.js +4 -3
  38. package/src/lib/orchestrator/internals/preflight.d.ts +2 -16
  39. package/src/lib/orchestrator/internals/preflight.js +2 -36
  40. package/src/lib/orchestrator/internals/push-chain-tx.js +5 -1
  41. package/src/lib/orchestrator/internals/response-builder.js +23 -7
  42. package/src/lib/orchestrator/internals/route-handlers.js +182 -227
  43. package/src/lib/orchestrator/internals/signing.js +7 -2
  44. package/src/lib/orchestrator/internals/svm-helpers.js +3 -3
  45. package/src/lib/orchestrator/internals/svm-rent.d.ts +42 -9
  46. package/src/lib/orchestrator/internals/svm-rent.js +148 -14
  47. package/src/lib/orchestrator/internals/tx-transformer.js +5 -2
  48. package/src/lib/orchestrator/max-pc-for-gas.d.ts +60 -0
  49. package/src/lib/orchestrator/max-pc-for-gas.js +66 -0
  50. package/src/lib/orchestrator/orchestrator.d.ts +8 -1
  51. package/src/lib/orchestrator/orchestrator.js +40 -2
  52. package/src/lib/orchestrator/orchestrator.types.d.ts +35 -23
  53. package/src/lib/orchestrator/payload-builders.d.ts +7 -6
  54. package/src/lib/orchestrator/payload-builders.js +18 -6
  55. package/src/lib/progress-hook/progress-hook.js +39 -38
  56. package/src/lib/push-chain/push-chain.d.ts +11 -1
  57. package/src/lib/push-chain/push-chain.js +2 -2
  58. package/src/lib/universal-tx-detector/cascade.js +14 -4
  59. package/src/lib/universal-tx-detector/child-inbounds.js +9 -1
  60. package/src/lib/universal-tx-detector/detector.js +9 -3
  61. package/src/lib/universal-tx-detector/events.d.ts +3 -1
  62. package/src/lib/universal-tx-detector/events.js +3 -1
  63. package/src/lib/universal-tx-detector/normalize.d.ts +42 -0
  64. package/src/lib/universal-tx-detector/normalize.js +127 -0
  65. package/src/lib/universal-tx-detector/svm-events.d.ts +1 -1
  66. package/src/lib/universal-tx-detector/svm-events.js +1 -1
  67. package/src/lib/utils/external-tx-hash.d.ts +37 -0
  68. package/src/lib/utils/external-tx-hash.js +62 -0
@@ -112,6 +112,37 @@ function resolveR2Prc20TokenSvm(params, targetChain, pushNetwork, hasSvmExecute)
112
112
  }
113
113
  return { prc20Token, burnAmount };
114
114
  }
115
+ function resolveR3SvmDrain(params, sourceChain, pushNetwork) {
116
+ var _a;
117
+ let drainAmount = BigInt(0);
118
+ let tokenMintHex;
119
+ let prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, pushNetwork);
120
+ if (((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) && params.funds.amount > BigInt(0)) {
121
+ drainAmount = params.funds.amount;
122
+ const token = params.funds.token;
123
+ if ((token === null || token === void 0 ? void 0 : token.mechanism) === 'approve') {
124
+ const mintPk = new web3_js_1.PublicKey(token.address);
125
+ tokenMintHex = ('0x' + Buffer.from(mintPk.toBytes()).toString('hex'));
126
+ prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token).address;
127
+ }
128
+ }
129
+ else if (params.value && params.value > BigInt(0)) {
130
+ drainAmount = params.value;
131
+ }
132
+ return { prc20Token, drainAmount, tokenMintHex };
133
+ }
134
+ function buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, inboundNonce) {
135
+ if (!params.data) {
136
+ return undefined;
137
+ }
138
+ const executeParams = (0, helpers_1.toExecuteParams)(params);
139
+ const multicallCtx = Object.assign(Object.assign({}, ctx), { universalSigner: Object.assign(Object.assign({}, ctx.universalSigner), { account: Object.assign(Object.assign({}, ctx.universalSigner.account), { chain: sourceChain }) }) });
140
+ const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(multicallCtx, executeParams.to, (0, payload_builders_1.buildExecuteMulticall)({ execute: executeParams, ueaAddress }));
141
+ const inboundPayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, {
142
+ nonce: inboundNonce !== null && inboundNonce !== void 0 ? inboundNonce : BigInt(0),
143
+ });
144
+ return (0, viem_1.hexToBytes)(inboundPayload);
145
+ }
115
146
  /**
116
147
  * Build the CEA-side multicall payload for a Route 2 EVM outbound. Needs the
117
148
  * resolved `ceaAddress` because the native-value self-call branch skips the
@@ -337,6 +368,7 @@ function executeUoaToCea(ctx, params, executeFn) {
337
368
  // --- 202: Gas estimation (spec-ordered before 203) ---
338
369
  let gasFee = BigInt(0);
339
370
  let protocolFee = BigInt(0);
371
+ let outboundGasPrice = BigInt(0);
340
372
  let gasToken = selectors_1.ZERO_ADDRESS;
341
373
  let universalCoreAddress;
342
374
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
@@ -345,6 +377,7 @@ function executeUoaToCea(ctx, params, executeFn) {
345
377
  const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimitForQuery, targetChain);
346
378
  gasFee = result.gasFee;
347
379
  protocolFee = result.protocolFee;
380
+ outboundGasPrice = result.gasPrice;
348
381
  gasToken = result.gasToken;
349
382
  universalCoreAddress = result.universalCoreAddress;
350
383
  (0, context_1.printLog)(ctx, `executeUoaToCea — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}`);
@@ -376,19 +409,20 @@ function executeUoaToCea(ctx, params, executeFn) {
376
409
  (0, context_1.printLog)(ctx, `executeUoaToCea — MIGRATION: using raw MIGRATION_SELECTOR payload (${ceaPayload}), native PRC-20 ${prc20Token}`);
377
410
  }
378
411
  // Build outbound request struct for the gateway
379
- // NOTE: `target` is a LEGACY/DUMMY parameter for contract compatibility.
380
- // The deployed UniversalGatewayPC still expects this field, but the relay does NOT use it
381
- // to determine the actual destination. The relay determines destination from the PRC-20 token's
382
- // SOURCE_CHAIN_NAMESPACE. We pass the CEA address as a non-zero placeholder.
383
- // This field will be removed in future contract upgrades.
384
- const targetBytes = ceaAddress; // Dummy value - any non-zero address works
385
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, gasLimitForQuery, ceaPayload, ueaAddress // revert recipient is the UEA
386
- );
412
+ // Empty CEA payloads intentionally use zero recipient to park funds in the
413
+ // CEA under the post-audit single-call guard.
414
+ const targetBytes = ceaPayload === '0x'
415
+ ? selectors_1.ZERO_ADDRESS
416
+ : ceaAddress;
417
+ (0, payload_builders_1.assertCeaFundsParkingInvariant)(targetBytes, ceaPayload);
418
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, gasLimitForQuery, ceaPayload, ueaAddress, // revert recipient is the UEA
419
+ (_d = params.maxPCForGas) !== null && _d !== void 0 ? _d : BigInt(0), outboundGasPrice);
387
420
  (0, context_1.printLog)(ctx, `executeUoaToCea — outbound request: ${JSON.stringify({
388
421
  target: outboundReq.target,
389
422
  token: outboundReq.token,
390
423
  amount: outboundReq.amount.toString(),
391
424
  gasLimit: outboundReq.gasLimit.toString(),
425
+ maxPCForGas: outboundReq.maxPCForGas.toString(),
392
426
  payloadLength: outboundReq.payload.length,
393
427
  revertRecipient: outboundReq.revertRecipient,
394
428
  }, null, 2)}`);
@@ -430,12 +464,16 @@ function executeUoaToCea(ctx, params, executeFn) {
430
464
  }
431
465
  }
432
466
  }
433
- // Pool-quote + 10% safety buffer. Over-send is refunded by the contract.
467
+ // estimateNativeValueForSwap applies the full 2.2x safety buffer
468
+ // internally (pool-slippage cushion + the 10% headroom that used to be
469
+ // multiplied here). Folded into one place so the function's cap branch
470
+ // bounds the final value — the previous outer ×110/100 on top of the
471
+ // inner cap broke preflight whenever the cap fired. Over-send is refunded
472
+ // by the contract.
434
473
  let nativeValueForGas = BigInt(0);
435
474
  if (universalCoreAddress && gasFee > BigInt(0)) {
436
- const estimated = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, effectiveBalance);
437
- nativeValueForGas = (estimated * BigInt(110)) / BigInt(100);
438
- (0, context_1.printLog)(ctx, `executeUoaToCea — nativeValueForGas: pool-quote=${estimated.toString()}, with 10% buffer=${nativeValueForGas.toString()}`);
475
+ nativeValueForGas = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, effectiveBalance);
476
+ (0, context_1.printLog)(ctx, `executeUoaToCea — nativeValueForGas=${nativeValueForGas.toString()} (includes 2.2x buffer)`);
439
477
  }
440
478
  // Hard safety cap so a broken pool price can't drain the UEA.
441
479
  if (nativeValueForGas > EVM_NATIVE_VALUE_SAFETY_CAP) {
@@ -457,8 +495,7 @@ function executeUoaToCea(ctx, params, executeFn) {
457
495
  args: [ueaAddress],
458
496
  });
459
497
  }
460
- const allowUnderfundedR2Evm = ((_d = params.options) === null || _d === void 0 ? void 0 : _d.allowUnderfundedSwap) === true;
461
- const preflightR2Evm = (0, preflight_1.runPreflight)({
498
+ (0, preflight_1.runPreflight)({
462
499
  ctx,
463
500
  ueaAddress,
464
501
  ueaBalance: effectiveBalance,
@@ -468,13 +505,8 @@ function executeUoaToCea(ctx, params, executeFn) {
468
505
  burnToken: prc20Token,
469
506
  burnAmount,
470
507
  prc20Balance: prc20BalanceR2Evm,
471
- allowUnderfundedSwap: allowUnderfundedR2Evm,
472
508
  });
473
- if (!preflightR2Evm.ok) {
474
- nativeValueForGas = preflightR2Evm.legacyClampedValue;
475
- (0, context_1.printLog)(ctx, `executeUoaToCea — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
476
- }
477
- // ok=true → nativeValueForGas already covers required + reserve; no adjust needed.
509
+ // Preflight success means nativeValueForGas already covers required + reserve; no adjust needed.
478
510
  // Build outbound multicalls (approve burn + sendUniversalTxOutbound with native value)
479
511
  const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
480
512
  prc20Token,
@@ -536,7 +568,7 @@ function executeUoaToCea(ctx, params, executeFn) {
536
568
  */
537
569
  function executeUoaToCeaSvm(ctx, params, target, executeFn) {
538
570
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
539
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
571
+ var _a, _b, _c, _d, _e, _f, _g, _h;
540
572
  const targetChain = target.chain;
541
573
  const targetAddress = target.address; // 0x-prefixed, 32 bytes
542
574
  const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
@@ -563,44 +595,49 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
563
595
  // --- 202: Gas estimation (spec-ordered before 203) ---
564
596
  let gasFee = BigInt(0);
565
597
  let protocolFeeSvm = BigInt(0);
598
+ let outboundGasPriceSvm = BigInt(0);
566
599
  let gasToken = selectors_1.ZERO_ADDRESS;
567
600
  let universalCoreAddress;
568
601
  // Effective gas limit seeded from the user param. When the user omits
569
- // gasLimit, it'll be derived from the gasFee/gasPrice response so the
570
- // on-chain record carries a non-zero compute budget the relay can use.
602
+ // gasLimit, it'll be derived from the gas quote so the on-chain record
603
+ // carries a non-zero compute budget the relay can use.
571
604
  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
605
+ // SVM finalize budget: the Solana gateway requires gas_fee to cover the
606
+ // relayer signature fee, ExecutedSubTx rent, a compute buffer, and
607
+ // conditionally CEA ATA rent for SPL outbounds. The on-chain quote can be
608
+ // below that floor, so we compare the quoted fee in lamports and re-query
609
+ // with a bumped gasLimit when required.
610
+ const svmFundsToken = burnAmount > BigInt(0)
611
+ ? (_e = params.funds) === null || _e === void 0 ? void 0 : _e.token
579
612
  : undefined;
580
- effectiveGasLimit = yield (0, svm_rent_1.maybeBumpForCeaAtaRent)({
581
- ctx,
582
- ueaAddress,
583
- targetChain,
584
- splMintBase58,
585
- burnAmount,
586
- effectiveGasLimit,
587
- });
613
+ const splMintBase58 = (svmFundsToken === null || svmFundsToken === void 0 ? void 0 : svmFundsToken.mechanism) === 'approve' ? svmFundsToken.address : undefined;
588
614
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
589
615
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_01, targetChain);
590
616
  try {
591
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, effectiveGasLimit, targetChain);
617
+ let result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, effectiveGasLimit, targetChain);
618
+ result = yield (0, svm_rent_1.ensureSvmFinalizeGasBudgetQuote)({
619
+ ctx,
620
+ ueaAddress,
621
+ targetChain,
622
+ prc20Token,
623
+ quote: result,
624
+ splMintBase58,
625
+ burnAmount,
626
+ pathTag: 'executeUoaToCeaSvm',
627
+ });
592
628
  gasFee = result.gasFee;
593
629
  protocolFeeSvm = result.protocolFee;
630
+ outboundGasPriceSvm = result.gasPrice;
594
631
  gasToken = result.gasToken;
595
632
  universalCoreAddress = result.universalCoreAddress;
596
- // When user omits gasLimit (sent as 0), the contract computes fees using its internal
597
- // baseGasLimitByChainNamespace. But the relay reads the stored gasLimit=0 from the
598
- // on-chain outbound record and uses it as the Solana compute budget — 0 CU means the
599
- // relay cannot execute the tx. Derive the effective limit from gasFee/gasPrice so
600
- // the stored record has a non-zero compute budget the relay can use.
601
- if (!params.gasLimit && result.gasPrice > BigInt(0)) {
602
- effectiveGasLimit = result.gasFee / result.gasPrice;
603
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — derived effectiveGasLimit: ${effectiveGasLimit} (gasFee=${result.gasFee} / gasPrice=${result.gasPrice})`);
633
+ // When user omits gasLimit (sent as 0), the contract computes fees using
634
+ // its internal baseGasLimitByChainNamespace. But the relay reads the
635
+ // stored gasLimit from the outbound record. Store the resolved value from
636
+ // UniversalCore so the relayer has a non-zero SVM budget.
637
+ effectiveGasLimit = result.gasLimitUsed;
638
+ if (!params.gasLimit) {
639
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — resolved effectiveGasLimit: ${effectiveGasLimit} ` +
640
+ `(gasFee=${result.gasFee}, gasPrice=${result.gasPrice})`);
604
641
  }
605
642
  (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}`);
606
643
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_02, targetChain, protocolFeeSvm, gasFee);
@@ -616,13 +653,14 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
616
653
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_02, ueaAddress, targetAddress, targetChain, true);
617
654
  // --- Build outbound request ---
618
655
  // targetBytes: for execute = program id (resolver output); for withdraw = to.address
619
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress // revert recipient is the UEA
620
- );
656
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress, // revert recipient is the UEA
657
+ (_f = params.maxPCForGas) !== null && _f !== void 0 ? _f : BigInt(0), outboundGasPriceSvm);
621
658
  (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — outbound request: ${JSON.stringify({
622
659
  target: outboundReq.target,
623
660
  token: outboundReq.token,
624
661
  amount: outboundReq.amount.toString(),
625
662
  gasLimit: outboundReq.gasLimit.toString(),
663
+ maxPCForGas: outboundReq.maxPCForGas.toString(),
626
664
  payloadLength: (outboundReq.payload.length - 2) / 2,
627
665
  revertRecipient: outboundReq.revertRecipient,
628
666
  }, null, 2)}`);
@@ -648,8 +686,10 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
648
686
  }
649
687
  const ueaNonce = (!isUEADeployed || isNativePushEOASvm) ? BigInt(0) : yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
650
688
  // R2 does NOT use Case A/B/C gas abstraction (scoped to R1 + R3 only).
651
- // Size msg.value from the live WPC/gasToken Uniswap V3 pool quote plus
652
- // a 10% safety buffer. swapAndBurnGas refunds the excess as PC via
689
+ // Size msg.value from the live WPC/gasToken Uniswap V3 pool quote. The
690
+ // 2.2x safety buffer is applied inside estimateNativeValueForSwap (was
691
+ // split as inner 2x + outer 10% — combined to keep the cap branch
692
+ // mathematically correct). swapAndBurnGas refunds the excess as PC via
653
693
  // refundUnusedGas.
654
694
  //
655
695
  // SVM pools (WPC/pSOL) are thinner than EVM pools so the pool-quote can
@@ -664,9 +704,10 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
664
704
  const SVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for outer-tx gas
665
705
  let nativeValueForGas = BigInt(0);
666
706
  if (universalCoreAddress && gasFee > BigInt(0)) {
667
- const estimated = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, ueaBalance);
668
- nativeValueForGas = (estimated * BigInt(110)) / BigInt(100);
669
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — nativeValueForGas: pool-quote=${estimated.toString()}, with 10% buffer=${nativeValueForGas.toString()}`);
707
+ // 2.2x safety buffer is applied inside estimateNativeValueForSwap so
708
+ // the function's balance-cap branch bounds the final value correctly.
709
+ nativeValueForGas = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, ueaBalance);
710
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — nativeValueForGas=${nativeValueForGas.toString()} (includes 2.2x buffer)`);
670
711
  (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R2_SVM');
671
712
  }
672
713
  // Pre-flight: throws InsufficientUEABalanceError when UEA cannot cover
@@ -688,8 +729,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
688
729
  args: [ueaAddress],
689
730
  });
690
731
  }
691
- const allowUnderfundedR2Svm = ((_j = params.options) === null || _j === void 0 ? void 0 : _j.allowUnderfundedSwap) === true;
692
- const preflightR2Svm = (0, preflight_1.runPreflight)({
732
+ (0, preflight_1.runPreflight)({
693
733
  ctx,
694
734
  ueaAddress,
695
735
  ueaBalance,
@@ -699,12 +739,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
699
739
  burnToken: prc20Token,
700
740
  burnAmount,
701
741
  prc20Balance: prc20BalanceR2Svm,
702
- allowUnderfundedSwap: allowUnderfundedR2Svm,
703
742
  });
704
- if (!preflightR2Svm.ok) {
705
- nativeValueForGas = preflightR2Svm.legacyClampedValue;
706
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
707
- }
708
743
  // --- Build Push Chain multicalls (approve + sendUniversalTxOutbound) ---
709
744
  // Reuse the same builder as EVM — this part is identical
710
745
  const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
@@ -769,7 +804,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
769
804
  */
770
805
  function executeCeaToPush(ctx, params, executeFn) {
771
806
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
772
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
807
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
773
808
  // 1. Validate and extract source chain
774
809
  if (!((_a = params.from) === null || _a === void 0 ? void 0 : _a.chain)) {
775
810
  throw new Error('Route 3 (CEA -> Push) requires from.chain to specify the source CEA chain');
@@ -806,11 +841,6 @@ function executeCeaToPush(ctx, params, executeFn) {
806
841
  (0, context_1.printLog)(ctx, `executeCeaToPush — sourceChain: ${sourceChain}, CEA: ${ceaAddress}, deployed: ${ceaDeployed}`);
807
842
  // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
808
843
  // if CEA doesn't exist yet. No SDK-side blocking needed.
809
- // 5. Get UniversalGateway address on source chain
810
- const gatewayAddress = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
811
- if (!gatewayAddress) {
812
- throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
813
- }
814
844
  // 6. Build multicall for CEA to execute on source chain (self-calls via sendUniversalTxToUEA)
815
845
  const ceaMulticalls = [];
816
846
  // Determine token and amount for sendUniversalTxToUEA
@@ -826,7 +856,7 @@ function executeCeaToPush(ctx, params, executeFn) {
826
856
  amount = params.funds.amount;
827
857
  }
828
858
  else {
829
- // ERC20 token - need approval for gateway before sendUniversalTxToUEA
859
+ // ERC20 token. CEA handles gateway approval internally around sendUniversalTxToUEA.
830
860
  tokenAddress = token.address;
831
861
  amount = params.funds.amount;
832
862
  }
@@ -843,33 +873,7 @@ function executeCeaToPush(ctx, params, executeFn) {
843
873
  // query and relay execution, causing sendUniversalTxToUEA to revert with
844
874
  // InsufficientBalance. Pre-existing CEA funds remain parked and can be swept separately.
845
875
  const bridgeAmount = amount;
846
- // Note: CEA contract may reject amount=0 in sendUniversalTxToUEA.
847
- // Keeping bridgeAmount as-is (0) for payload-only to test precompile behavior.
848
- // For ERC20 tokens, add approve call for the bridge amount
849
- // (CEA approves gateway to spend the Vault-deposited amount)
850
- // Reset to 0 first for USDT-style tokens that revert on non-zero to non-zero approve.
851
- if (tokenAddress !== selectors_1.ZERO_ADDRESS && bridgeAmount > BigInt(0)) {
852
- const approveZeroData = (0, viem_1.encodeFunctionData)({
853
- abi: erc20_evm_1.ERC20_EVM,
854
- functionName: 'approve',
855
- args: [gatewayAddress, BigInt(0)],
856
- });
857
- ceaMulticalls.push({
858
- to: tokenAddress,
859
- value: BigInt(0),
860
- data: approveZeroData,
861
- });
862
- const approveData = (0, viem_1.encodeFunctionData)({
863
- abi: erc20_evm_1.ERC20_EVM,
864
- functionName: 'approve',
865
- args: [gatewayAddress, bridgeAmount],
866
- });
867
- ceaMulticalls.push({
868
- to: tokenAddress,
869
- value: BigInt(0),
870
- data: approveData,
871
- });
872
- }
876
+ // The post-audit CEA allows amount=0 for payload-only sendUniversalTxToUEA.
873
877
  // Pre-fetch UEA nonce — needed for the inbound UniversalPayload struct
874
878
  // Skip getCode if accountStatusCache already confirmed deployment
875
879
  let isUEADeployed;
@@ -903,8 +907,8 @@ function executeCeaToPush(ctx, params, executeFn) {
903
907
  // so the inbound will arrive when the UEA nonce is already incremented.
904
908
  pushPayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: ueaNonce + BigInt(1) });
905
909
  }
906
- // Build sendUniversalTxToUEA self-call on CEA
907
- // CEA multicall: to=CEA (self-call), value=0
910
+ // Build sendUniversalTxToUEA self-call on CEA. CEA self-calls must always
911
+ // use value=0; native sends spend from the CEA's existing balance.
908
912
  // CEA internally calls gateway.sendUniversalTxFromCEA(...)
909
913
  // Uses bridgeAmount (= burn amount deposited by Vault)
910
914
  const sendUniversalTxCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, // to: CEA address (self-call)
@@ -929,20 +933,22 @@ function executeCeaToPush(ctx, params, executeFn) {
929
933
  // tokens FROM the CEA back to Push Chain, not to round-trip them.
930
934
  const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
931
935
  const burnAmount = BigInt(0);
936
+ const outboundGasLimit = (_f = params.gasLimit) !== null && _f !== void 0 ? _f : selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
932
937
  (0, context_1.printLog)(ctx, `executeCeaToPush — prc20Token: ${prc20Token}, burnAmount: ${burnAmount.toString()}`);
933
938
  // 9. Build outbound request (same structure as Route 2)
934
939
  // target = CEA address (for self-execution), value = 0 in payload
935
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, // target: CEA address (to=CEA for self-execution)
940
+ let outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, // target: CEA address (to=CEA for self-execution)
936
941
  prc20Token, // token: native PRC-20 for source chain (for namespace lookup)
937
942
  burnAmount, // amount: 0 (payload-only, CEA uses its own balance)
938
- (_f = params.gasLimit) !== null && _f !== void 0 ? _f : BigInt(0), ceaPayload, // payload: ABI-encoded CEA multicall
939
- ueaAddress // revertRecipient: UEA
940
- );
943
+ outboundGasLimit, ceaPayload, // payload: ABI-encoded CEA multicall
944
+ ueaAddress, // revertRecipient: UEA
945
+ (_g = params.maxPCForGas) !== null && _g !== void 0 ? _g : BigInt(0));
941
946
  (0, context_1.printLog)(ctx, `executeCeaToPush — outbound request: ${JSON.stringify({
942
947
  target: outboundReq.target,
943
948
  token: outboundReq.token,
944
949
  amount: outboundReq.amount.toString(),
945
950
  gasLimit: outboundReq.gasLimit.toString(),
951
+ maxPCForGas: outboundReq.maxPCForGas.toString(),
946
952
  payloadLength: outboundReq.payload.length,
947
953
  revertRecipient: outboundReq.revertRecipient,
948
954
  }, null, 2)}`);
@@ -953,20 +959,22 @@ function executeCeaToPush(ctx, params, executeFn) {
953
959
  // 11. Query gas fees from UniversalCore
954
960
  let gasFee = BigInt(0);
955
961
  let protocolFeeR3 = BigInt(0);
962
+ let outboundGasPriceR3 = BigInt(0);
956
963
  let nativeValueForGas = BigInt(0);
957
964
  let gasToken = selectors_1.ZERO_ADDRESS;
958
965
  let sizingDecisionR3;
959
966
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
960
- const gasLimit = outboundReq.gasLimit;
961
967
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01, sourceChain);
962
968
  try {
963
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, sourceChain);
969
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundGasLimit, sourceChain);
964
970
  gasToken = result.gasToken;
965
971
  gasFee = result.gasFee;
972
+ outboundGasPriceR3 = result.gasPrice;
966
973
  protocolFeeR3 = result.protocolFee;
967
974
  nativeValueForGas = result.nativeValueForGas;
968
975
  sizingDecisionR3 = result.sizing;
969
- (0, context_1.printLog)(ctx, `executeCeaToPush — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}, sizing=${(_h = (_g = result.sizing) === null || _g === void 0 ? void 0 : _g.category) !== null && _h !== void 0 ? _h : 'legacy'}`);
976
+ outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, outboundGasLimit, ceaPayload, ueaAddress, (_h = params.maxPCForGas) !== null && _h !== void 0 ? _h : BigInt(0), outboundGasPriceR3);
977
+ (0, context_1.printLog)(ctx, `executeCeaToPush — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}, sizing=${(_k = (_j = result.sizing) === null || _j === void 0 ? void 0 : _j.category) !== null && _k !== void 0 ? _k : 'legacy'}`);
970
978
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02, sourceChain, protocolFeeR3, gasFee);
971
979
  }
972
980
  catch (err) {
@@ -1040,20 +1048,14 @@ function executeCeaToPush(ctx, params, executeFn) {
1040
1048
  // Pre-flight (R3 EVM): no PRC-20 burn check — `burnAmount = 0` makes this
1041
1049
  // payload-only (UEA never holds the source-chain PRC-20; see plan §9 #4).
1042
1050
  // 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)({
1051
+ (0, preflight_1.runPreflight)({
1045
1052
  ctx,
1046
1053
  ueaAddress,
1047
1054
  ueaBalance: effectiveBalance,
1048
1055
  requiredValue: nativeValueForGas,
1049
1056
  gasReserve: OUTBOUND_GAS_RESERVE_R3,
1050
1057
  pathTag: 'R3_EVM',
1051
- allowUnderfundedSwap: allowUnderfundedR3Evm,
1052
1058
  });
1053
- if (!preflightR3Evm.ok) {
1054
- nativeValueForGas = preflightR3Evm.legacyClampedValue;
1055
- (0, context_1.printLog)(ctx, `executeCeaToPush — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
1056
- }
1057
1059
  // 12. Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1058
1060
  const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
1059
1061
  prc20Token,
@@ -1132,41 +1134,31 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1132
1134
  const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
1133
1135
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_301, sourceChain, lockerContract);
1134
1136
  (0, context_1.printLog)(ctx, `executeCeaToPushSvm — sourceChain: ${sourceChain}, gateway: ${lockerContract}`);
1135
- // Determine drain amount and token
1136
- let drainAmount = BigInt(0);
1137
- let tokenMintHex;
1138
- if (((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) && params.funds.amount > BigInt(0)) {
1139
- // SPL token drain
1140
- drainAmount = params.funds.amount;
1141
- const token = params.funds.token;
1142
- if (token && token.address) {
1143
- // Convert SPL mint address to 32-byte hex
1144
- const mintPk = new web3_js_1.PublicKey(token.address);
1145
- tokenMintHex = ('0x' + Buffer.from(mintPk.toBytes()).toString('hex'));
1146
- }
1147
- }
1148
- else if (params.value && params.value > BigInt(0)) {
1149
- // Native SOL drain
1150
- drainAmount = params.value;
1151
- }
1152
- // Route 3 SVM: ALWAYS use native PRC-20 for chain namespace lookup + gas fees.
1153
- // CEA uses its own pre-existing balance — no PRC-20 burn needed on Push Chain.
1154
- const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
1155
- // Build the SVM CPI payload (send_universal_tx_to_uea wrapped in execute)
1156
- // If params.data is provided, pass it as extraPayload for Push Chain execution
1157
- let extraPayload;
1158
- if (params.data && typeof params.data === 'string') {
1159
- extraPayload = (0, viem_1.hexToBytes)(params.data);
1160
- }
1137
+ // Route 3 SVM: CEA uses its own pre-existing balance — no PRC-20 burn
1138
+ // needed on Push Chain. The outer request token must still match the source
1139
+ // asset so the relayer supplies the same Solana mint that the payload
1140
+ // encodes; otherwise the gateway rejects with InvalidMint.
1141
+ const { prc20Token, drainAmount, tokenMintHex } = resolveR3SvmDrain(params, sourceChain, ctx.pushNetwork);
1161
1142
  // Derive CEA PDA as revert recipient: ["push_identity", ueaAddress_20_bytes]
1162
1143
  const ueaBytes = Buffer.from(ueaAddress.slice(2), 'hex'); // 20 bytes
1163
1144
  const [ceaPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes], programPk);
1164
1145
  const ceaPdaHex = ('0x' + Buffer.from(ceaPda.toBytes()).toString('hex'));
1146
+ // Pre-fetch UEA status before payload construction. The inbound
1147
+ // UniversalPayload must use the nonce after this outbound execution, because
1148
+ // the Push-side execute() below consumes one UEA nonce before the Solana
1149
+ // round-trip payload returns.
1150
+ const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
1151
+ const [ueaCode, ueaBalance] = yield Promise.all([
1152
+ ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
1153
+ ctx.pushClient.getBalance(ueaAddress),
1154
+ ]);
1155
+ const isUEADeployed = ueaCode !== undefined;
1156
+ const ueaNonce = isUEADeployed ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress) : BigInt(0);
1165
1157
  const svmPayload = (0, payload_builders_1.encodeSvmCeaToUeaPayload)({
1166
1158
  gatewayProgramHex,
1167
1159
  drainAmount,
1168
1160
  tokenMintHex,
1169
- extraPayload,
1161
+ extraPayload: buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, ueaNonce + BigInt(1)),
1170
1162
  revertRecipientHex: ceaPdaHex,
1171
1163
  });
1172
1164
  (0, context_1.printLog)(ctx, `executeCeaToPushSvm — drainAmount: ${drainAmount.toString()}, payload length: ${(svmPayload.length - 2) / 2} bytes`);
@@ -1177,33 +1169,42 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1177
1169
  // (e.g. pSOL). The UGPC upgrade post-2026-03-19 already accepts amount = 0
1178
1170
  // on the cascade path, and the same precompile is used here.
1179
1171
  const burnAmount = BigInt(0);
1172
+ let effectiveGasLimit = (_a = params.gasLimit) !== null && _a !== void 0 ? _a : BigInt(0);
1180
1173
  // Build outbound request: target = gateway program (self-call)
1181
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, (_b = params.gasLimit) !== null && _b !== void 0 ? _b : BigInt(0), svmPayload, ueaAddress);
1182
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — outbound request: target=${outboundReq.target.slice(0, 20)}..., token=${outboundReq.token}`);
1183
- // Pre-fetch UEA status early — balance is needed for gas value calculation
1184
- const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
1185
- const [ueaCode, ueaBalance] = yield Promise.all([
1186
- ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
1187
- ctx.pushClient.getBalance(ueaAddress),
1188
- ]);
1189
- const isUEADeployed = ueaCode !== undefined;
1190
- const ueaNonce = isUEADeployed ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress) : BigInt(0);
1174
+ let outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress, (_b = params.maxPCForGas) !== null && _b !== void 0 ? _b : BigInt(0));
1175
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — outbound request: target=${outboundReq.target.slice(0, 20)}..., token=${outboundReq.token}, maxPCForGas=${outboundReq.maxPCForGas}`);
1191
1176
  // Query gas fees
1192
1177
  let gasFee = BigInt(0);
1193
1178
  let protocolFeeR3Svm = BigInt(0);
1179
+ let outboundGasPriceR3Svm = BigInt(0);
1194
1180
  let nativeValueForGas = BigInt(0);
1195
1181
  let gasToken = selectors_1.ZERO_ADDRESS;
1196
1182
  let sizingDecisionR3Svm;
1197
1183
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
1198
1184
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01, sourceChain);
1199
1185
  try {
1200
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundReq.gasLimit, sourceChain);
1186
+ let result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundReq.gasLimit, sourceChain);
1187
+ result = yield (0, svm_rent_1.ensureSvmFinalizeGasBudgetQuote)({
1188
+ ctx,
1189
+ ueaAddress,
1190
+ targetChain: sourceChain,
1191
+ prc20Token,
1192
+ quote: result,
1193
+ splMintBase58: undefined,
1194
+ burnAmount: BigInt(0),
1195
+ pathTag: 'executeCeaToPushSvm',
1196
+ });
1201
1197
  gasToken = result.gasToken;
1202
1198
  gasFee = result.gasFee;
1199
+ outboundGasPriceR3Svm = result.gasPrice;
1203
1200
  protocolFeeR3Svm = result.protocolFee;
1204
1201
  nativeValueForGas = result.nativeValueForGas;
1205
1202
  sizingDecisionR3Svm = result.sizing;
1206
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — gasFee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}, sizing=${(_d = (_c = result.sizing) === null || _c === void 0 ? void 0 : _c.category) !== null && _d !== void 0 ? _d : 'legacy'}`);
1203
+ effectiveGasLimit = result.gasLimitUsed;
1204
+ outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress, (_c = params.maxPCForGas) !== null && _c !== void 0 ? _c : BigInt(0), outboundGasPriceR3Svm);
1205
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — resolved effectiveGasLimit: ${effectiveGasLimit} ` +
1206
+ `(gasFee=${result.gasFee}, gasPrice=${result.gasPrice})`);
1207
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — gasFee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}, sizing=${(_e = (_d = result.sizing) === null || _d === void 0 ? void 0 : _d.category) !== null && _e !== void 0 ? _e : 'legacy'}`);
1207
1208
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02, sourceChain, protocolFeeR3Svm, gasFee);
1208
1209
  }
1209
1210
  catch (err) {
@@ -1263,23 +1264,16 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1263
1264
  }
1264
1265
  // SVM warn-threshold telemetry (no truncation — pre-flight handles drain protection).
1265
1266
  (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)({
1267
+ // Pre-flight (R3 SVM): no PRC-20 burn check. R3 SVM uses burnAmount=0
1268
+ // because the drain amount lives inside the SVM instruction data.
1269
+ (0, preflight_1.runPreflight)({
1271
1270
  ctx,
1272
1271
  ueaAddress,
1273
1272
  ueaBalance: currentBalance,
1274
1273
  requiredValue: nativeValueForGas,
1275
1274
  gasReserve: OUTBOUND_GAS_RESERVE_R3_SVM,
1276
1275
  pathTag: 'R3_SVM',
1277
- allowUnderfundedSwap: allowUnderfundedR3Svm,
1278
1276
  });
1279
- if (!preflightR3Svm.ok) {
1280
- nativeValueForGas = preflightR3Svm.legacyClampedValue;
1281
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
1282
- }
1283
1277
  // Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1284
1278
  const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
1285
1279
  prc20Token,
@@ -1316,17 +1310,11 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1316
1310
  response.chain = sourceChain;
1317
1311
  const chainInfo = chain_1.CHAIN_INFO[sourceChain];
1318
1312
  response.chainNamespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
1319
- // R3 SVM inbound round-trip tracking is disabled.
1320
- //
1321
- // For Route 3 SVM drains (rescue_funds-style flow, txType=RESCUE_FUNDS),
1322
- // empirical check on donut showed the keeper does NOT create a child
1323
- // universalTxId on Push for the Solana-side `UniversalTx(fromCEA=true)`
1324
- // log — the parent utxId's `outboundStatus=OBSERVED` is the terminal state.
1325
- // Polling for a deterministically-derived child id therefore never
1326
- // resolves, and the cosmos `inbound_tx_hash` text-search fallback also
1327
- // returns 0 hits. Receipts now resolve as soon as the Solana outbound is
1328
- // observed instead of timing out at 300s.
1329
- response._expectsInboundRoundTrip = false;
1313
+ // R3 SVM mirrors EVM R3: when funds drain back to Push, the Solana-side
1314
+ // send_universal_tx_to_uea emits an inbound leg whose payload can execute on
1315
+ // Push. Payload-only R3 has no child inbound UTX, so skip round-trip tracking
1316
+ // when no amount is being drained.
1317
+ response._expectsInboundRoundTrip = drainAmount > BigInt(0);
1330
1318
  return response;
1331
1319
  });
1332
1320
  }
@@ -1352,7 +1340,7 @@ function executeCeaToCea(ctx, params, executeFn) {
1352
1340
  */
1353
1341
  function buildPayloadForRoute(ctx, params, route, nonce) {
1354
1342
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
1355
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
1343
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
1356
1344
  const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
1357
1345
  switch (route) {
1358
1346
  case route_detector_1.TransactionRoute.UOA_TO_PUSH: {
@@ -1396,13 +1384,13 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1396
1384
  }
1397
1385
  else if (hasExecute) {
1398
1386
  prc20Token = (0, helpers_1.getNativePRC20ForChain)(target.chain, ctx.pushNetwork);
1399
- burnAmount = BigInt(1);
1387
+ burnAmount = BigInt(0);
1400
1388
  }
1401
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_b = params.gasLimit) !== null && _b !== void 0 ? _b : BigInt(0), payload, ueaAddress);
1389
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_b = params.gasLimit) !== null && _b !== void 0 ? _b : BigInt(0), payload, ueaAddress, (_c = params.maxPCForGas) !== null && _c !== void 0 ? _c : BigInt(0));
1402
1390
  return { payload, gatewayRequest: outboundReq };
1403
1391
  }
1404
1392
  // EVM path: Resolve CEA address first (needed for self-transfer check)
1405
- const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, target.chain, (_c = ctx.rpcUrls[target.chain]) === null || _c === void 0 ? void 0 : _c[0]);
1393
+ const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, target.chain, (_d = ctx.rpcUrls[target.chain]) === null || _d === void 0 ? void 0 : _d[0]);
1406
1394
  // Build CEA outbound payload
1407
1395
  const multicalls = [];
1408
1396
  if (params.data) {
@@ -1435,21 +1423,24 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1435
1423
  const payload = (0, payload_builders_1.buildCeaMulticallPayload)(multicalls);
1436
1424
  let prc20Token = selectors_1.ZERO_ADDRESS;
1437
1425
  let burnAmount = BigInt(0);
1438
- if ((_d = params.funds) === null || _d === void 0 ? void 0 : _d.amount) {
1426
+ if ((_e = params.funds) === null || _e === void 0 ? void 0 : _e.amount) {
1439
1427
  const token = params.funds.token;
1440
1428
  if (token) {
1441
1429
  prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token).address;
1442
1430
  burnAmount = params.funds.amount;
1443
1431
  }
1444
1432
  }
1445
- const targetBytes = ceaAddress;
1446
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_e = params.gasLimit) !== null && _e !== void 0 ? _e : BigInt(0), payload, ueaAddress);
1433
+ const targetBytes = payload === '0x'
1434
+ ? selectors_1.ZERO_ADDRESS
1435
+ : ceaAddress;
1436
+ (0, payload_builders_1.assertCeaFundsParkingInvariant)(targetBytes, payload);
1437
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_f = params.gasLimit) !== null && _f !== void 0 ? _f : BigInt(0), payload, ueaAddress, (_g = params.maxPCForGas) !== null && _g !== void 0 ? _g : BigInt(0));
1447
1438
  return { payload, gatewayRequest: outboundReq };
1448
1439
  }
1449
1440
  case route_detector_1.TransactionRoute.CEA_TO_PUSH: {
1450
1441
  // Route 3: CEA -> Push Chain
1451
- // Build CEA multicall (approve + sendUniversalTxFromCEA) and wrap in outbound
1452
- if (!((_f = params.from) === null || _f === void 0 ? void 0 : _f.chain)) {
1442
+ // Build CEA multicall (sendUniversalTxToUEA self-call) and wrap in outbound
1443
+ if (!((_h = params.from) === null || _h === void 0 ? void 0 : _h.chain)) {
1453
1444
  throw new Error('Route 3 (CEA -> Push) requires from.chain');
1454
1445
  }
1455
1446
  const sourceChain = params.from.chain;
@@ -1462,21 +1453,7 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1462
1453
  }
1463
1454
  const programPk = new web3_js_1.PublicKey(lockerContract);
1464
1455
  const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
1465
- let drainAmount = BigInt(0);
1466
- let tokenMintHex;
1467
- if (((_g = params.funds) === null || _g === void 0 ? void 0 : _g.amount) && params.funds.amount > BigInt(0)) {
1468
- drainAmount = params.funds.amount;
1469
- const token = params.funds.token;
1470
- if (token && token.address) {
1471
- const mintPk = new web3_js_1.PublicKey(token.address);
1472
- tokenMintHex = ('0x' + Buffer.from(mintPk.toBytes()).toString('hex'));
1473
- }
1474
- }
1475
- else if (params.value && params.value > BigInt(0)) {
1476
- drainAmount = params.value;
1477
- }
1478
- // Route 3 SVM: ALWAYS use native PRC-20, CEA uses its own balance.
1479
- const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
1456
+ const { prc20Token, drainAmount, tokenMintHex } = resolveR3SvmDrain(params, sourceChain, ctx.pushNetwork);
1480
1457
  // Derive CEA PDA as revert recipient
1481
1458
  const ueaBytes2 = Buffer.from(ueaAddress.slice(2), 'hex');
1482
1459
  const [ceaPda2] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes2], programPk);
@@ -1485,25 +1462,22 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1485
1462
  gatewayProgramHex,
1486
1463
  drainAmount,
1487
1464
  tokenMintHex,
1465
+ extraPayload: buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, nonce + BigInt(1)),
1488
1466
  revertRecipientHex: ceaPdaHex2,
1489
1467
  });
1490
- // burnAmount = 1 (minimum for precompile; drain amount lives inside the ixData)
1491
- const burnAmount = BigInt(1);
1492
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, (_h = params.gasLimit) !== null && _h !== void 0 ? _h : BigInt(0), svmPayload, ueaAddress);
1468
+ // Payload-only outbound: drain amount lives inside ixData.
1469
+ const burnAmount = BigInt(0);
1470
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, (_j = params.gasLimit) !== null && _j !== void 0 ? _j : BigInt(0), svmPayload, ueaAddress, (_k = params.maxPCForGas) !== null && _k !== void 0 ? _k : BigInt(0));
1493
1471
  return { payload: svmPayload, gatewayRequest: outboundReq };
1494
1472
  }
1495
- const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_j = ctx.rpcUrls[sourceChain]) === null || _j === void 0 ? void 0 : _j[0]);
1473
+ const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_l = ctx.rpcUrls[sourceChain]) === null || _l === void 0 ? void 0 : _l[0]);
1496
1474
  // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
1497
1475
  // if CEA doesn't exist yet. No SDK-side blocking needed.
1498
- const gatewayAddr = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
1499
- if (!gatewayAddr) {
1500
- throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
1501
- }
1502
1476
  // Build CEA multicalls (self-calls via sendUniversalTxToUEA)
1503
1477
  const ceaMulticalls = [];
1504
1478
  let tokenAddress = selectors_1.ZERO_ADDRESS;
1505
1479
  let amount = BigInt(0);
1506
- if ((_k = params.funds) === null || _k === void 0 ? void 0 : _k.amount) {
1480
+ if ((_m = params.funds) === null || _m === void 0 ? void 0 : _m.amount) {
1507
1481
  const token = params.funds.token;
1508
1482
  if (token) {
1509
1483
  if (token.mechanism === 'native') {
@@ -1513,27 +1487,6 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1513
1487
  else {
1514
1488
  tokenAddress = token.address;
1515
1489
  amount = params.funds.amount;
1516
- // Reset allowance to 0 first for USDT-style tokens, then approve
1517
- const approveZeroData = (0, viem_1.encodeFunctionData)({
1518
- abi: erc20_evm_1.ERC20_EVM,
1519
- functionName: 'approve',
1520
- args: [gatewayAddr, BigInt(0)],
1521
- });
1522
- ceaMulticalls.push({
1523
- to: tokenAddress,
1524
- value: BigInt(0),
1525
- data: approveZeroData,
1526
- });
1527
- const approveData = (0, viem_1.encodeFunctionData)({
1528
- abi: erc20_evm_1.ERC20_EVM,
1529
- functionName: 'approve',
1530
- args: [gatewayAddr, amount],
1531
- });
1532
- ceaMulticalls.push({
1533
- to: tokenAddress,
1534
- value: BigInt(0),
1535
- data: approveData,
1536
- });
1537
1490
  }
1538
1491
  }
1539
1492
  }
@@ -1559,7 +1512,8 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1559
1512
  const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, pushDestination, multicallData);
1560
1513
  pushPayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: ueaNonceHop + BigInt(1) });
1561
1514
  }
1562
- // Build sendUniversalTxToUEA self-call on CEA (to=CEA, value=0)
1515
+ // Build sendUniversalTxToUEA self-call on CEA. CEA self-calls must
1516
+ // always use value=0; native sends spend from the CEA balance.
1563
1517
  const sendCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, tokenAddress, amount, pushPayload, ceaAddress);
1564
1518
  ceaMulticalls.push(sendCall);
1565
1519
  const ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
@@ -1567,7 +1521,8 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1567
1521
  // Route 3: CEA uses its own pre-existing balance — no PRC-20 burn needed.
1568
1522
  // burnAmount = 0 makes this a payload-only outbound relay.
1569
1523
  const burnAmount = BigInt(0);
1570
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, (_l = params.gasLimit) !== null && _l !== void 0 ? _l : BigInt(0), ceaPayload, ueaAddress);
1524
+ const outboundGasLimit = (_o = params.gasLimit) !== null && _o !== void 0 ? _o : selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
1525
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, outboundGasLimit, ceaPayload, ueaAddress, (_p = params.maxPCForGas) !== null && _p !== void 0 ? _p : BigInt(0));
1571
1526
  return { payload: ceaPayload, gatewayRequest: outboundReq };
1572
1527
  }
1573
1528
  default: