@pushchain/core 5.1.18 → 6.0.1

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 (57) 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 +118 -2
  8. package/src/lib/constants/abi/universalGatewayPC.evm.js +41 -2
  9. package/src/lib/constants/abi/universalGatewayV0.json +1 -1
  10. package/src/lib/constants/chain.d.ts +2 -0
  11. package/src/lib/constants/chain.js +18 -14
  12. package/src/lib/constants/index.d.ts +1 -1
  13. package/src/lib/constants/tokens.d.ts +10 -1
  14. package/src/lib/constants/tokens.js +5 -2
  15. package/src/lib/formatters.d.ts +6 -0
  16. package/src/lib/formatters.js +14 -0
  17. package/src/lib/index.d.ts +2 -0
  18. package/src/lib/index.js +7 -1
  19. package/src/lib/orchestrator/internals/cascade.d.ts +3 -1
  20. package/src/lib/orchestrator/internals/cascade.js +189 -103
  21. package/src/lib/orchestrator/internals/errors.js +1 -1
  22. package/src/lib/orchestrator/internals/execute-funds-only.js +3 -2
  23. package/src/lib/orchestrator/internals/execute-funds-payload.js +2 -1
  24. package/src/lib/orchestrator/internals/execute-standard.js +4 -4
  25. package/src/lib/orchestrator/internals/gas-calculator.d.ts +1 -0
  26. package/src/lib/orchestrator/internals/gas-calculator.js +13 -11
  27. package/src/lib/orchestrator/internals/helpers.js +2 -0
  28. package/src/lib/orchestrator/internals/inbound-tracker.js +4 -2
  29. package/src/lib/orchestrator/internals/outbound-sync.js +73 -60
  30. package/src/lib/orchestrator/internals/pc-usd-oracle.d.ts +1 -1
  31. package/src/lib/orchestrator/internals/pc-usd-oracle.js +4 -3
  32. package/src/lib/orchestrator/internals/preflight.d.ts +8 -5
  33. package/src/lib/orchestrator/internals/preflight.js +35 -21
  34. package/src/lib/orchestrator/internals/push-chain-tx.js +5 -1
  35. package/src/lib/orchestrator/internals/route-handlers.js +175 -202
  36. package/src/lib/orchestrator/internals/signing.js +7 -2
  37. package/src/lib/orchestrator/internals/svm-helpers.js +3 -3
  38. package/src/lib/orchestrator/internals/svm-rent.d.ts +42 -9
  39. package/src/lib/orchestrator/internals/svm-rent.js +157 -14
  40. package/src/lib/orchestrator/internals/tx-transformer.js +5 -2
  41. package/src/lib/orchestrator/max-pc-for-gas.d.ts +60 -0
  42. package/src/lib/orchestrator/max-pc-for-gas.js +66 -0
  43. package/src/lib/orchestrator/orchestrator.d.ts +6 -6
  44. package/src/lib/orchestrator/orchestrator.js +10 -3
  45. package/src/lib/orchestrator/orchestrator.types.d.ts +48 -10
  46. package/src/lib/orchestrator/payload-builders.d.ts +7 -6
  47. package/src/lib/orchestrator/payload-builders.js +19 -7
  48. package/src/lib/progress-hook/progress-hook.js +59 -27
  49. package/src/lib/progress-hook/progress-hook.types.d.ts +3 -3
  50. package/src/lib/progress-hook/progress-hook.types.js +4 -2
  51. package/src/lib/push-chain/push-chain.d.ts +10 -4
  52. package/src/lib/push-chain/push-chain.js +2 -2
  53. package/src/lib/universal-tx-detector/events.d.ts +3 -1
  54. package/src/lib/universal-tx-detector/events.js +3 -1
  55. package/src/lib/universal-tx-detector/svm-events.d.ts +1 -1
  56. package/src/lib/universal-tx-detector/svm-events.js +1 -1
  57. package/src/lib/utils.js +1 -1
@@ -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
@@ -281,7 +312,7 @@ function executeMultiChain(ctx, params, executeFn) {
281
312
  */
282
313
  function executeUoaToCea(ctx, params, executeFn) {
283
314
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
284
- var _a, _b, _c;
315
+ var _a, _b, _c, _d, _e;
285
316
  const target = params.to;
286
317
  const targetChain = target.chain;
287
318
  const targetAddress = target.address;
@@ -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
- target: outboundReq.target,
421
+ recipient: outboundReq.recipient,
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)}`);
@@ -446,10 +480,9 @@ function executeUoaToCea(ctx, params, executeFn) {
446
480
  (0, context_1.printLog)(ctx, `executeUoaToCea — capping nativeValueForGas at 200 UPC ceiling (was ${nativeValueForGas.toString()})`);
447
481
  nativeValueForGas = EVM_NATIVE_VALUE_SAFETY_CAP;
448
482
  }
449
- // Pre-flight: throws InsufficientUEABalanceError when UEA cannot cover
450
- // (nativeValueForGas + gasReserve) or the PRC-20 burn amount. Replaces the
451
- // legacy silent-clamp that previously submitted under-funded swaps and
452
- // bubbled up as Uniswap STF reverts. PRC-20 check fires whenever
483
+ // Pre-flight: warns by default when UEA cannot cover (nativeValueForGas +
484
+ // gasReserve) or the PRC-20 burn amount; throws only when
485
+ // options.enforceGasCheck=true. PRC-20 check fires whenever
453
486
  // burnAmount > 0 — user must pre-hold the burn token, even when it equals
454
487
  // the gas token (the gateway does transferFrom BEFORE the gas swap).
455
488
  let prc20BalanceR2Evm;
@@ -471,6 +504,7 @@ function executeUoaToCea(ctx, params, executeFn) {
471
504
  burnToken: prc20Token,
472
505
  burnAmount,
473
506
  prc20Balance: prc20BalanceR2Evm,
507
+ enforceGasCheck: ((_e = params.options) === null || _e === void 0 ? void 0 : _e.enforceGasCheck) === true,
474
508
  });
475
509
  // Preflight success means nativeValueForGas already covers required + reserve; no adjust needed.
476
510
  // Build outbound multicalls (approve burn + sendUniversalTxOutbound with native value)
@@ -534,7 +568,7 @@ function executeUoaToCea(ctx, params, executeFn) {
534
568
  */
535
569
  function executeUoaToCeaSvm(ctx, params, target, executeFn) {
536
570
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
537
- var _a, _b, _c, _d, _e, _f, _g, _h;
571
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
538
572
  const targetChain = target.chain;
539
573
  const targetAddress = target.address; // 0x-prefixed, 32 bytes
540
574
  const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
@@ -561,44 +595,49 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
561
595
  // --- 202: Gas estimation (spec-ordered before 203) ---
562
596
  let gasFee = BigInt(0);
563
597
  let protocolFeeSvm = BigInt(0);
598
+ let outboundGasPriceSvm = BigInt(0);
564
599
  let gasToken = selectors_1.ZERO_ADDRESS;
565
600
  let universalCoreAddress;
566
601
  // Effective gas limit seeded from the user param. When the user omits
567
- // gasLimit, it'll be derived from the gasFee/gasPrice response so the
568
- // 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.
569
604
  let effectiveGasLimit = (_d = params.gasLimit) !== null && _d !== void 0 ? _d : BigInt(0);
570
- // Conditional CEA-ATA rent bump: when the SPL outbound's destination ATA
571
- // does not yet exist on Solana, the gateway needs an extra ~2.04M lamports
572
- // for ATA-create rent. The on-chain quote does NOT include this, so bump
573
- // effectiveGasLimit BEFORE queryOutboundGasFee so the returned gasFee
574
- // already covers the rent.
575
- const splMintBase58 = burnAmount > BigInt(0)
576
- ? (_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
577
612
  : undefined;
578
- effectiveGasLimit = yield (0, svm_rent_1.maybeBumpForCeaAtaRent)({
579
- ctx,
580
- ueaAddress,
581
- targetChain,
582
- splMintBase58,
583
- burnAmount,
584
- effectiveGasLimit,
585
- });
613
+ const splMintBase58 = (svmFundsToken === null || svmFundsToken === void 0 ? void 0 : svmFundsToken.mechanism) === 'approve' ? svmFundsToken.address : undefined;
586
614
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
587
615
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_01, targetChain);
588
616
  try {
589
- 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
+ });
590
628
  gasFee = result.gasFee;
591
629
  protocolFeeSvm = result.protocolFee;
630
+ outboundGasPriceSvm = result.gasPrice;
592
631
  gasToken = result.gasToken;
593
632
  universalCoreAddress = result.universalCoreAddress;
594
- // When user omits gasLimit (sent as 0), the contract computes fees using its internal
595
- // baseGasLimitByChainNamespace. But the relay reads the stored gasLimit=0 from the
596
- // on-chain outbound record and uses it as the Solana compute budget — 0 CU means the
597
- // relay cannot execute the tx. Derive the effective limit from gasFee/gasPrice so
598
- // the stored record has a non-zero compute budget the relay can use.
599
- if (!params.gasLimit && result.gasPrice > BigInt(0)) {
600
- effectiveGasLimit = result.gasFee / result.gasPrice;
601
- (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})`);
602
641
  }
603
642
  (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}`);
604
643
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_02, targetChain, protocolFeeSvm, gasFee);
@@ -614,13 +653,14 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
614
653
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_02, ueaAddress, targetAddress, targetChain, true);
615
654
  // --- Build outbound request ---
616
655
  // targetBytes: for execute = program id (resolver output); for withdraw = to.address
617
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress // revert recipient is the UEA
618
- );
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);
619
658
  (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — outbound request: ${JSON.stringify({
620
- target: outboundReq.target,
659
+ recipient: outboundReq.recipient,
621
660
  token: outboundReq.token,
622
661
  amount: outboundReq.amount.toString(),
623
662
  gasLimit: outboundReq.gasLimit.toString(),
663
+ maxPCForGas: outboundReq.maxPCForGas.toString(),
624
664
  payloadLength: (outboundReq.payload.length - 2) / 2,
625
665
  revertRecipient: outboundReq.revertRecipient,
626
666
  }, null, 2)}`);
@@ -670,10 +710,9 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
670
710
  (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — nativeValueForGas=${nativeValueForGas.toString()} (includes 2.2x buffer)`);
671
711
  (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R2_SVM');
672
712
  }
673
- // Pre-flight: throws InsufficientUEABalanceError when UEA cannot cover
674
- // (nativeValueForGas + SVM_GAS_RESERVE) or the PRC-20 burn amount.
675
- // Replaces the legacy silent-clamp that previously submitted under-funded
676
- // swaps and bubbled up as Uniswap STF reverts.
713
+ // Pre-flight: warns by default when UEA cannot cover (nativeValueForGas +
714
+ // SVM_GAS_RESERVE) or the PRC-20 burn amount; throws only when
715
+ // options.enforceGasCheck=true.
677
716
  // PRC-20 burn-balance pre-check fires whenever `burnAmount > 0`, regardless
678
717
  // of whether `prc20Token` happens to equal `gasToken`. The gateway's
679
718
  // `sendUniversalTxOutbound` does `transferFrom(user, gateway, burnAmount)`
@@ -699,6 +738,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
699
738
  burnToken: prc20Token,
700
739
  burnAmount,
701
740
  prc20Balance: prc20BalanceR2Svm,
741
+ enforceGasCheck: ((_j = params.options) === null || _j === void 0 ? void 0 : _j.enforceGasCheck) === true,
702
742
  });
703
743
  // --- Build Push Chain multicalls (approve + sendUniversalTxOutbound) ---
704
744
  // Reuse the same builder as EVM — this part is identical
@@ -764,7 +804,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
764
804
  */
765
805
  function executeCeaToPush(ctx, params, executeFn) {
766
806
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
767
- var _a, _b, _c, _d, _e, _f, _g, _h;
807
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
768
808
  // 1. Validate and extract source chain
769
809
  if (!((_a = params.from) === null || _a === void 0 ? void 0 : _a.chain)) {
770
810
  throw new Error('Route 3 (CEA -> Push) requires from.chain to specify the source CEA chain');
@@ -801,11 +841,6 @@ function executeCeaToPush(ctx, params, executeFn) {
801
841
  (0, context_1.printLog)(ctx, `executeCeaToPush — sourceChain: ${sourceChain}, CEA: ${ceaAddress}, deployed: ${ceaDeployed}`);
802
842
  // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
803
843
  // if CEA doesn't exist yet. No SDK-side blocking needed.
804
- // 5. Get UniversalGateway address on source chain
805
- const gatewayAddress = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
806
- if (!gatewayAddress) {
807
- throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
808
- }
809
844
  // 6. Build multicall for CEA to execute on source chain (self-calls via sendUniversalTxToUEA)
810
845
  const ceaMulticalls = [];
811
846
  // Determine token and amount for sendUniversalTxToUEA
@@ -821,7 +856,7 @@ function executeCeaToPush(ctx, params, executeFn) {
821
856
  amount = params.funds.amount;
822
857
  }
823
858
  else {
824
- // ERC20 token - need approval for gateway before sendUniversalTxToUEA
859
+ // ERC20 token. CEA handles gateway approval internally around sendUniversalTxToUEA.
825
860
  tokenAddress = token.address;
826
861
  amount = params.funds.amount;
827
862
  }
@@ -838,33 +873,7 @@ function executeCeaToPush(ctx, params, executeFn) {
838
873
  // query and relay execution, causing sendUniversalTxToUEA to revert with
839
874
  // InsufficientBalance. Pre-existing CEA funds remain parked and can be swept separately.
840
875
  const bridgeAmount = amount;
841
- // Note: CEA contract may reject amount=0 in sendUniversalTxToUEA.
842
- // Keeping bridgeAmount as-is (0) for payload-only to test precompile behavior.
843
- // For ERC20 tokens, add approve call for the bridge amount
844
- // (CEA approves gateway to spend the Vault-deposited amount)
845
- // Reset to 0 first for USDT-style tokens that revert on non-zero to non-zero approve.
846
- if (tokenAddress !== selectors_1.ZERO_ADDRESS && bridgeAmount > BigInt(0)) {
847
- const approveZeroData = (0, viem_1.encodeFunctionData)({
848
- abi: erc20_evm_1.ERC20_EVM,
849
- functionName: 'approve',
850
- args: [gatewayAddress, BigInt(0)],
851
- });
852
- ceaMulticalls.push({
853
- to: tokenAddress,
854
- value: BigInt(0),
855
- data: approveZeroData,
856
- });
857
- const approveData = (0, viem_1.encodeFunctionData)({
858
- abi: erc20_evm_1.ERC20_EVM,
859
- functionName: 'approve',
860
- args: [gatewayAddress, bridgeAmount],
861
- });
862
- ceaMulticalls.push({
863
- to: tokenAddress,
864
- value: BigInt(0),
865
- data: approveData,
866
- });
867
- }
876
+ // The post-audit CEA allows amount=0 for payload-only sendUniversalTxToUEA.
868
877
  // Pre-fetch UEA nonce — needed for the inbound UniversalPayload struct
869
878
  // Skip getCode if accountStatusCache already confirmed deployment
870
879
  let isUEADeployed;
@@ -927,18 +936,19 @@ function executeCeaToPush(ctx, params, executeFn) {
927
936
  const outboundGasLimit = (_f = params.gasLimit) !== null && _f !== void 0 ? _f : selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
928
937
  (0, context_1.printLog)(ctx, `executeCeaToPush — prc20Token: ${prc20Token}, burnAmount: ${burnAmount.toString()}`);
929
938
  // 9. Build outbound request (same structure as Route 2)
930
- // target = CEA address (for self-execution), value = 0 in payload
931
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, // target: CEA address (to=CEA for self-execution)
939
+ // recipient = CEA address (for self-execution), value = 0 in payload
940
+ let outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, // recipient: CEA address (to=CEA for self-execution)
932
941
  prc20Token, // token: native PRC-20 for source chain (for namespace lookup)
933
942
  burnAmount, // amount: 0 (payload-only, CEA uses its own balance)
934
943
  outboundGasLimit, ceaPayload, // payload: ABI-encoded CEA multicall
935
- ueaAddress // revertRecipient: UEA
936
- );
944
+ ueaAddress, // revertRecipient: UEA
945
+ (_g = params.maxPCForGas) !== null && _g !== void 0 ? _g : BigInt(0));
937
946
  (0, context_1.printLog)(ctx, `executeCeaToPush — outbound request: ${JSON.stringify({
938
- target: outboundReq.target,
947
+ recipient: outboundReq.recipient,
939
948
  token: outboundReq.token,
940
949
  amount: outboundReq.amount.toString(),
941
950
  gasLimit: outboundReq.gasLimit.toString(),
951
+ maxPCForGas: outboundReq.maxPCForGas.toString(),
942
952
  payloadLength: outboundReq.payload.length,
943
953
  revertRecipient: outboundReq.revertRecipient,
944
954
  }, null, 2)}`);
@@ -949,6 +959,7 @@ function executeCeaToPush(ctx, params, executeFn) {
949
959
  // 11. Query gas fees from UniversalCore
950
960
  let gasFee = BigInt(0);
951
961
  let protocolFeeR3 = BigInt(0);
962
+ let outboundGasPriceR3 = BigInt(0);
952
963
  let nativeValueForGas = BigInt(0);
953
964
  let gasToken = selectors_1.ZERO_ADDRESS;
954
965
  let sizingDecisionR3;
@@ -958,10 +969,12 @@ function executeCeaToPush(ctx, params, executeFn) {
958
969
  const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundGasLimit, sourceChain);
959
970
  gasToken = result.gasToken;
960
971
  gasFee = result.gasFee;
972
+ outboundGasPriceR3 = result.gasPrice;
961
973
  protocolFeeR3 = result.protocolFee;
962
974
  nativeValueForGas = result.nativeValueForGas;
963
975
  sizingDecisionR3 = result.sizing;
964
- (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'}`);
965
978
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02, sourceChain, protocolFeeR3, gasFee);
966
979
  }
967
980
  catch (err) {
@@ -1034,7 +1047,8 @@ function executeCeaToPush(ctx, params, executeFn) {
1034
1047
  }
1035
1048
  // Pre-flight (R3 EVM): no PRC-20 burn check — `burnAmount = 0` makes this
1036
1049
  // payload-only (UEA never holds the source-chain PRC-20; see plan §9 #4).
1037
- // Native UPC check still applies for the gas swap.
1050
+ // Native UPC check still applies for the gas swap. It warns by default and
1051
+ // throws only when options.enforceGasCheck=true.
1038
1052
  (0, preflight_1.runPreflight)({
1039
1053
  ctx,
1040
1054
  ueaAddress,
@@ -1042,6 +1056,7 @@ function executeCeaToPush(ctx, params, executeFn) {
1042
1056
  requiredValue: nativeValueForGas,
1043
1057
  gasReserve: OUTBOUND_GAS_RESERVE_R3,
1044
1058
  pathTag: 'R3_EVM',
1059
+ enforceGasCheck: ((_l = params.options) === null || _l === void 0 ? void 0 : _l.enforceGasCheck) === true,
1045
1060
  });
1046
1061
  // 12. Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1047
1062
  const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
@@ -1107,7 +1122,7 @@ function executeCeaToPush(ctx, params, executeFn) {
1107
1122
  */
1108
1123
  function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1109
1124
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
1110
- var _a, _b, _c, _d;
1125
+ var _a, _b, _c, _d, _e, _f;
1111
1126
  if (typeof params.to !== 'string') {
1112
1127
  throw new Error('Route 3 SVM (CEA -> Push): to must be a Push Chain address (string), not a ChainTarget');
1113
1128
  }
@@ -1121,41 +1136,31 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1121
1136
  const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
1122
1137
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_301, sourceChain, lockerContract);
1123
1138
  (0, context_1.printLog)(ctx, `executeCeaToPushSvm — sourceChain: ${sourceChain}, gateway: ${lockerContract}`);
1124
- // Determine drain amount and token
1125
- let drainAmount = BigInt(0);
1126
- let tokenMintHex;
1127
- if (((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) && params.funds.amount > BigInt(0)) {
1128
- // SPL token drain
1129
- drainAmount = params.funds.amount;
1130
- const token = params.funds.token;
1131
- if (token && token.address) {
1132
- // Convert SPL mint address to 32-byte hex
1133
- const mintPk = new web3_js_1.PublicKey(token.address);
1134
- tokenMintHex = ('0x' + Buffer.from(mintPk.toBytes()).toString('hex'));
1135
- }
1136
- }
1137
- else if (params.value && params.value > BigInt(0)) {
1138
- // Native SOL drain
1139
- drainAmount = params.value;
1140
- }
1141
- // Route 3 SVM: ALWAYS use native PRC-20 for chain namespace lookup + gas fees.
1142
- // CEA uses its own pre-existing balance — no PRC-20 burn needed on Push Chain.
1143
- const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
1144
- // Build the SVM CPI payload (send_universal_tx_to_uea wrapped in execute)
1145
- // If params.data is provided, pass it as extraPayload for Push Chain execution
1146
- let extraPayload;
1147
- if (params.data && typeof params.data === 'string') {
1148
- extraPayload = (0, viem_1.hexToBytes)(params.data);
1149
- }
1139
+ // Route 3 SVM: CEA uses its own pre-existing balance — no PRC-20 burn
1140
+ // needed on Push Chain. The outer request token must still match the source
1141
+ // asset so the relayer supplies the same Solana mint that the payload
1142
+ // encodes; otherwise the gateway rejects with InvalidMint.
1143
+ const { prc20Token, drainAmount, tokenMintHex } = resolveR3SvmDrain(params, sourceChain, ctx.pushNetwork);
1150
1144
  // Derive CEA PDA as revert recipient: ["push_identity", ueaAddress_20_bytes]
1151
1145
  const ueaBytes = Buffer.from(ueaAddress.slice(2), 'hex'); // 20 bytes
1152
1146
  const [ceaPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes], programPk);
1153
1147
  const ceaPdaHex = ('0x' + Buffer.from(ceaPda.toBytes()).toString('hex'));
1148
+ // Pre-fetch UEA status before payload construction. The inbound
1149
+ // UniversalPayload must use the nonce after this outbound execution, because
1150
+ // the Push-side execute() below consumes one UEA nonce before the Solana
1151
+ // round-trip payload returns.
1152
+ const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
1153
+ const [ueaCode, ueaBalance] = yield Promise.all([
1154
+ ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
1155
+ ctx.pushClient.getBalance(ueaAddress),
1156
+ ]);
1157
+ const isUEADeployed = ueaCode !== undefined;
1158
+ const ueaNonce = isUEADeployed ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress) : BigInt(0);
1154
1159
  const svmPayload = (0, payload_builders_1.encodeSvmCeaToUeaPayload)({
1155
1160
  gatewayProgramHex,
1156
1161
  drainAmount,
1157
1162
  tokenMintHex,
1158
- extraPayload,
1163
+ extraPayload: buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, ueaNonce + BigInt(1)),
1159
1164
  revertRecipientHex: ceaPdaHex,
1160
1165
  });
1161
1166
  (0, context_1.printLog)(ctx, `executeCeaToPushSvm — drainAmount: ${drainAmount.toString()}, payload length: ${(svmPayload.length - 2) / 2} bytes`);
@@ -1166,33 +1171,42 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1166
1171
  // (e.g. pSOL). The UGPC upgrade post-2026-03-19 already accepts amount = 0
1167
1172
  // on the cascade path, and the same precompile is used here.
1168
1173
  const burnAmount = BigInt(0);
1169
- // Build outbound request: target = gateway program (self-call)
1170
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, (_b = params.gasLimit) !== null && _b !== void 0 ? _b : BigInt(0), svmPayload, ueaAddress);
1171
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — outbound request: target=${outboundReq.target.slice(0, 20)}..., token=${outboundReq.token}`);
1172
- // Pre-fetch UEA status early — balance is needed for gas value calculation
1173
- const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
1174
- const [ueaCode, ueaBalance] = yield Promise.all([
1175
- ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
1176
- ctx.pushClient.getBalance(ueaAddress),
1177
- ]);
1178
- const isUEADeployed = ueaCode !== undefined;
1179
- const ueaNonce = isUEADeployed ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress) : BigInt(0);
1174
+ let effectiveGasLimit = (_a = params.gasLimit) !== null && _a !== void 0 ? _a : BigInt(0);
1175
+ // Build outbound request: recipient = gateway program (self-call)
1176
+ let outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress, (_b = params.maxPCForGas) !== null && _b !== void 0 ? _b : BigInt(0));
1177
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — outbound request: recipient=${outboundReq.recipient.slice(0, 20)}..., token=${outboundReq.token}, maxPCForGas=${outboundReq.maxPCForGas}`);
1180
1178
  // Query gas fees
1181
1179
  let gasFee = BigInt(0);
1182
1180
  let protocolFeeR3Svm = BigInt(0);
1181
+ let outboundGasPriceR3Svm = BigInt(0);
1183
1182
  let nativeValueForGas = BigInt(0);
1184
1183
  let gasToken = selectors_1.ZERO_ADDRESS;
1185
1184
  let sizingDecisionR3Svm;
1186
1185
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
1187
1186
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01, sourceChain);
1188
1187
  try {
1189
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundReq.gasLimit, sourceChain);
1188
+ let result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundReq.gasLimit, sourceChain);
1189
+ result = yield (0, svm_rent_1.ensureSvmFinalizeGasBudgetQuote)({
1190
+ ctx,
1191
+ ueaAddress,
1192
+ targetChain: sourceChain,
1193
+ prc20Token,
1194
+ quote: result,
1195
+ splMintBase58: undefined,
1196
+ burnAmount: BigInt(0),
1197
+ pathTag: 'executeCeaToPushSvm',
1198
+ });
1190
1199
  gasToken = result.gasToken;
1191
1200
  gasFee = result.gasFee;
1201
+ outboundGasPriceR3Svm = result.gasPrice;
1192
1202
  protocolFeeR3Svm = result.protocolFee;
1193
1203
  nativeValueForGas = result.nativeValueForGas;
1194
1204
  sizingDecisionR3Svm = result.sizing;
1195
- (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'}`);
1205
+ effectiveGasLimit = result.gasLimitUsed;
1206
+ outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress, (_c = params.maxPCForGas) !== null && _c !== void 0 ? _c : BigInt(0), outboundGasPriceR3Svm);
1207
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — resolved effectiveGasLimit: ${effectiveGasLimit} ` +
1208
+ `(gasFee=${result.gasFee}, gasPrice=${result.gasPrice})`);
1209
+ (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'}`);
1196
1210
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02, sourceChain, protocolFeeR3Svm, gasFee);
1197
1211
  }
1198
1212
  catch (err) {
@@ -1233,13 +1247,13 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1233
1247
  : UPWARD_BASE_CEILING_R3_SVM;
1234
1248
  // Upward-only allocation: when balance has headroom over the calibrated
1235
1249
  // value, consume more of it (up to the ceiling above). Down-clamp +
1236
- // zero-balance throws live in pre-flight.
1250
+ // zero-balance handling lives in pre-flight.
1237
1251
  // Ordering note: pre-flight runs AFTER the upward adjust below; the
1238
1252
  // semantics are equivalent to running it before — when upward adjust
1239
1253
  // applies it sets nativeValueForGas to balance-reserve (capped), and the
1240
1254
  // post-adjust pre-flight check is always satisfied. When upward adjust is
1241
1255
  // skipped (balance too low), pre-flight runs against the calibrated value
1242
- // and throws cleanly on shortfall.
1256
+ // and either warns or throws based on options.enforceGasCheck.
1243
1257
  if (currentBalance > OUTBOUND_GAS_RESERVE_R3_SVM &&
1244
1258
  currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM > nativeValueForGas) {
1245
1259
  const balanceDriven = currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM;
@@ -1252,9 +1266,8 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1252
1266
  }
1253
1267
  // SVM warn-threshold telemetry (no truncation — pre-flight handles drain protection).
1254
1268
  (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R3_SVM');
1255
- // Pre-flight (R3 SVM): no PRC-20 burn check — `burnAmount = 1` is a legacy
1256
- // precompile-validation shim against a token UEAs structurally don't hold
1257
- // (see plan §9 #4 / §10.2 latent-bug ticket). Native UPC check still applies.
1269
+ // Pre-flight (R3 SVM): no PRC-20 burn check. R3 SVM uses burnAmount=0
1270
+ // because the drain amount lives inside the SVM instruction data.
1258
1271
  (0, preflight_1.runPreflight)({
1259
1272
  ctx,
1260
1273
  ueaAddress,
@@ -1262,6 +1275,7 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1262
1275
  requiredValue: nativeValueForGas,
1263
1276
  gasReserve: OUTBOUND_GAS_RESERVE_R3_SVM,
1264
1277
  pathTag: 'R3_SVM',
1278
+ enforceGasCheck: ((_f = params.options) === null || _f === void 0 ? void 0 : _f.enforceGasCheck) === true,
1265
1279
  });
1266
1280
  // Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1267
1281
  const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
@@ -1299,17 +1313,11 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1299
1313
  response.chain = sourceChain;
1300
1314
  const chainInfo = chain_1.CHAIN_INFO[sourceChain];
1301
1315
  response.chainNamespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
1302
- // R3 SVM inbound round-trip tracking is disabled.
1303
- //
1304
- // For Route 3 SVM drains (rescue_funds-style flow, txType=RESCUE_FUNDS),
1305
- // empirical check on donut showed the keeper does NOT create a child
1306
- // universalTxId on Push for the Solana-side `UniversalTx(fromCEA=true)`
1307
- // log — the parent utxId's `outboundStatus=OBSERVED` is the terminal state.
1308
- // Polling for a deterministically-derived child id therefore never
1309
- // resolves, and the cosmos `inbound_tx_hash` text-search fallback also
1310
- // returns 0 hits. Receipts now resolve as soon as the Solana outbound is
1311
- // observed instead of timing out at 300s.
1312
- response._expectsInboundRoundTrip = false;
1316
+ // R3 SVM mirrors EVM R3: when funds drain back to Push, the Solana-side
1317
+ // send_universal_tx_to_uea emits an inbound leg whose payload can execute on
1318
+ // Push. Payload-only R3 has no child inbound UTX, so skip round-trip tracking
1319
+ // when no amount is being drained.
1320
+ response._expectsInboundRoundTrip = drainAmount > BigInt(0);
1313
1321
  return response;
1314
1322
  });
1315
1323
  }
@@ -1335,7 +1343,7 @@ function executeCeaToCea(ctx, params, executeFn) {
1335
1343
  */
1336
1344
  function buildPayloadForRoute(ctx, params, route, nonce) {
1337
1345
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
1338
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
1346
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
1339
1347
  const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
1340
1348
  switch (route) {
1341
1349
  case route_detector_1.TransactionRoute.UOA_TO_PUSH: {
@@ -1379,13 +1387,13 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1379
1387
  }
1380
1388
  else if (hasExecute) {
1381
1389
  prc20Token = (0, helpers_1.getNativePRC20ForChain)(target.chain, ctx.pushNetwork);
1382
- burnAmount = BigInt(1);
1390
+ burnAmount = BigInt(0);
1383
1391
  }
1384
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_b = params.gasLimit) !== null && _b !== void 0 ? _b : BigInt(0), payload, ueaAddress);
1392
+ 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));
1385
1393
  return { payload, gatewayRequest: outboundReq };
1386
1394
  }
1387
1395
  // EVM path: Resolve CEA address first (needed for self-transfer check)
1388
- 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]);
1396
+ 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]);
1389
1397
  // Build CEA outbound payload
1390
1398
  const multicalls = [];
1391
1399
  if (params.data) {
@@ -1418,21 +1426,24 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1418
1426
  const payload = (0, payload_builders_1.buildCeaMulticallPayload)(multicalls);
1419
1427
  let prc20Token = selectors_1.ZERO_ADDRESS;
1420
1428
  let burnAmount = BigInt(0);
1421
- if ((_d = params.funds) === null || _d === void 0 ? void 0 : _d.amount) {
1429
+ if ((_e = params.funds) === null || _e === void 0 ? void 0 : _e.amount) {
1422
1430
  const token = params.funds.token;
1423
1431
  if (token) {
1424
1432
  prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token).address;
1425
1433
  burnAmount = params.funds.amount;
1426
1434
  }
1427
1435
  }
1428
- const targetBytes = ceaAddress;
1429
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_e = params.gasLimit) !== null && _e !== void 0 ? _e : BigInt(0), payload, ueaAddress);
1436
+ const targetBytes = payload === '0x'
1437
+ ? selectors_1.ZERO_ADDRESS
1438
+ : ceaAddress;
1439
+ (0, payload_builders_1.assertCeaFundsParkingInvariant)(targetBytes, payload);
1440
+ 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));
1430
1441
  return { payload, gatewayRequest: outboundReq };
1431
1442
  }
1432
1443
  case route_detector_1.TransactionRoute.CEA_TO_PUSH: {
1433
1444
  // Route 3: CEA -> Push Chain
1434
- // Build CEA multicall (approve + sendUniversalTxFromCEA) and wrap in outbound
1435
- if (!((_f = params.from) === null || _f === void 0 ? void 0 : _f.chain)) {
1445
+ // Build CEA multicall (sendUniversalTxToUEA self-call) and wrap in outbound
1446
+ if (!((_h = params.from) === null || _h === void 0 ? void 0 : _h.chain)) {
1436
1447
  throw new Error('Route 3 (CEA -> Push) requires from.chain');
1437
1448
  }
1438
1449
  const sourceChain = params.from.chain;
@@ -1445,21 +1456,7 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1445
1456
  }
1446
1457
  const programPk = new web3_js_1.PublicKey(lockerContract);
1447
1458
  const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
1448
- let drainAmount = BigInt(0);
1449
- let tokenMintHex;
1450
- if (((_g = params.funds) === null || _g === void 0 ? void 0 : _g.amount) && params.funds.amount > BigInt(0)) {
1451
- drainAmount = params.funds.amount;
1452
- const token = params.funds.token;
1453
- if (token && token.address) {
1454
- const mintPk = new web3_js_1.PublicKey(token.address);
1455
- tokenMintHex = ('0x' + Buffer.from(mintPk.toBytes()).toString('hex'));
1456
- }
1457
- }
1458
- else if (params.value && params.value > BigInt(0)) {
1459
- drainAmount = params.value;
1460
- }
1461
- // Route 3 SVM: ALWAYS use native PRC-20, CEA uses its own balance.
1462
- const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
1459
+ const { prc20Token, drainAmount, tokenMintHex } = resolveR3SvmDrain(params, sourceChain, ctx.pushNetwork);
1463
1460
  // Derive CEA PDA as revert recipient
1464
1461
  const ueaBytes2 = Buffer.from(ueaAddress.slice(2), 'hex');
1465
1462
  const [ceaPda2] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes2], programPk);
@@ -1468,25 +1465,22 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1468
1465
  gatewayProgramHex,
1469
1466
  drainAmount,
1470
1467
  tokenMintHex,
1468
+ extraPayload: buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, nonce + BigInt(1)),
1471
1469
  revertRecipientHex: ceaPdaHex2,
1472
1470
  });
1473
- // burnAmount = 1 (minimum for precompile; drain amount lives inside the ixData)
1474
- const burnAmount = BigInt(1);
1475
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, (_h = params.gasLimit) !== null && _h !== void 0 ? _h : BigInt(0), svmPayload, ueaAddress);
1471
+ // Payload-only outbound: drain amount lives inside ixData.
1472
+ const burnAmount = BigInt(0);
1473
+ 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));
1476
1474
  return { payload: svmPayload, gatewayRequest: outboundReq };
1477
1475
  }
1478
- const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_j = ctx.rpcUrls[sourceChain]) === null || _j === void 0 ? void 0 : _j[0]);
1476
+ const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_l = ctx.rpcUrls[sourceChain]) === null || _l === void 0 ? void 0 : _l[0]);
1479
1477
  // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
1480
1478
  // if CEA doesn't exist yet. No SDK-side blocking needed.
1481
- const gatewayAddr = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
1482
- if (!gatewayAddr) {
1483
- throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
1484
- }
1485
1479
  // Build CEA multicalls (self-calls via sendUniversalTxToUEA)
1486
1480
  const ceaMulticalls = [];
1487
1481
  let tokenAddress = selectors_1.ZERO_ADDRESS;
1488
1482
  let amount = BigInt(0);
1489
- if ((_k = params.funds) === null || _k === void 0 ? void 0 : _k.amount) {
1483
+ if ((_m = params.funds) === null || _m === void 0 ? void 0 : _m.amount) {
1490
1484
  const token = params.funds.token;
1491
1485
  if (token) {
1492
1486
  if (token.mechanism === 'native') {
@@ -1496,27 +1490,6 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1496
1490
  else {
1497
1491
  tokenAddress = token.address;
1498
1492
  amount = params.funds.amount;
1499
- // Reset allowance to 0 first for USDT-style tokens, then approve
1500
- const approveZeroData = (0, viem_1.encodeFunctionData)({
1501
- abi: erc20_evm_1.ERC20_EVM,
1502
- functionName: 'approve',
1503
- args: [gatewayAddr, BigInt(0)],
1504
- });
1505
- ceaMulticalls.push({
1506
- to: tokenAddress,
1507
- value: BigInt(0),
1508
- data: approveZeroData,
1509
- });
1510
- const approveData = (0, viem_1.encodeFunctionData)({
1511
- abi: erc20_evm_1.ERC20_EVM,
1512
- functionName: 'approve',
1513
- args: [gatewayAddr, amount],
1514
- });
1515
- ceaMulticalls.push({
1516
- to: tokenAddress,
1517
- value: BigInt(0),
1518
- data: approveData,
1519
- });
1520
1493
  }
1521
1494
  }
1522
1495
  }
@@ -1551,8 +1524,8 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1551
1524
  // Route 3: CEA uses its own pre-existing balance — no PRC-20 burn needed.
1552
1525
  // burnAmount = 0 makes this a payload-only outbound relay.
1553
1526
  const burnAmount = BigInt(0);
1554
- const outboundGasLimit = (_l = params.gasLimit) !== null && _l !== void 0 ? _l : selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
1555
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, outboundGasLimit, ceaPayload, ueaAddress);
1527
+ const outboundGasLimit = (_o = params.gasLimit) !== null && _o !== void 0 ? _o : selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
1528
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, outboundGasLimit, ceaPayload, ueaAddress, (_p = params.maxPCForGas) !== null && _p !== void 0 ? _p : BigInt(0));
1556
1529
  return { payload: ceaPayload, gatewayRequest: outboundReq };
1557
1530
  }
1558
1531
  default: