@pushchain/core 6.0.19 → 6.0.21

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 (112) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/package.json +1 -1
  3. package/src/lib/cache/cache.js +2 -0
  4. package/src/lib/constants/abi/index.d.ts +1 -0
  5. package/src/lib/constants/abi/index.js +8 -1
  6. package/src/lib/constants/abi/pc20.evm.d.ts +217 -0
  7. package/src/lib/constants/abi/pc20.evm.js +150 -0
  8. package/src/lib/constants/abi/prc20.evm.d.ts +172 -0
  9. package/src/lib/constants/abi/prc20.evm.js +97 -0
  10. package/src/lib/constants/abi/universalGatewayV0.json +1 -1
  11. package/src/lib/constants/chain.js +38 -8
  12. package/src/lib/constants/selectors.js +2 -2
  13. package/src/lib/constants/tokens.js +4 -3
  14. package/src/lib/formatters.js +16 -6
  15. package/src/lib/generated/uexecutor/v1/query.js +2 -4
  16. package/src/lib/generated/uexecutor/v1/types.d.ts +14 -0
  17. package/src/lib/generated/uexecutor/v1/types.js +90 -47
  18. package/src/lib/generated/uexecutor/v2/query.js +2 -4
  19. package/src/lib/generated/uexecutor/v2/types.d.ts +20 -0
  20. package/src/lib/generated/uexecutor/v2/types.js +49 -33
  21. package/src/lib/generated/v1/tx.js +30 -37
  22. package/src/lib/index.d.ts +4 -1
  23. package/src/lib/index.js +21 -3
  24. package/src/lib/internal/buffer.js +1 -2
  25. package/src/lib/orchestrator/cea-utils.js +73 -83
  26. package/src/lib/orchestrator/internals/account-manager.js +130 -139
  27. package/src/lib/orchestrator/internals/cascade.d.ts +0 -6
  28. package/src/lib/orchestrator/internals/cascade.js +1184 -1059
  29. package/src/lib/orchestrator/internals/confirmation.js +138 -148
  30. package/src/lib/orchestrator/internals/error-decoder.js +1 -1
  31. package/src/lib/orchestrator/internals/errors.js +12 -3
  32. package/src/lib/orchestrator/internals/execute-funds-only.d.ts +2 -2
  33. package/src/lib/orchestrator/internals/execute-funds-only.js +297 -259
  34. package/src/lib/orchestrator/internals/execute-funds-payload.d.ts +2 -2
  35. package/src/lib/orchestrator/internals/execute-funds-payload.js +220 -220
  36. package/src/lib/orchestrator/internals/execute-standard.d.ts +0 -8
  37. package/src/lib/orchestrator/internals/execute-standard.js +339 -342
  38. package/src/lib/orchestrator/internals/gas-calculator.js +497 -520
  39. package/src/lib/orchestrator/internals/gas-usd-sizer.js +101 -110
  40. package/src/lib/orchestrator/internals/gateway-client.js +102 -103
  41. package/src/lib/orchestrator/internals/helpers.js +30 -27
  42. package/src/lib/orchestrator/internals/inbound-tracker.js +234 -238
  43. package/src/lib/orchestrator/internals/outbound-sync.js +463 -452
  44. package/src/lib/orchestrator/internals/outbound-tracker.js +27 -34
  45. package/src/lib/orchestrator/internals/payload-builder.d.ts +2 -2
  46. package/src/lib/orchestrator/internals/payload-builder.js +119 -100
  47. package/src/lib/orchestrator/internals/pc-usd-oracle.js +121 -129
  48. package/src/lib/orchestrator/internals/pc20/address-codec.d.ts +31 -0
  49. package/src/lib/orchestrator/internals/pc20/address-codec.js +124 -0
  50. package/src/lib/orchestrator/internals/pc20/chain-namespace.d.ts +68 -0
  51. package/src/lib/orchestrator/internals/pc20/chain-namespace.js +136 -0
  52. package/src/lib/orchestrator/internals/pc20/errors.d.ts +160 -0
  53. package/src/lib/orchestrator/internals/pc20/errors.js +236 -0
  54. package/src/lib/orchestrator/internals/pc20/export.d.ts +172 -0
  55. package/src/lib/orchestrator/internals/pc20/export.js +412 -0
  56. package/src/lib/orchestrator/internals/pc20/gate.d.ts +67 -0
  57. package/src/lib/orchestrator/internals/pc20/gate.js +184 -0
  58. package/src/lib/orchestrator/internals/pc20/resolver.d.ts +187 -0
  59. package/src/lib/orchestrator/internals/pc20/resolver.js +658 -0
  60. package/src/lib/orchestrator/internals/pc20/svm.d.ts +189 -0
  61. package/src/lib/orchestrator/internals/pc20/svm.js +286 -0
  62. package/src/lib/orchestrator/internals/pc20/tracking.d.ts +46 -0
  63. package/src/lib/orchestrator/internals/pc20/tracking.js +109 -0
  64. package/src/lib/orchestrator/internals/preflight.js +1 -1
  65. package/src/lib/orchestrator/internals/progress-route-hooks.js +2 -2
  66. package/src/lib/orchestrator/internals/push-chain-tx.js +319 -331
  67. package/src/lib/orchestrator/internals/quote.js +90 -94
  68. package/src/lib/orchestrator/internals/rescue.js +76 -79
  69. package/src/lib/orchestrator/internals/response-builder.d.ts +28 -1
  70. package/src/lib/orchestrator/internals/response-builder.js +817 -701
  71. package/src/lib/orchestrator/internals/route-handlers.d.ts +7 -15
  72. package/src/lib/orchestrator/internals/route-handlers.js +1512 -1178
  73. package/src/lib/orchestrator/internals/signing.js +78 -83
  74. package/src/lib/orchestrator/internals/svm-bridge.d.ts +2 -2
  75. package/src/lib/orchestrator/internals/svm-bridge.js +161 -93
  76. package/src/lib/orchestrator/internals/svm-helpers.d.ts +15 -1
  77. package/src/lib/orchestrator/internals/svm-helpers.js +68 -24
  78. package/src/lib/orchestrator/internals/svm-rent.d.ts +19 -1
  79. package/src/lib/orchestrator/internals/svm-rent.js +163 -141
  80. package/src/lib/orchestrator/internals/tx-transformer.js +26 -23
  81. package/src/lib/orchestrator/internals/uea-manager.js +89 -86
  82. package/src/lib/orchestrator/max-pc-for-gas.js +5 -6
  83. package/src/lib/orchestrator/orchestrator.js +217 -200
  84. package/src/lib/orchestrator/orchestrator.types.d.ts +121 -1
  85. package/src/lib/orchestrator/orchestrator.types.js +19 -0
  86. package/src/lib/orchestrator/payload-builders.d.ts +2 -2
  87. package/src/lib/orchestrator/payload-builders.js +26 -24
  88. package/src/lib/orchestrator/route-detector.js +26 -9
  89. package/src/lib/orchestrator/svm-idl/ix-encoder.js +1 -2
  90. package/src/lib/orchestrator/svm-idl/normalize-address.js +2 -2
  91. package/src/lib/orchestrator/svm-idl/resolve.js +1 -2
  92. package/src/lib/price-fetch/price-fetch.js +67 -68
  93. package/src/lib/progress-hook/progress-hook.js +124 -72
  94. package/src/lib/push-chain/push-chain.js +171 -153
  95. package/src/lib/push-client/push-client.js +223 -237
  96. package/src/lib/universal/account/account.js +349 -365
  97. package/src/lib/universal/signer/signer.d.ts +2 -1
  98. package/src/lib/universal/signer/signer.js +315 -299
  99. package/src/lib/universal-tx-detector/auto-detect.js +55 -60
  100. package/src/lib/universal-tx-detector/cascade.js +160 -172
  101. package/src/lib/universal-tx-detector/child-inbounds.js +67 -75
  102. package/src/lib/universal-tx-detector/classify.js +2 -3
  103. package/src/lib/universal-tx-detector/detector-svm.js +139 -147
  104. package/src/lib/universal-tx-detector/detector.js +145 -157
  105. package/src/lib/universal-tx-detector/normalize.js +7 -7
  106. package/src/lib/utils/external-tx-hash.js +1 -2
  107. package/src/lib/utils.d.ts +44 -0
  108. package/src/lib/utils.js +762 -683
  109. package/src/lib/vm-client/evm-client.js +186 -202
  110. package/src/lib/vm-client/svm-client.d.ts +1 -1
  111. package/src/lib/vm-client/svm-client.js +220 -224
  112. package/src/lib/vm-client/vm-client.types.d.ts +14 -2
@@ -1,4 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.getRequiredNativeBalanceForOutbound = getRequiredNativeBalanceForOutbound;
27
+ exports.shouldSkipFeeLockingForOutbound = shouldSkipFeeLockingForOutbound;
28
+ exports.floorR3NativeValueForGas = floorR3NativeValueForGas;
29
+ exports.resolveR2DestinationFundsToken = resolveR2DestinationFundsToken;
30
+ exports.executeMultiChain = executeMultiChain;
31
+ exports.executeUoaToCea = executeUoaToCea;
32
+ exports.executeUoaToCeaSvm = executeUoaToCeaSvm;
33
+ exports.executeCeaToPush = executeCeaToPush;
34
+ exports.executeCeaToPushSvm = executeCeaToPushSvm;
35
+ exports.executeCeaToCea = executeCeaToCea;
36
+ exports.buildPayloadForRoute = buildPayloadForRoute;
37
+ const gate_1 = require("./pc20/gate");
38
+ const export_1 = require("./pc20/export");
2
39
  /**
3
40
  * Route handler functions extracted from Orchestrator.
4
41
  *
@@ -14,18 +51,6 @@
14
51
  * Every method receives an `OrchestratorContext` as first parameter and an
15
52
  * `executeFn` callback that replaces the former `this.execute()` call.
16
53
  */
17
- Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.getRequiredNativeBalanceForOutbound = getRequiredNativeBalanceForOutbound;
19
- exports.shouldSkipFeeLockingForOutbound = shouldSkipFeeLockingForOutbound;
20
- exports.resolveR2DestinationFundsToken = resolveR2DestinationFundsToken;
21
- exports.executeMultiChain = executeMultiChain;
22
- exports.executeUoaToCea = executeUoaToCea;
23
- exports.executeUoaToCeaSvm = executeUoaToCeaSvm;
24
- exports.executeCeaToPush = executeCeaToPush;
25
- exports.executeCeaToPushSvm = executeCeaToPushSvm;
26
- exports.executeCeaToCea = executeCeaToCea;
27
- exports.buildPayloadForRoute = buildPayloadForRoute;
28
- const tslib_1 = require("tslib");
29
54
  const web3_js_1 = require("@solana/web3.js");
30
55
  const viem_1 = require("viem");
31
56
  const context_1 = require("./context");
@@ -43,8 +68,11 @@ const price_fetch_1 = require("../../price-fetch/price-fetch");
43
68
  const route_detector_1 = require("../route-detector");
44
69
  const cea_utils_1 = require("../cea-utils");
45
70
  const build_payload_1 = require("../svm-idl/build-payload");
46
- const data_utils_1 = require("../data-utils");
47
71
  const payload_builders_1 = require("../payload-builders");
72
+ const svm_1 = require("./pc20/svm");
73
+ const svm_rent_2 = require("./svm-rent");
74
+ const data_utils_1 = require("../data-utils");
75
+ const payload_builders_2 = require("../payload-builders");
48
76
  const selectors_1 = require("../../constants/selectors");
49
77
  const erc20_evm_1 = require("../../constants/abi/erc20.evm");
50
78
  const push_chain_1 = require("../../push-chain/push-chain");
@@ -55,6 +83,16 @@ function shouldSkipFeeLockingForOutbound(opts) {
55
83
  return (opts.isUEADeployed &&
56
84
  opts.balance >= getRequiredNativeBalanceForOutbound(opts.nativeValueForGas, opts.gasReserve));
57
85
  }
86
+ /**
87
+ * Keep Route 3's USD/category sizing, but never let it undercut the live
88
+ * WPC -> destination-gas-token swap requirement. `bufferedSwapValue` is the
89
+ * balance-aware result from estimateNativeValueForSwap; protocol fees are
90
+ * native PC and therefore sit on top of that swap budget.
91
+ */
92
+ function floorR3NativeValueForGas(sizedValue, bufferedSwapValue, protocolFee) {
93
+ const livePoolFloor = bufferedSwapValue + protocolFee;
94
+ return livePoolFloor > sizedValue ? livePoolFloor : sizedValue;
95
+ }
58
96
  // =============================================================================
59
97
  // Route 2 (UEA → CEA) phase helpers
60
98
  // =============================================================================
@@ -64,7 +102,6 @@ function shouldSkipFeeLockingForOutbound(opts) {
64
102
  * CEA address, so it can run before the 203 account-resolution phase.
65
103
  */
66
104
  function resolveR2Prc20TokenEvm(params, targetChain, pushNetwork) {
67
- var _a;
68
105
  let prc20Token = selectors_1.ZERO_ADDRESS;
69
106
  let burnAmount = BigInt(0);
70
107
  if (params.migration) {
@@ -72,7 +109,15 @@ function resolveR2Prc20TokenEvm(params, targetChain, pushNetwork) {
72
109
  prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, pushNetwork);
73
110
  burnAmount = BigInt(0);
74
111
  }
75
- else if ((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) {
112
+ else if (params.funds?.amount) {
113
+ // PC20 export: the token being locked IS the Push-native PC20. There is no
114
+ // synthetic PRC20 to look up, and the resolver already established it.
115
+ const pc20 = params._pc20;
116
+ if (pc20) {
117
+ prc20Token = pc20.pushAddress;
118
+ burnAmount = params.funds.amount;
119
+ return { prc20Token, burnAmount };
120
+ }
76
121
  // User explicitly specified funds with token
77
122
  const token = params.funds.token;
78
123
  if (token) {
@@ -93,22 +138,23 @@ function resolveR2Prc20TokenEvm(params, targetChain, pushNetwork) {
93
138
  return { prc20Token, burnAmount };
94
139
  }
95
140
  function resolveR2DestinationFundsToken(token, targetChain, pushNetwork) {
96
- var _a, _b, _c;
97
141
  const pushToken = token;
98
142
  if (!pushToken.prc20Address || pushToken.sourceChain !== targetChain) {
99
143
  return token;
100
144
  }
101
- const destinationTokens = (_a = tokens_1.MOVEABLE_TOKENS[targetChain]) !== null && _a !== void 0 ? _a : [];
145
+ const destinationTokens = tokens_1.MOVEABLE_TOKENS[targetChain] ?? [];
102
146
  const nativePrc20 = (0, helpers_1.getNativePRC20ForChain)(targetChain, pushNetwork);
103
147
  if (pushToken.prc20Address.toLowerCase() === nativePrc20.toLowerCase()) {
104
- return ((_b = destinationTokens.find((candidate) => candidate.mechanism === 'native')) !== null && _b !== void 0 ? _b : {
105
- symbol: token.symbol,
106
- decimals: token.decimals,
107
- address: selectors_1.ZERO_ADDRESS,
108
- mechanism: 'native',
109
- });
148
+ return (destinationTokens.find((candidate) => candidate.mechanism === 'native') ??
149
+ {
150
+ symbol: token.symbol,
151
+ decimals: token.decimals,
152
+ address: selectors_1.ZERO_ADDRESS,
153
+ mechanism: 'native',
154
+ });
110
155
  }
111
- return ((_c = destinationTokens.find((candidate) => candidate.symbol === token.symbol)) !== null && _c !== void 0 ? _c : token);
156
+ return (destinationTokens.find((candidate) => candidate.symbol === token.symbol) ??
157
+ token);
112
158
  }
113
159
  /**
114
160
  * SVM counterpart to `resolveR2Prc20TokenEvm`. SVM adds an EXECUTE-only
@@ -116,10 +162,15 @@ function resolveR2DestinationFundsToken(token, targetChain, pushNetwork) {
116
162
  * a PRC-20 token for the chain-namespace lookup.
117
163
  */
118
164
  function resolveR2Prc20TokenSvm(params, targetChain, pushNetwork, hasSvmExecute) {
119
- var _a;
120
165
  let prc20Token = selectors_1.ZERO_ADDRESS;
121
166
  let burnAmount = BigInt(0);
122
- if ((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) {
167
+ if (params.funds?.amount) {
168
+ // PC20 export: the locked token IS the Push-native PC20 — no synthetic
169
+ // PRC20 lookup. Mirrors the EVM branch in resolveR2Prc20TokenEvm.
170
+ const pc20 = params._pc20;
171
+ if (pc20) {
172
+ return { prc20Token: pc20.pushAddress, burnAmount: params.funds.amount };
173
+ }
123
174
  const token = params.funds.token;
124
175
  if (token) {
125
176
  prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token).address;
@@ -143,15 +194,23 @@ function resolveR2Prc20TokenSvm(params, targetChain, pushNetwork, hasSvmExecute)
143
194
  return { prc20Token, burnAmount };
144
195
  }
145
196
  function resolveR3SvmDrain(params, sourceChain, pushNetwork) {
146
- var _a;
147
197
  let drainAmount = BigInt(0);
148
198
  let tokenMintHex;
149
199
  let splMintBase58;
150
200
  let prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, pushNetwork);
151
- if (((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) && params.funds.amount > BigInt(0)) {
201
+ if (params.funds?.amount && params.funds.amount > BigInt(0)) {
152
202
  drainAmount = params.funds.amount;
153
203
  const token = params.funds.token;
154
- if ((token === null || token === void 0 ? void 0 : token.mechanism) === 'approve') {
204
+ const pc20 = params._pc20;
205
+ if (pc20?.direction === 'import' && token) {
206
+ // PC20 CEA burn: the drained token is the wrapper mint. The outbound
207
+ // relay leg stays on the native PRC-20 (it is a payload relay, not a
208
+ // PC20 export), so prc20Token is left as initialized above.
209
+ const mintPk = new web3_js_1.PublicKey(token.address);
210
+ tokenMintHex = ('0x' + Buffer.from(mintPk.toBytes()).toString('hex'));
211
+ splMintBase58 = token.address;
212
+ }
213
+ else if (token?.mechanism === 'approve') {
155
214
  const mintPk = new web3_js_1.PublicKey(token.address);
156
215
  tokenMintHex = ('0x' + Buffer.from(mintPk.toBytes()).toString('hex'));
157
216
  splMintBase58 = token.address;
@@ -164,21 +223,37 @@ function resolveR3SvmDrain(params, sourceChain, pushNetwork) {
164
223
  return { prc20Token, drainAmount, tokenMintHex, splMintBase58 };
165
224
  }
166
225
  function buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, inboundNonce) {
167
- var _a, _b;
226
+ // PC20: the unlocked Push-native token lands on the UEA (program-enforced
227
+ // recipient), so a funds-only burn whose destination differs needs a
228
+ // Push-side forward — same rule as the EVM R3 branch.
229
+ const extraPc20 = params._pc20;
230
+ // Always forward for PC20 funds — a payload-less burn emits a selector-only
231
+ // raw_payload that the chain hard-fails (see the R3 EVM branch note).
232
+ const pc20Forward = extraPc20?.direction === 'import' && Boolean(params.funds?.amount);
168
233
  const shouldBuildPushPayload = (0, data_utils_1.hasExecutablePayloadData)(params.data) ||
169
- Boolean(((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) && ((_b = params.value) !== null && _b !== void 0 ? _b : BigInt(0)) > BigInt(0));
234
+ Boolean(params.funds?.amount && (params.value ?? BigInt(0)) > BigInt(0)) ||
235
+ pc20Forward;
170
236
  if (!shouldBuildPushPayload) {
171
237
  return undefined;
172
238
  }
173
239
  const executeParams = (0, helpers_1.toExecuteParams)(params);
174
- const multicallCtx = Object.assign(Object.assign({}, ctx), { universalSigner: Object.assign(Object.assign({}, ctx.universalSigner), { account: Object.assign(Object.assign({}, ctx.universalSigner.account), { chain: sourceChain }) }) });
175
- const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(multicallCtx, executeParams.to, (0, payload_builders_1.buildExecuteMulticall)({
176
- execute: executeParams,
240
+ const multicallCtx = {
241
+ ...ctx,
242
+ universalSigner: {
243
+ ...ctx.universalSigner,
244
+ account: {
245
+ ...ctx.universalSigner.account,
246
+ chain: sourceChain,
247
+ },
248
+ },
249
+ };
250
+ const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(multicallCtx, executeParams.to, (0, payload_builders_2.buildExecuteMulticall)({
251
+ execute: (0, gate_1.assertLegacyFunds)(executeParams),
177
252
  ueaAddress,
178
253
  allowSelfValueCall: true,
179
254
  }));
180
- const inboundPayload = (0, payload_builders_1.buildInboundUniversalPayloadSvm)(multicallPayload, {
181
- nonce: inboundNonce !== null && inboundNonce !== void 0 ? inboundNonce : BigInt(0),
255
+ const inboundPayload = (0, payload_builders_2.buildInboundUniversalPayloadSvm)(multicallPayload, {
256
+ nonce: inboundNonce ?? BigInt(0),
182
257
  });
183
258
  return (0, viem_1.hexToBytes)(inboundPayload);
184
259
  }
@@ -187,17 +262,27 @@ function buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, inboundNon
187
262
  * resolved `ceaAddress` because the native-value self-call branch skips the
188
263
  * multicall when the target IS the CEA (avoids a CEA._handleMulticall revert).
189
264
  */
190
- function buildR2CeaPayloadEvm(params, ceaAddress, targetAddress, targetChain, pushNetwork) {
191
- var _a, _b, _c, _d, _e;
265
+ function buildR2CeaPayloadEvm(params, ceaAddress, targetAddress, targetChain, pushNetwork,
266
+ /**
267
+ * PC20 export only: the destination wrapper address (registered, or predicted
268
+ * for a first export). Destination settlement mints the wrapper into the CEA
269
+ * before running user data, so delivering funds to the requested recipient
270
+ * needs an explicit wrapper.transfer() built here.
271
+ */
272
+ pc20Wrapper) {
192
273
  if (params.migration) {
193
- return (0, payload_builders_1.buildMigrationPayload)();
274
+ return (0, payload_builders_2.buildMigrationPayload)();
194
275
  }
195
276
  const ceaMulticalls = [];
196
277
  const hasData = (0, data_utils_1.hasExecutablePayloadData)(params.data);
197
- const fundsToken = ((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount)
198
- ? resolveR2DestinationFundsToken(params.funds.token, targetChain, pushNetwork)
278
+ const fundsToken = params.funds?.amount
279
+ ? pc20Wrapper
280
+ // A PC20 has no MoveableToken form; the wrapper is an ordinary ERC20 on
281
+ // the destination, so it forwards through the same transfer() path.
282
+ ? { address: pc20Wrapper, mechanism: 'approve' }
283
+ : resolveR2DestinationFundsToken(params.funds.token, targetChain, pushNetwork)
199
284
  : undefined;
200
- const nativeFundsValue = ((_b = params.funds) === null || _b === void 0 ? void 0 : _b.amount) && (fundsToken === null || fundsToken === void 0 ? void 0 : fundsToken.mechanism) === 'native'
285
+ const nativeFundsValue = params.funds?.amount && fundsToken?.mechanism === 'native'
201
286
  ? params.funds.amount
202
287
  : BigInt(0);
203
288
  if (hasData) {
@@ -208,7 +293,7 @@ function buildR2CeaPayloadEvm(params, ceaAddress, targetAddress, targetChain, pu
208
293
  // When ERC-20 funds are provided with a single payload, auto-prepend a
209
294
  // transfer() call so the tokens minted to the CEA are forwarded to the
210
295
  // target address. Mirrors the Route 1 behavior in buildExecuteMulticall.
211
- if ((_c = params.funds) === null || _c === void 0 ? void 0 : _c.amount) {
296
+ if (params.funds?.amount) {
212
297
  if (fundsToken && fundsToken.mechanism !== 'native') {
213
298
  const erc20Transfer = (0, viem_1.encodeFunctionData)({
214
299
  abi: erc20_evm_1.ERC20_EVM,
@@ -226,13 +311,13 @@ function buildR2CeaPayloadEvm(params, ceaAddress, targetAddress, targetChain, pu
226
311
  // contract receives it alongside the payload call.
227
312
  ceaMulticalls.push({
228
313
  to: targetAddress,
229
- value: ((_d = params.value) !== null && _d !== void 0 ? _d : BigInt(0)) + nativeFundsValue,
314
+ value: (params.value ?? BigInt(0)) + nativeFundsValue,
230
315
  data: params.data,
231
316
  });
232
317
  }
233
318
  }
234
319
  else {
235
- if (((_e = params.funds) === null || _e === void 0 ? void 0 : _e.amount) && fundsToken && fundsToken.mechanism !== 'native') {
320
+ if (params.funds?.amount && fundsToken && fundsToken.mechanism !== 'native') {
236
321
  // ERC-20 funds: forward via ERC20.transfer from the CEA.
237
322
  const erc20Transfer = (0, viem_1.encodeFunctionData)({
238
323
  abi: erc20_evm_1.ERC20_EVM,
@@ -268,7 +353,7 @@ function buildR2CeaPayloadEvm(params, ceaAddress, targetAddress, targetChain, pu
268
353
  }
269
354
  }
270
355
  }
271
- return (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
356
+ return (0, payload_builders_2.buildCeaMulticallPayload)(ceaMulticalls);
272
357
  }
273
358
  const SIZER_HOOK_BY_ROUTE = {
274
359
  R3: {
@@ -287,7 +372,7 @@ const SIZER_HOOK_BY_ROUTE = {
287
372
  */
288
373
  function fireSizingHook(ctx, route, chain, sizing) {
289
374
  const hook = SIZER_HOOK_BY_ROUTE[route][sizing.category];
290
- const ONE_USD_8D = BigInt(100000000);
375
+ const ONE_USD_8D = BigInt(100_000_000);
291
376
  const totalDepositUSD = sizing.category === 'A' ? ONE_USD_8D : sizing.gasUsd;
292
377
  (0, context_1.fireProgressHook)(ctx, hook, chain, sizing.gasLegNativePc, sizing.overflowNativePc, totalDepositUSD);
293
378
  }
@@ -297,44 +382,42 @@ function fireSizingHook(ctx, route, chain, sizing) {
297
382
  /**
298
383
  * Dispatch a universal execution to the correct route handler.
299
384
  */
300
- function executeMultiChain(ctx, params, executeFn) {
301
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
302
- // Validate route parameters
303
- (0, route_detector_1.validateRouteParams)(params, {
304
- clientChain: ctx.universalSigner.account.chain,
305
- });
306
- // Detect the transaction route
307
- const route = (0, route_detector_1.detectRoute)(params);
308
- (0, context_1.printLog)(ctx, `executeMultiChain — detected route: ${route}, params: ${JSON.stringify({
309
- to: typeof params.to === 'string'
310
- ? params.to
311
- : { address: params.to.address, chain: params.to.chain },
312
- from: params.from,
313
- hasValue: params.value !== undefined,
314
- hasData: params.data !== undefined,
315
- hasFunds: params.funds !== undefined,
316
- }, null, 2)}`);
317
- let response;
318
- switch (route) {
319
- case route_detector_1.TransactionRoute.UOA_TO_PUSH:
320
- response = yield executeFn((0, helpers_1.toExecuteParams)(params));
321
- break;
322
- case route_detector_1.TransactionRoute.UOA_TO_CEA:
323
- response = yield executeUoaToCea(ctx, params, executeFn);
324
- break;
325
- case route_detector_1.TransactionRoute.CEA_TO_PUSH:
326
- response = yield executeCeaToPush(ctx, params, executeFn);
327
- break;
328
- case route_detector_1.TransactionRoute.CEA_TO_CEA:
329
- response = yield executeCeaToCea(ctx, params, executeFn);
330
- break;
331
- default:
332
- throw new Error(`Unknown transaction route: ${route}`);
333
- }
334
- // Set the route on the response for .wait() to use
335
- response.route = route;
336
- return response;
385
+ async function executeMultiChain(ctx, params, executeFn) {
386
+ // Validate route parameters
387
+ (0, route_detector_1.validateRouteParams)(params, {
388
+ clientChain: ctx.universalSigner.account.chain,
337
389
  });
390
+ // Detect the transaction route
391
+ const route = (0, route_detector_1.detectRoute)(params);
392
+ (0, context_1.printLog)(ctx, `executeMultiChain — detected route: ${route}, params: ${JSON.stringify({
393
+ to: typeof params.to === 'string'
394
+ ? params.to
395
+ : { address: params.to.address, chain: params.to.chain },
396
+ from: params.from,
397
+ hasValue: params.value !== undefined,
398
+ hasData: params.data !== undefined,
399
+ hasFunds: params.funds !== undefined,
400
+ }, null, 2)}`);
401
+ let response;
402
+ switch (route) {
403
+ case route_detector_1.TransactionRoute.UOA_TO_PUSH:
404
+ response = await executeFn((0, helpers_1.toExecuteParams)(params));
405
+ break;
406
+ case route_detector_1.TransactionRoute.UOA_TO_CEA:
407
+ response = await executeUoaToCea(ctx, params, executeFn);
408
+ break;
409
+ case route_detector_1.TransactionRoute.CEA_TO_PUSH:
410
+ response = await executeCeaToPush(ctx, params, executeFn);
411
+ break;
412
+ case route_detector_1.TransactionRoute.CEA_TO_CEA:
413
+ response = await executeCeaToCea(ctx, params, executeFn);
414
+ break;
415
+ default:
416
+ throw new Error(`Unknown transaction route: ${route}`);
417
+ }
418
+ // Set the route on the response for .wait() to use
419
+ response.route = route;
420
+ return response;
338
421
  }
339
422
  // ---------------------------------------------------------------------------
340
423
  // executeUoaToCea (Route 2 — EVM)
@@ -349,257 +432,289 @@ function executeMultiChain(ctx, params, executeFn) {
349
432
  * The multicall is executed through the normal execute() flow which handles
350
433
  * fee-locking on the origin chain and signature verification.
351
434
  */
352
- function executeUoaToCea(ctx, params, executeFn) {
353
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
354
- var _a, _b, _c, _d, _e;
355
- const target = params.to;
356
- const targetChain = target.chain;
357
- const targetAddress = target.address;
358
- const isSvm = (0, payload_builders_1.isSvmChain)(targetChain);
359
- // Validate target address based on VM type
360
- if (isSvm) {
361
- // SVM: 32-byte hex address
362
- if (!(0, payload_builders_1.isValidSolanaHexAddress)(targetAddress)) {
363
- throw new Error(`Invalid Solana address: ${targetAddress}. ` +
364
- `Expected 0x + 64 hex chars (32 bytes).`);
365
- }
366
- const ZERO_32 = ('0x' + '0'.repeat(64));
367
- if (targetAddress.toLowerCase() === ZERO_32.toLowerCase()) {
368
- throw new Error(`Cannot send to zero address on Solana. ` +
369
- `This would result in permanent loss of funds.`);
370
- }
435
+ async function executeUoaToCea(ctx, params, executeFn) {
436
+ const target = params.to;
437
+ const targetChain = target.chain;
438
+ const targetAddress = target.address;
439
+ const isSvm = (0, payload_builders_2.isSvmChain)(targetChain);
440
+ // Validate target address based on VM type
441
+ if (isSvm) {
442
+ // SVM: 32-byte hex address
443
+ if (!(0, payload_builders_2.isValidSolanaHexAddress)(targetAddress)) {
444
+ throw new Error(`Invalid Solana address: ${targetAddress}. ` +
445
+ `Expected 0x + 64 hex chars (32 bytes).`);
371
446
  }
372
- else {
373
- // EVM: 20-byte hex address
374
- // Zero address is allowed for multicall (data is array) — the actual targets are in the data entries.
375
- const isMulticall = Array.isArray(params.data);
376
- if (targetAddress.toLowerCase() === selectors_1.ZERO_ADDRESS.toLowerCase() && !isMulticall) {
377
- throw new Error(`Cannot send to zero address (0x0000...0000). ` +
378
- `This would result in permanent loss of funds.`);
379
- }
380
- }
381
- // Validate chain supports outbound operations
382
- if (!(0, cea_utils_1.chainSupportsOutbound)(targetChain)) {
383
- throw new Error(`Chain ${targetChain} does not support outbound operations. ` +
384
- `Supported chains: BNB_TESTNET, ETHEREUM_SEPOLIA, SOLANA_DEVNET, etc.`);
385
- }
386
- // Branch based on VM type
387
- if (isSvm) {
388
- return executeUoaToCeaSvm(ctx, params, target, executeFn);
447
+ const ZERO_32 = ('0x' + '0'.repeat(64));
448
+ if (targetAddress.toLowerCase() === ZERO_32.toLowerCase()) {
449
+ throw new Error(`Cannot send to zero address on Solana. ` +
450
+ `This would result in permanent loss of funds.`);
389
451
  }
390
- // ===== EVM path (existing logic) =====
391
- // R2 pre-broadcast progress: external chain detected
392
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_201, targetChain, targetAddress);
393
- // Compute UEA address (sync, offchain derivation — no RPC).
394
- const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
395
- (0, context_1.printLog)(ctx, `executeUoaToCea — target chain: ${targetChain}, target address: ${targetAddress}, UEA: ${ueaAddress}`);
396
- // --- Token resolution (chain-local, before account resolution) ---
397
- // Resolve the PRC-20 burn token + burn amount ahead of CEA resolution.
398
- // Chain-local — see `resolveR2Prc20TokenEvm` above.
399
- const { prc20Token, burnAmount } = resolveR2Prc20TokenEvm(params, targetChain, ctx.pushNetwork);
400
- if (params.value && params.value > BigInt(0)) {
401
- (0, context_1.printLog)(ctx, `executeUoaToCea — auto-selected native PRC-20 ${prc20Token} for chain ${targetChain}, amount: ${burnAmount.toString()}`);
402
- }
403
- else if (!params.migration &&
404
- !((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) &&
405
- (0, data_utils_1.hasExecutablePayloadData)(params.data)) {
406
- (0, context_1.printLog)(ctx, `executeUoaToCea — PAYLOAD-only: using native PRC-20 ${prc20Token} for chain ${targetChain} with zero burn amount`);
452
+ }
453
+ else {
454
+ // EVM: 20-byte hex address
455
+ // Zero address is allowed for multicall (data is array) — the actual targets are in the data entries.
456
+ const isMulticall = Array.isArray(params.data);
457
+ if (targetAddress.toLowerCase() === selectors_1.ZERO_ADDRESS.toLowerCase() && !isMulticall) {
458
+ throw new Error(`Cannot send to zero address (0x0000...0000). ` +
459
+ `This would result in permanent loss of funds.`);
407
460
  }
408
- const gasLimitForQuery = (_b = params.gasLimit) !== null && _b !== void 0 ? _b : BigInt(0);
409
- // --- 202: Gas estimation (spec-ordered before 203) ---
410
- let gasFee = BigInt(0);
411
- let protocolFee = BigInt(0);
412
- let gasToken = selectors_1.ZERO_ADDRESS;
413
- let universalCoreAddress;
414
- if (prc20Token !== selectors_1.ZERO_ADDRESS) {
415
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_01, targetChain);
416
- try {
417
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimitForQuery, targetChain);
418
- gasFee = result.gasFee;
419
- protocolFee = result.protocolFee;
420
- gasToken = result.gasToken;
421
- universalCoreAddress = result.universalCoreAddress;
422
- (0, context_1.printLog)(ctx, `executeUoaToCea — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}`);
423
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_02, targetChain, protocolFee, gasFee);
424
- }
425
- catch (err) {
426
- throw new Error(`Failed to query outbound gas fee: ${err}`);
427
- }
461
+ }
462
+ // Validate chain supports outbound operations
463
+ if (!(0, cea_utils_1.chainSupportsOutbound)(targetChain)) {
464
+ throw new Error(`Chain ${targetChain} does not support outbound operations. ` +
465
+ `Supported chains: BNB_TESTNET, ETHEREUM_SEPOLIA, SOLANA_DEVNET, etc.`);
466
+ }
467
+ // Branch based on VM type
468
+ if (isSvm) {
469
+ return executeUoaToCeaSvm(ctx, params, target, executeFn);
470
+ }
471
+ // ===== EVM path (existing logic) =====
472
+ // R2 pre-broadcast progress: external chain detected
473
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_201, targetChain, targetAddress);
474
+ // Compute UEA address (sync, offchain derivation — no RPC).
475
+ const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
476
+ (0, context_1.printLog)(ctx, `executeUoaToCea — target chain: ${targetChain}, target address: ${targetAddress}, UEA: ${ueaAddress}`);
477
+ // --- Token resolution (chain-local, before account resolution) ---
478
+ // Resolve the PRC-20 burn token + burn amount ahead of CEA resolution.
479
+ // Chain-local — see `resolveR2Prc20TokenEvm` above.
480
+ const { prc20Token, burnAmount } = resolveR2Prc20TokenEvm(params, targetChain, ctx.pushNetwork);
481
+ if (params.value && params.value > BigInt(0)) {
482
+ (0, context_1.printLog)(ctx, `executeUoaToCea — auto-selected native PRC-20 ${prc20Token} for chain ${targetChain}, amount: ${burnAmount.toString()}`);
483
+ }
484
+ else if (!params.migration &&
485
+ !params.funds?.amount &&
486
+ (0, data_utils_1.hasExecutablePayloadData)(params.data)) {
487
+ (0, context_1.printLog)(ctx, `executeUoaToCea — PAYLOAD-only: using native PRC-20 ${prc20Token} for chain ${targetChain} with zero burn amount`);
488
+ }
489
+ const gasLimitForQuery = params.gasLimit ?? BigInt(0);
490
+ // --- 202: Gas estimation (spec-ordered before 203) ---
491
+ let gasFee = BigInt(0);
492
+ let protocolFee = BigInt(0);
493
+ let gasToken = selectors_1.ZERO_ADDRESS;
494
+ let universalCoreAddress;
495
+ if (prc20Token !== selectors_1.ZERO_ADDRESS) {
496
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_01, targetChain);
497
+ try {
498
+ // PC20 exports must not use getOutboundTxGasAndFees — only
499
+ // getPC20ExportGasAndFees accounts for the first-export deployment
500
+ // overhead, and quoting the wrong one strands the destination transfer.
501
+ const pc20Export = params._pc20;
502
+ const result = pc20Export?.direction === 'export'
503
+ ? await (0, export_1.queryPC20OutboundGasFee)(ctx, prc20Token, gasLimitForQuery, targetChain)
504
+ : await (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimitForQuery, targetChain);
505
+ gasFee = result.gasFee;
506
+ protocolFee = result.protocolFee;
507
+ gasToken = result.gasToken;
508
+ universalCoreAddress = result.universalCoreAddress;
509
+ (0, context_1.printLog)(ctx, `executeUoaToCea — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}`);
510
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_02, targetChain, protocolFee, gasFee);
428
511
  }
429
- // --- 203: CEA resolution + UEA status (single round-trip burst) ---
430
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_01, targetChain);
431
- const signerChain = ctx.universalSigner.account.chain;
432
- const isNativePushEOA = (0, helpers_1.isPushChain)(signerChain);
433
- const [{ cea: ceaAddress, isDeployed: ceaDeployed }, [ueaCode, ueaBalance],] = yield Promise.all([
434
- (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_c = ctx.rpcUrls[targetChain]) === null || _c === void 0 ? void 0 : _c[0]),
435
- Promise.all([
436
- ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
437
- ctx.pushClient.getBalance(ueaAddress),
438
- ]),
439
- ]);
440
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_02, ueaAddress, ceaAddress, targetChain, ceaDeployed);
441
- (0, context_1.printLog)(ctx, `executeUoaToCea — CEA address: ${ceaAddress}, deployed: ${ceaDeployed}`);
442
- const isUEADeployed = isNativePushEOA || ueaCode !== undefined;
443
- const ueaNonce = (!isUEADeployed || isNativePushEOA) ? BigInt(0) : yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
444
- // --- Build CEA payload (requires ceaAddress for self-call check) ---
445
- // Delegates to `buildR2CeaPayloadEvm` — see helper definition above.
446
- const ceaPayload = buildR2CeaPayloadEvm(params, ceaAddress, targetAddress, targetChain, ctx.pushNetwork);
447
- if (params.migration) {
448
- (0, context_1.printLog)(ctx, `executeUoaToCea — MIGRATION: using raw MIGRATION_SELECTOR payload (${ceaPayload}), native PRC-20 ${prc20Token}`);
512
+ catch (err) {
513
+ throw new Error(`Failed to query outbound gas fee: ${err}`);
449
514
  }
450
- // Build outbound request struct for the gateway
451
- // Empty CEA payloads intentionally use zero recipient to park funds in the
452
- // CEA under the post-audit single-call guard.
453
- const targetBytes = ceaPayload === '0x'
454
- ? selectors_1.ZERO_ADDRESS
455
- : ceaAddress;
456
- (0, payload_builders_1.assertCeaFundsParkingInvariant)(targetBytes, ceaPayload);
457
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, gasLimitForQuery, ceaPayload, ueaAddress, // revert recipient is the UEA
458
- (_d = params.maxPCForGas) !== null && _d !== void 0 ? _d : BigInt(0));
459
- (0, context_1.printLog)(ctx, `executeUoaToCea — outbound request: ${JSON.stringify({
460
- recipient: outboundReq.recipient,
461
- token: outboundReq.token,
462
- amount: outboundReq.amount.toString(),
463
- gasLimit: outboundReq.gasLimit.toString(),
464
- maxPCForGas: outboundReq.maxPCForGas.toString(),
465
- payloadLength: outboundReq.payload.length,
466
- revertRecipient: outboundReq.revertRecipient,
467
- }, null, 2)}`);
468
- // Get UniversalGatewayPC address
469
- const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
470
- // Build the multicall that will execute ON Push Chain from UEA context
471
- // This includes: 1) approve PRC-20 (if needed), 2) call sendUniversalTxOutbound
472
- const pushChainMulticalls = [];
473
- // ---------------------------------------------------------------------
474
- // Native-value selection — R2 does NOT use Case A/B/C gas abstraction.
475
- // That feature is scoped to R1 (fee-lock USD caps) and R3 (CEA → Push
476
- // outbound msg.value). For R2 we size `nativeValueForGas` from the
477
- // live WPC/gasToken Uniswap V3 pool quote plus a 10% safety buffer;
478
- // `swapAndBurnGas` refunds any excess as PC back to UEA via
479
- // refundUnusedGas.
480
- //
481
- // Fresh-wallet prediction (below) is retained: it estimates post-fee-lock
482
- // UPC balance for UEAs that haven't been funded yet, independent of
483
- // Case A/B/C sizing.
484
- // ---------------------------------------------------------------------
485
- const EVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for outer-tx gas
486
- const EVM_NATIVE_VALUE_SAFETY_CAP = BigInt(200e18); // 200 UPC absolute ceiling
487
- const ROUTE2_MINIMUM_DEPOSIT_USD = utils_1.Utils.helpers.parseUnits('10', 8); // $10
488
- // Effective balance: real for deployed UEAs; predicted post-fee-lock for fresh.
489
- let effectiveBalance = ueaBalance;
490
- if (!isUEADeployed && effectiveBalance <= EVM_GAS_RESERVE) {
491
- const ethPrice = yield new price_fetch_1.PriceFetch(ctx.rpcUrls).getPrice(signerChain);
492
- const nativeAmountETH = ethPrice > BigInt(0)
493
- ? (ROUTE2_MINIMUM_DEPOSIT_USD * BigInt(1e18) + (ethPrice - BigInt(1))) / ethPrice + BigInt(1)
494
- : BigInt(0);
495
- if (nativeAmountETH > BigInt(0)) {
496
- const originPrc20 = (0, helpers_1.getNativePRC20ForChain)(signerChain, ctx.pushNetwork);
497
- const predictedUPC = yield (0, gas_calculator_1.estimateDepositFromLockedNative)(ctx, nativeAmountETH, originPrc20);
498
- if (predictedUPC > BigInt(0)) {
499
- // 10% safety margin for slippage between quote and actual execution
500
- effectiveBalance = (predictedUPC * BigInt(90)) / BigInt(100);
501
- (0, context_1.printLog)(ctx, `executeUoaToCea — fresh wallet: Uniswap quote predicts ${predictedUPC.toString()} UPC ` +
502
- `from ${nativeAmountETH.toString()} wei pETH, using ${effectiveBalance.toString()} (90%)`);
503
- }
515
+ }
516
+ // --- 203: CEA resolution + UEA status (single round-trip burst) ---
517
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_01, targetChain);
518
+ const signerChain = ctx.universalSigner.account.chain;
519
+ const isNativePushEOA = (0, helpers_1.isPushChain)(signerChain);
520
+ const [{ cea: ceaAddress, isDeployed: ceaDeployed }, [ueaCode, ueaBalance],] = await Promise.all([
521
+ (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, ctx.rpcUrls[targetChain]?.[0]),
522
+ Promise.all([
523
+ ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
524
+ ctx.pushClient.getBalance(ueaAddress),
525
+ ]),
526
+ ]);
527
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_02, ueaAddress, ceaAddress, targetChain, ceaDeployed);
528
+ (0, context_1.printLog)(ctx, `executeUoaToCea — CEA address: ${ceaAddress}, deployed: ${ceaDeployed}`);
529
+ const isUEADeployed = isNativePushEOA || ueaCode !== undefined;
530
+ const ueaNonce = (!isUEADeployed || isNativePushEOA) ? BigInt(0) : await (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
531
+ // --- Build CEA payload (requires ceaAddress for self-call check) ---
532
+ // Delegates to `buildR2CeaPayloadEvm` — see helper definition above.
533
+ //
534
+ // PC20 export: resolve the destination wrapper BEFORE anything approves or
535
+ // locks the source token. A first export has no registered wrapper, so the
536
+ // address is predicted from the live registered factory; if that cannot be
537
+ // established this throws here, with nothing yet committed.
538
+ const pc20Descriptor = params._pc20;
539
+ const isPc20Export = pc20Descriptor?.direction === 'export';
540
+ let pc20Wrapper;
541
+ if (isPc20Export && pc20Descriptor) {
542
+ const { wrapper } = await (0, export_1.validatePC20Export)(ctx, pc20Descriptor, targetChain, { network: ctx.pushNetwork, rpcUrls: ctx.rpcUrls });
543
+ pc20Wrapper = wrapper.address;
544
+ (0, context_1.printLog)(ctx, `executeUoaToCea — PC20 export: destination wrapper ${pc20Wrapper} ` +
545
+ `(${wrapper.deployed ? 'registered' : 'predicted, first export'})`);
546
+ }
547
+ const ceaPayload = buildR2CeaPayloadEvm(params, ceaAddress, targetAddress, targetChain, ctx.pushNetwork, pc20Wrapper);
548
+ if (params.migration) {
549
+ (0, context_1.printLog)(ctx, `executeUoaToCea — MIGRATION: using raw MIGRATION_SELECTOR payload (${ceaPayload}), native PRC-20 ${prc20Token}`);
550
+ }
551
+ // Build outbound request struct for the gateway
552
+ // Empty CEA payloads intentionally use zero recipient to park funds in the
553
+ // CEA under the post-audit single-call guard.
554
+ const targetBytes = ceaPayload === '0x'
555
+ ? selectors_1.ZERO_ADDRESS
556
+ : ceaAddress;
557
+ (0, payload_builders_2.assertCeaFundsParkingInvariant)(targetBytes, ceaPayload);
558
+ // PC20 export: the gateway expects the PC20 selector plus the destination
559
+ // wrapper metadata ahead of the user data. Inbound is the mirror image — the
560
+ // external gateway prepends the selector there, so the SDK must not.
561
+ const outboundPayload = isPc20Export && pc20Descriptor
562
+ ? (0, export_1.buildPC20ExportPayload)({
563
+ destinationChain: targetChain,
564
+ name: pc20Descriptor.name,
565
+ symbol: pc20Descriptor.symbol,
566
+ decimals: pc20Descriptor.decimals,
567
+ destinationUserData: ceaPayload,
568
+ })
569
+ : ceaPayload;
570
+ const outboundReq = (0, payload_builders_2.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, gasLimitForQuery, outboundPayload, ueaAddress, // revert recipient is the UEA
571
+ params.maxPCForGas ?? BigInt(0));
572
+ (0, context_1.printLog)(ctx, `executeUoaToCea — outbound request: ${JSON.stringify({
573
+ recipient: outboundReq.recipient,
574
+ token: outboundReq.token,
575
+ amount: outboundReq.amount.toString(),
576
+ gasLimit: outboundReq.gasLimit.toString(),
577
+ maxPCForGas: outboundReq.maxPCForGas.toString(),
578
+ payloadLength: outboundReq.payload.length,
579
+ revertRecipient: outboundReq.revertRecipient,
580
+ }, null, 2)}`);
581
+ // Get UniversalGatewayPC address
582
+ const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
583
+ // Build the multicall that will execute ON Push Chain from UEA context
584
+ // This includes: 1) approve PRC-20 (if needed), 2) call sendUniversalTxOutbound
585
+ const pushChainMulticalls = [];
586
+ // ---------------------------------------------------------------------
587
+ // Native-value selection — R2 does NOT use Case A/B/C gas abstraction.
588
+ // That feature is scoped to R1 (fee-lock USD caps) and R3 (CEA → Push
589
+ // outbound msg.value). For R2 we size `nativeValueForGas` from the
590
+ // live WPC/gasToken Uniswap V3 pool quote plus a 10% safety buffer;
591
+ // `swapAndBurnGas` refunds any excess as PC back to UEA via
592
+ // refundUnusedGas.
593
+ //
594
+ // Fresh-wallet prediction (below) is retained: it estimates post-fee-lock
595
+ // UPC balance for UEAs that haven't been funded yet, independent of
596
+ // Case A/B/C sizing.
597
+ // ---------------------------------------------------------------------
598
+ const EVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for outer-tx gas
599
+ const ROUTE2_MINIMUM_DEPOSIT_USD = utils_1.Utils.helpers.parseUnits('10', 8); // $10
600
+ // Effective balance: real for deployed UEAs; predicted post-fee-lock for fresh.
601
+ let effectiveBalance = ueaBalance;
602
+ if (!isUEADeployed && effectiveBalance <= EVM_GAS_RESERVE) {
603
+ const ethPrice = await new price_fetch_1.PriceFetch(ctx.rpcUrls).getPrice(signerChain);
604
+ const nativeAmountETH = ethPrice > BigInt(0)
605
+ ? (ROUTE2_MINIMUM_DEPOSIT_USD * BigInt(1e18) + (ethPrice - BigInt(1))) / ethPrice + BigInt(1)
606
+ : BigInt(0);
607
+ if (nativeAmountETH > BigInt(0)) {
608
+ const originPrc20 = (0, helpers_1.getNativePRC20ForChain)(signerChain, ctx.pushNetwork);
609
+ const predictedUPC = await (0, gas_calculator_1.estimateDepositFromLockedNative)(ctx, nativeAmountETH, originPrc20);
610
+ if (predictedUPC > BigInt(0)) {
611
+ // 10% safety margin for slippage between quote and actual execution
612
+ effectiveBalance = (predictedUPC * BigInt(90)) / BigInt(100);
613
+ (0, context_1.printLog)(ctx, `executeUoaToCea — fresh wallet: Uniswap quote predicts ${predictedUPC.toString()} UPC ` +
614
+ `from ${nativeAmountETH.toString()} wei pETH, using ${effectiveBalance.toString()} (90%)`);
504
615
  }
505
616
  }
506
- // estimateNativeValueForSwap applies the full 2.2x safety buffer
507
- // internally (pool-slippage cushion + the 10% headroom that used to be
508
- // multiplied here). Folded into one place so the function's cap branch
509
- // bounds the final value — the previous outer ×110/100 on top of the
510
- // inner cap broke preflight whenever the cap fired. Over-send is refunded
511
- // by the contract.
512
- let nativeValueForGas = BigInt(0);
513
- if (universalCoreAddress && gasFee > BigInt(0)) {
514
- nativeValueForGas = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, effectiveBalance);
515
- (0, context_1.printLog)(ctx, `executeUoaToCea — nativeValueForGas=${nativeValueForGas.toString()} (includes 2.2x buffer)`);
516
- }
517
- // Hard safety cap so a broken pool price can't drain the UEA.
518
- if (nativeValueForGas > EVM_NATIVE_VALUE_SAFETY_CAP) {
519
- (0, context_1.printLog)(ctx, `executeUoaToCea — capping nativeValueForGas at 200 UPC ceiling (was ${nativeValueForGas.toString()})`);
520
- nativeValueForGas = EVM_NATIVE_VALUE_SAFETY_CAP;
521
- }
522
- // Pre-flight: warns by default when UEA cannot cover (nativeValueForGas +
523
- // gasReserve) or the PRC-20 burn amount; throws only when
524
- // options.enforceGasCheck=true. PRC-20 check fires whenever
525
- // burnAmount > 0 — user must pre-hold the burn token, even when it equals
526
- // the gas token (the gateway does transferFrom BEFORE the gas swap).
527
- let prc20BalanceR2Evm;
528
- if (burnAmount > BigInt(0)) {
529
- prc20BalanceR2Evm = yield ctx.pushClient.readContract({
530
- address: prc20Token,
531
- abi: erc20_evm_1.ERC20_EVM,
532
- functionName: 'balanceOf',
533
- args: [ueaAddress],
534
- });
535
- }
536
- const requiredNativeBalanceR2Evm = getRequiredNativeBalanceForOutbound(nativeValueForGas, EVM_GAS_RESERVE);
537
- const skipFeeLockingR2Evm = shouldSkipFeeLockingForOutbound({
538
- isUEADeployed,
539
- balance: effectiveBalance,
540
- nativeValueForGas,
541
- gasReserve: EVM_GAS_RESERVE,
542
- });
543
- (0, preflight_1.runPreflight)({
544
- ctx,
545
- ueaAddress,
546
- ueaBalance: effectiveBalance,
547
- requiredValue: nativeValueForGas,
548
- gasReserve: EVM_GAS_RESERVE,
549
- pathTag: 'R2_EVM',
550
- burnToken: prc20Token,
551
- burnAmount,
552
- prc20Balance: prc20BalanceR2Evm,
553
- enforceGasCheck: ((_e = params.options) === null || _e === void 0 ? void 0 : _e.enforceGasCheck) === true,
554
- });
555
- // Preflight success means nativeValueForGas already covers required + reserve; no adjust needed.
556
- // Build outbound multicalls (approve burn + sendUniversalTxOutbound with native value)
557
- const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
558
- prc20Token,
559
- gasToken,
560
- burnAmount,
561
- gasFee,
562
- nativeValueForGas,
563
- gatewayPcAddress,
564
- outboundRequest: outboundReq,
617
+ }
618
+ // estimateNativeValueForSwap applies the full 2.2x safety buffer
619
+ // internally (pool-slippage cushion + the 10% headroom that used to be
620
+ // multiplied here). Folded into one place so the function's cap branch
621
+ // bounds the final value — the previous outer ×110/100 on top of the
622
+ // inner cap broke preflight whenever the cap fired. Over-send is refunded
623
+ // by the contract.
624
+ let nativeValueForGas = BigInt(0);
625
+ if (universalCoreAddress && gasFee > BigInt(0)) {
626
+ nativeValueForGas = await (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, effectiveBalance);
627
+ (0, context_1.printLog)(ctx, `executeUoaToCea — nativeValueForGas=${nativeValueForGas.toString()} (includes 2.2x buffer)`);
628
+ }
629
+ // Pre-flight: warns by default when UEA cannot cover (nativeValueForGas +
630
+ // gasReserve) or the PRC-20 burn amount; throws only when
631
+ // options.enforceGasCheck=true. PRC-20 check fires whenever
632
+ // burnAmount > 0 — user must pre-hold the burn token, even when it equals
633
+ // the gas token (the gateway does transferFrom BEFORE the gas swap).
634
+ let prc20BalanceR2Evm;
635
+ if (burnAmount > BigInt(0)) {
636
+ prc20BalanceR2Evm = await ctx.pushClient.readContract({
637
+ address: prc20Token,
638
+ abi: erc20_evm_1.ERC20_EVM,
639
+ functionName: 'balanceOf',
640
+ args: [ueaAddress],
565
641
  });
566
- pushChainMulticalls.push(...outboundMulticalls);
567
- (0, context_1.printLog)(ctx, `executeUoaToCea — Push Chain multicall has ${pushChainMulticalls.length} operations`);
568
- // Execute through the normal execute() flow
569
- // This handles fee-locking on origin chain and executes the multicall from UEA context
570
- // Sum native values from multicall entries for proper fee calculation
571
- const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => { var _a; return sum + ((_a = mc.value) !== null && _a !== void 0 ? _a : BigInt(0)); }, BigInt(0));
572
- const executeParams = Object.assign({ to: ueaAddress, value: multicallNativeValue, data: pushChainMulticalls, _ueaStatus: {
573
- isDeployed: isUEADeployed,
574
- nonce: ueaNonce,
575
- balance: ueaBalance,
576
- }, _skipFeeLocking: skipFeeLockingR2Evm, _requiredFundsOverride: requiredNativeBalanceR2Evm }, (!skipFeeLockingR2Evm ? { _minimumDepositUsd: ROUTE2_MINIMUM_DEPOSIT_USD } : {}));
577
- // Signature request — user is prompted to sign the universal payload.
578
- // executeFn is monolithic (sign → broadcast), so we emit the logical
579
- // post-sign / pre-broadcast markers as a burst after it returns so
580
- // consumers see the spec-ordered stream: 204-01 → 204-02 → 204-03 → 207.
581
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_01);
582
- let response;
583
- try {
584
- response = yield executeFn(executeParams);
585
- }
586
- catch (err) {
587
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_04, err instanceof Error ? err.message : String(err));
588
- // Suppress the outer orchestrator catch's 299-02 terminal — 204-04
589
- // already surfaced the signature/broadcast failure to the consumer.
590
- ctx._routeTerminalEmitted = true;
591
- throw err;
592
- }
593
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_02);
594
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_03);
595
- // R2 broadcast marker — Push Chain tx has been built, signed, and
596
- // accepted by the node. Replaces the suppressed 107 emission for this route.
597
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_207, targetChain);
598
- // Add chain info to response
599
- response.chain = targetChain;
600
- response.chainNamespace = (0, helpers_1.getChainNamespace)(targetChain);
601
- return response;
642
+ }
643
+ const requiredNativeBalanceR2Evm = getRequiredNativeBalanceForOutbound(nativeValueForGas, EVM_GAS_RESERVE);
644
+ const skipFeeLockingR2Evm = shouldSkipFeeLockingForOutbound({
645
+ isUEADeployed,
646
+ balance: effectiveBalance,
647
+ nativeValueForGas,
648
+ gasReserve: EVM_GAS_RESERVE,
602
649
  });
650
+ (0, preflight_1.runPreflight)({
651
+ ctx,
652
+ ueaAddress,
653
+ ueaBalance: effectiveBalance,
654
+ requiredValue: nativeValueForGas,
655
+ gasReserve: EVM_GAS_RESERVE,
656
+ pathTag: 'R2_EVM',
657
+ burnToken: prc20Token,
658
+ burnAmount,
659
+ prc20Balance: prc20BalanceR2Evm,
660
+ enforceGasCheck: params.options?.enforceGasCheck === true,
661
+ });
662
+ // Preflight success means nativeValueForGas already covers required + reserve; no adjust needed.
663
+ // Build outbound multicalls (approve burn + sendUniversalTxOutbound with native value)
664
+ const outboundMulticalls = (0, payload_builders_2.buildOutboundApprovalAndCall)({
665
+ prc20Token,
666
+ gasToken,
667
+ burnAmount,
668
+ gasFee,
669
+ nativeValueForGas,
670
+ gatewayPcAddress,
671
+ outboundRequest: outboundReq,
672
+ });
673
+ pushChainMulticalls.push(...outboundMulticalls);
674
+ (0, context_1.printLog)(ctx, `executeUoaToCea — Push Chain multicall has ${pushChainMulticalls.length} operations`);
675
+ // Execute through the normal execute() flow
676
+ // This handles fee-locking on origin chain and executes the multicall from UEA context
677
+ // Sum native values from multicall entries for proper fee calculation
678
+ const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => sum + (mc.value ?? BigInt(0)), BigInt(0));
679
+ const executeParams = {
680
+ to: ueaAddress, // multicall executes from UEA
681
+ value: multicallNativeValue, // ensures correct requiredFunds calculation
682
+ data: pushChainMulticalls, // array triggers multicall mode
683
+ _ueaStatus: {
684
+ isDeployed: isUEADeployed,
685
+ nonce: ueaNonce,
686
+ balance: ueaBalance,
687
+ },
688
+ _skipFeeLocking: skipFeeLockingR2Evm,
689
+ _requiredFundsOverride: requiredNativeBalanceR2Evm,
690
+ // Ensure fee-locking deposits enough for the outbound swap whenever it is needed.
691
+ ...(!skipFeeLockingR2Evm ? { _minimumDepositUsd: ROUTE2_MINIMUM_DEPOSIT_USD } : {}),
692
+ };
693
+ // Signature request — user is prompted to sign the universal payload.
694
+ // executeFn is monolithic (sign → broadcast), so we emit the logical
695
+ // post-sign / pre-broadcast markers as a burst after it returns so
696
+ // consumers see the spec-ordered stream: 204-01 → 204-02 → 204-03 → 207.
697
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_01);
698
+ let response;
699
+ try {
700
+ response = await executeFn(executeParams);
701
+ }
702
+ catch (err) {
703
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_04, err instanceof Error ? err.message : String(err));
704
+ // Suppress the outer orchestrator catch's 299-02 terminal — 204-04
705
+ // already surfaced the signature/broadcast failure to the consumer.
706
+ ctx._routeTerminalEmitted = true;
707
+ throw err;
708
+ }
709
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_02);
710
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_03);
711
+ // R2 broadcast marker — Push Chain tx has been built, signed, and
712
+ // accepted by the node. Replaces the suppressed 107 emission for this route.
713
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_207, targetChain);
714
+ // Add chain info to response
715
+ response.chain = targetChain;
716
+ response.chainNamespace = (0, helpers_1.getChainNamespace)(targetChain);
717
+ return response;
603
718
  }
604
719
  // ---------------------------------------------------------------------------
605
720
  // executeUoaToCeaSvm (Route 2 — SVM)
@@ -612,234 +727,324 @@ function executeUoaToCea(ctx, params, executeFn) {
612
727
  * 2. Withdraw SPL: Burn PRC-20 on Push Chain, recipient gets SPL token
613
728
  * 3. Execute (CPI): Burn pSOL + execute CPI on target Solana program
614
729
  */
615
- function executeUoaToCeaSvm(ctx, params, target, executeFn) {
616
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
617
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
618
- const targetChain = target.chain;
619
- const targetAddress = target.address; // 0x-prefixed, 32 bytes
620
- const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
621
- // R2 pre-broadcast progress (SVM) — external chain detected
622
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_201, targetChain, targetAddress);
623
- const { svmPayload, targetBytes, hasExecute: hasSvmExecute, } = (0, build_payload_1.buildSvmPayloadFromParams)({
624
- data: params.data,
625
- to: target,
626
- senderUea: ueaAddress,
627
- });
628
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — target: ${targetAddress}, chain: ${targetChain}, ` +
629
- `hasSvmExecute: ${hasSvmExecute}, value: ${(_b = (_a = params.value) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : '0'}`);
630
- if (hasSvmExecute) {
631
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — encoded execute payload: ${(svmPayload.length - 2) / 2} bytes`);
632
- }
633
- (0, payload_builders_1.assertSvmPayloadWithinRelayLimit)(svmPayload, `Route 2 SVM ${targetChain}`);
634
- // --- Determine PRC-20 token and burn amount (chain-local, pre-203) ---
635
- const { prc20Token, burnAmount } = resolveR2Prc20TokenSvm(params, targetChain, ctx.pushNetwork, hasSvmExecute);
636
- if (params.value && params.value > BigInt(0)) {
637
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — auto-selected native PRC-20 ${prc20Token} for chain ${targetChain}, amount: ${burnAmount.toString()}`);
638
- }
639
- else if (!((_c = params.funds) === null || _c === void 0 ? void 0 : _c.amount) && hasSvmExecute) {
640
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — EXECUTE-only: using PRC-20 ${prc20Token} with zero burn amount`);
641
- }
642
- // --- 202: Gas estimation (spec-ordered before 203) ---
643
- let gasFee = BigInt(0);
644
- let protocolFeeSvm = BigInt(0);
645
- let gasToken = selectors_1.ZERO_ADDRESS;
646
- let universalCoreAddress;
647
- // Effective gas limit seeded from the user param. When the user omits
648
- // gasLimit, it'll be derived from the gas quote so the on-chain record
649
- // carries a non-zero compute budget the relay can use.
650
- let effectiveGasLimit = (_d = params.gasLimit) !== null && _d !== void 0 ? _d : BigInt(0);
651
- // SVM finalize budget: the Solana gateway requires gas_fee to cover the
652
- // relayer signature fee, ExecutedSubTx rent, a compute buffer, and
653
- // conditionally CEA ATA rent for SPL outbounds. The on-chain quote can be
654
- // below that floor, so we compare the quoted fee in lamports and re-query
655
- // with a bumped gasLimit when required.
656
- const svmFundsToken = burnAmount > BigInt(0)
657
- ? (_e = params.funds) === null || _e === void 0 ? void 0 : _e.token
658
- : undefined;
659
- const splMintBase58 = (svmFundsToken === null || svmFundsToken === void 0 ? void 0 : svmFundsToken.mechanism) === 'approve' ? svmFundsToken.address : undefined;
660
- if (prc20Token !== selectors_1.ZERO_ADDRESS) {
661
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_01, targetChain);
730
+ async function executeUoaToCeaSvm(ctx, params, target, executeFn) {
731
+ const targetChain = target.chain;
732
+ const targetAddress = target.address; // 0x-prefixed, 32 bytes
733
+ const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
734
+ // R2 pre-broadcast progress (SVM) — external chain detected
735
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_201, targetChain, targetAddress);
736
+ const { svmPayload, targetBytes, hasExecute: hasSvmExecute, } = (0, build_payload_1.buildSvmPayloadFromParams)({
737
+ data: params.data,
738
+ to: target,
739
+ senderUea: ueaAddress,
740
+ });
741
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — target: ${targetAddress}, chain: ${targetChain}, ` +
742
+ `hasSvmExecute: ${hasSvmExecute}, value: ${params.value?.toString() ?? '0'}`);
743
+ if (hasSvmExecute) {
744
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — encoded execute payload: ${(svmPayload.length - 2) / 2} bytes`);
745
+ }
746
+ (0, payload_builders_2.assertSvmPayloadWithinRelayLimit)(svmPayload, `Route 2 SVM ${targetChain}`);
747
+ // --- PC20 export to Solana ---
748
+ // The destination wrapper is the deterministic pc20_mint PDA; resolving it
749
+ // here (pre-approval) also tells us whether this is a first export, which
750
+ // drives the rent floor below. The generic SVM payload is replaced by the
751
+ // PC20 payload: selector || abi(namespace,name,symbol,decimals) || userData.
752
+ //
753
+ // v1 ships funds-only with EMPTY userData: settlement mints into the
754
+ // sender's CEA ATA (pc20.rs id=5 mints to cea_ata and only userData moves
755
+ // funds onward — and a recipient ATA cannot exist before the mint does on a
756
+ // first export). Tokens in the CEA ATA remain under the sender's control and
757
+ // are retrievable via the existing R2 execute path. Recipient-ATA delivery
758
+ // userData lands with the repeat-export policy.
759
+ const svmPc20Descriptor = params._pc20;
760
+ const isSvmPc20Export = svmPc20Descriptor?.direction === 'export';
761
+ let svmPc20FirstExport = false;
762
+ let pc20SvmPayload = svmPayload;
763
+ let pc20TargetBytes = targetBytes;
764
+ if (isSvmPc20Export && svmPc20Descriptor) {
765
+ const { wrapper } = await (0, export_1.validatePC20Export)(ctx, svmPc20Descriptor, targetChain, { network: ctx.pushNetwork, rpcUrls: ctx.rpcUrls });
766
+ svmPc20FirstExport = !wrapper.deployed;
767
+ // Delivery: settlement mints into the CEA ATA and CPIs exactly one
768
+ // instruction from userData. When the recipient's ATA for the wrapper
769
+ // mint already exists, that instruction is an SPL TransferChecked to it.
770
+ // Otherwise userData stays empty and funds land in the sender's CEA ATA
771
+ // (retrievable via R2 execute) — a first export can never deliver, since
772
+ // an ATA cannot exist before its mint does.
773
+ let deliveryUserData = '0x';
774
+ let deliveryNote = 'funds-only → CEA ATA';
775
+ if (wrapper.deployed) {
662
776
  try {
663
- let result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, effectiveGasLimit, targetChain);
664
- result = yield (0, svm_rent_1.ensureSvmFinalizeGasBudgetQuote)({
665
- ctx,
666
- ueaAddress,
667
- targetChain,
668
- prc20Token,
669
- quote: result,
670
- splMintBase58,
671
- burnAmount,
672
- pathTag: 'executeUoaToCeaSvm',
673
- });
674
- gasFee = result.gasFee;
675
- protocolFeeSvm = result.protocolFee;
676
- gasToken = result.gasToken;
677
- universalCoreAddress = result.universalCoreAddress;
678
- // When user omits gasLimit (sent as 0), the contract computes fees using
679
- // its internal baseGasLimitByChainNamespace. But the relay reads the
680
- // stored gasLimit from the outbound record. Store the resolved value from
681
- // UniversalCore so the relayer has a non-zero SVM budget.
682
- effectiveGasLimit = result.gasLimitUsed;
683
- if (!params.gasLimit) {
684
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — resolved effectiveGasLimit: ${effectiveGasLimit} ` +
685
- `(gasFee=${result.gasFee}, gasPrice=${result.gasPrice})`);
777
+ const { PublicKey, Connection } = await Promise.resolve().then(() => __importStar(require('@solana/web3.js')));
778
+ const { buildPC20SvmDeliveryFields } = await Promise.resolve().then(() => __importStar(require('./pc20/svm')));
779
+ const mint = new PublicKey(wrapper.address);
780
+ const ceaAuthority = (0, svm_rent_2.deriveSvmCeaPda)(ueaAddress);
781
+ const recipientPk = new PublicKey(Buffer.from(targetAddress.slice(2), 'hex'));
782
+ const recipientAta = (0, svm_rent_2.deriveAtaPubkey)(recipientPk, mint);
783
+ const rpc = ctx.rpcUrls[targetChain]?.[0] ?? chain_1.CHAIN_INFO[targetChain].defaultRPC[0];
784
+ // 'confirmed' commitment: an ATA created moments ago must count.
785
+ // Settlement lands ~a minute later, so anything confirmed now is
786
+ // final long before the CPI runs; probing at 'finalized' would
787
+ // misroute fresh ATAs to the CEA fallback.
788
+ const ataInfo = await new Connection(rpc, 'confirmed').getAccountInfo(recipientAta);
789
+ if (ataInfo) {
790
+ const fields = buildPC20SvmDeliveryFields({
791
+ mint,
792
+ ceaAta: (0, svm_rent_2.deriveAtaPubkey)(ceaAuthority, mint),
793
+ recipientAta,
794
+ ceaAuthority,
795
+ // The PC20 gate guarantees funds.amount > 0 for an export;
796
+ // resolveR2Prc20TokenSvm (which defines burnAmount) runs later.
797
+ amount: params.funds.amount,
798
+ decimals: svmPc20Descriptor.decimals,
799
+ });
800
+ deliveryUserData = (0, payload_builders_1.encodeSvmExecutePayload)(fields);
801
+ deliveryNote = `delivering to recipient ATA ${recipientAta.toBase58()}`;
802
+ }
803
+ else {
804
+ deliveryNote = `recipient ATA ${recipientAta.toBase58()} missing → CEA ATA`;
686
805
  }
687
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}`);
688
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_02, targetChain, protocolFeeSvm, gasFee);
689
806
  }
690
807
  catch (err) {
691
- throw new Error(`Failed to query outbound gas fee: ${err}`);
808
+ // Delivery is an optimization, not a requirement — funds in the CEA
809
+ // ATA stay under the sender's control. Never fail the export over it.
810
+ deliveryNote = `ATA probe failed (${err instanceof Error ? err.message : String(err)}) → CEA ATA`;
692
811
  }
693
812
  }
694
- // --- 203: Execution-account resolution (SVM has a trivial CEA — the
695
- // target address IS the CEA, no async lookup needed). Fire the burst
696
- // after 202 to keep the progress-hook stream in spec order. ---
697
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_01, targetChain);
698
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_02, ueaAddress, targetAddress, targetChain, true);
699
- // --- Build outbound request ---
700
- // targetBytes: for execute = program id (resolver output); for withdraw = to.address
701
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress, // revert recipient is the UEA
702
- (_f = params.maxPCForGas) !== null && _f !== void 0 ? _f : BigInt(0));
703
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — outbound request: ${JSON.stringify({
704
- recipient: outboundReq.recipient,
705
- token: outboundReq.token,
706
- amount: outboundReq.amount.toString(),
707
- gasLimit: outboundReq.gasLimit.toString(),
708
- maxPCForGas: outboundReq.maxPCForGas.toString(),
709
- payloadLength: (outboundReq.payload.length - 2) / 2,
710
- revertRecipient: outboundReq.revertRecipient,
711
- }, null, 2)}`);
712
- // --- Pre-fetch UEA status — balance is needed for gas value calculation ---
713
- const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
714
- const signerChainSvm = ctx.universalSigner.account.chain;
715
- const isNativePushEOASvm = (0, helpers_1.isPushChain)(signerChainSvm);
716
- // Skip getCode if accountStatusCache already confirmed deployment
717
- const deployedHintSvm = (_h = (_g = ctx.accountStatusCache) === null || _g === void 0 ? void 0 : _g.uea) === null || _h === void 0 ? void 0 : _h.deployed;
718
- let ueaBalance;
719
- let isUEADeployed;
720
- if (deployedHintSvm || isNativePushEOASvm) {
721
- isUEADeployed = true;
722
- ueaBalance = yield ctx.pushClient.getBalance(ueaAddress);
723
- }
724
- else {
725
- const [ueaCode, balance] = yield Promise.all([
726
- ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
727
- ctx.pushClient.getBalance(ueaAddress),
728
- ]);
729
- isUEADeployed = ueaCode !== undefined;
730
- ueaBalance = balance;
731
- }
732
- const ueaNonce = (!isUEADeployed || isNativePushEOASvm) ? BigInt(0) : yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
733
- // R2 does NOT use Case A/B/C gas abstraction (scoped to R1 + R3 only).
734
- // Size msg.value from the live WPC/gasToken Uniswap V3 pool quote. The
735
- // 2.2x safety buffer is applied inside estimateNativeValueForSwap (was
736
- // split as inner 2x + outer 10% — combined to keep the cap branch
737
- // mathematically correct). swapAndBurnGas refunds the excess as PC via
738
- // refundUnusedGas.
739
- //
740
- // SVM pools (WPC/pSOL) are thinner than EVM pools so the pool-quote can
741
- // easily exceed the UEA's balance. Pre-flight (`runPreflight`) handles
742
- // drain protection: an under-funded quote throws cleanly with
743
- // `InsufficientUEABalanceError` and no cosmos tx is submitted. The
744
- // SVM_NATIVE_VALUE_WARN_THRESHOLD below is telemetry-only; it does NOT
745
- // truncate `nativeValueForGas`. v2 may add a hard cap once telemetry
746
- // justifies a number — until then the original engineer's pool-skew
747
- // warning still applies (capping risks false-positive truncation of
748
- // legitimate quotes).
749
- const SVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for outer-tx gas
750
- let nativeValueForGas = BigInt(0);
751
- if (universalCoreAddress && gasFee > BigInt(0)) {
752
- // 2.2x safety buffer is applied inside estimateNativeValueForSwap so
753
- // the function's balance-cap branch bounds the final value correctly.
754
- nativeValueForGas = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, ueaBalance);
755
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — nativeValueForGas=${nativeValueForGas.toString()} (includes 2.2x buffer)`);
756
- (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R2_SVM');
757
- }
758
- // Pre-flight: warns by default when UEA cannot cover (nativeValueForGas +
759
- // SVM_GAS_RESERVE) or the PRC-20 burn amount; throws only when
760
- // options.enforceGasCheck=true.
761
- // PRC-20 burn-balance pre-check fires whenever `burnAmount > 0`, regardless
762
- // of whether `prc20Token` happens to equal `gasToken`. The gateway's
763
- // `sendUniversalTxOutbound` does `transferFrom(user, gateway, burnAmount)`
764
- // BEFORE the gas swap, so user must pre-hold `burnAmount` of `prc20Token`
765
- // even when it's the same asset as the gas token (verified empirically:
766
- // EOA with 77k units of pSOL reverted on-chain when trying to burn 10M).
767
- let prc20BalanceR2Svm;
768
- if (burnAmount > BigInt(0)) {
769
- prc20BalanceR2Svm = yield ctx.pushClient.readContract({
770
- address: prc20Token,
771
- abi: erc20_evm_1.ERC20_EVM,
772
- functionName: 'balanceOf',
773
- args: [ueaAddress],
774
- });
775
- }
776
- const requiredNativeBalanceR2Svm = getRequiredNativeBalanceForOutbound(nativeValueForGas, SVM_GAS_RESERVE);
777
- const skipFeeLockingR2Svm = shouldSkipFeeLockingForOutbound({
778
- isUEADeployed,
779
- balance: ueaBalance,
780
- nativeValueForGas,
781
- gasReserve: SVM_GAS_RESERVE,
782
- });
783
- (0, preflight_1.runPreflight)({
784
- ctx,
785
- ueaAddress,
786
- ueaBalance,
787
- requiredValue: nativeValueForGas,
788
- gasReserve: SVM_GAS_RESERVE,
789
- pathTag: 'R2_SVM',
790
- burnToken: prc20Token,
791
- burnAmount,
792
- prc20Balance: prc20BalanceR2Svm,
793
- enforceGasCheck: ((_j = params.options) === null || _j === void 0 ? void 0 : _j.enforceGasCheck) === true,
794
- });
795
- // --- Build Push Chain multicalls (approve + sendUniversalTxOutbound) ---
796
- // Reuse the same builder as EVM — this part is identical
797
- const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
798
- prc20Token,
799
- gasToken,
800
- burnAmount,
801
- gasFee,
802
- nativeValueForGas,
803
- gatewayPcAddress,
804
- outboundRequest: outboundReq,
813
+ pc20SvmPayload = (0, export_1.buildPC20ExportPayload)({
814
+ destinationChain: targetChain,
815
+ name: svmPc20Descriptor.name,
816
+ symbol: svmPc20Descriptor.symbol,
817
+ decimals: svmPc20Descriptor.decimals,
818
+ destinationUserData: deliveryUserData,
805
819
  });
806
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — Push Chain multicall has ${pushChainMulticalls.length} operations`);
807
- // Sum native values from multicall entries for proper fee calculation
808
- const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => { var _a; return sum + ((_a = mc.value) !== null && _a !== void 0 ? _a : BigInt(0)); }, BigInt(0));
809
- const executeParams = {
810
- to: ueaAddress,
811
- value: multicallNativeValue, // ensures correct requiredFunds calculation
812
- data: pushChainMulticalls,
813
- _ueaStatus: {
814
- isDeployed: isUEADeployed,
815
- nonce: ueaNonce,
816
- balance: ueaBalance,
817
- },
818
- _skipFeeLocking: skipFeeLockingR2Svm,
819
- _requiredFundsOverride: requiredNativeBalanceR2Svm,
820
- };
821
- // Signature request — user is prompted to sign the universal payload.
822
- // Emit spec-ordered burst after executeFn returns (sign → verify → broadcast).
823
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_01);
824
- let response;
820
+ // Outbound recipient: the requested Solana address (0x-hex 32B, already
821
+ // normalized by the route detector). The validator parses it as a pubkey
822
+ // for the id=5 TSS message.
823
+ pc20TargetBytes = targetAddress;
824
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — PC20 export: wrapper mint ${wrapper.address} ` +
825
+ `(${wrapper.deployed ? 'exists' : 'first export'}), ${deliveryNote}`);
826
+ }
827
+ // --- Determine PRC-20 token and burn amount (chain-local, pre-203) ---
828
+ const { prc20Token, burnAmount } = resolveR2Prc20TokenSvm(params, targetChain, ctx.pushNetwork, hasSvmExecute);
829
+ if (params.value && params.value > BigInt(0)) {
830
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — auto-selected native PRC-20 ${prc20Token} for chain ${targetChain}, amount: ${burnAmount.toString()}`);
831
+ }
832
+ else if (!params.funds?.amount && hasSvmExecute) {
833
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — EXECUTE-only: using PRC-20 ${prc20Token} with zero burn amount`);
834
+ }
835
+ // --- 202: Gas estimation (spec-ordered before 203) ---
836
+ let gasFee = BigInt(0);
837
+ let protocolFeeSvm = BigInt(0);
838
+ let gasToken = selectors_1.ZERO_ADDRESS;
839
+ let universalCoreAddress;
840
+ // Effective gas limit seeded from the user param. When the user omits
841
+ // gasLimit, it'll be derived from the gas quote so the on-chain record
842
+ // carries a non-zero compute budget the relay can use.
843
+ let effectiveGasLimit = params.gasLimit ?? BigInt(0);
844
+ // SVM finalize budget: the Solana gateway requires gas_fee to cover the
845
+ // relayer signature fee, ExecutedSubTx rent, a compute buffer, and
846
+ // conditionally CEA ATA rent for SPL outbounds. The on-chain quote can be
847
+ // below that floor, so we compare the quoted fee in lamports and re-query
848
+ // with a bumped gasLimit when required.
849
+ const svmFundsToken = burnAmount > BigInt(0)
850
+ ? params.funds?.token
851
+ : undefined;
852
+ const splMintBase58 = svmFundsToken?.mechanism === 'approve' ? svmFundsToken.address : undefined;
853
+ if (prc20Token !== selectors_1.ZERO_ADDRESS) {
854
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_01, targetChain);
825
855
  try {
826
- response = yield executeFn(executeParams);
856
+ // PC20 exports quote via getPC20ExportGasAndFees — the only quote that
857
+ // includes first-export overhead (mint + state + ATA rent on Solana).
858
+ let result = isSvmPc20Export
859
+ ? await (0, export_1.queryPC20OutboundGasFee)(ctx, prc20Token, effectiveGasLimit, targetChain)
860
+ : await (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, effectiveGasLimit, targetChain);
861
+ result = await (0, svm_rent_1.ensureSvmFinalizeGasBudgetQuote)({
862
+ ctx,
863
+ ueaAddress,
864
+ targetChain,
865
+ prc20Token,
866
+ quote: result,
867
+ splMintBase58,
868
+ burnAmount,
869
+ pathTag: 'executeUoaToCeaSvm',
870
+ // PC20: id=5 rent floor (CEA ATA + mint/state on first export), and the
871
+ // bump path must re-quote via getPC20ExportGasAndFees — the default
872
+ // requote reverts for a token with no PRC20 config.
873
+ ...(isSvmPc20Export
874
+ ? {
875
+ pc20: { isFirstExport: svmPc20FirstExport },
876
+ requoteFn: (gl) => (0, export_1.queryPC20OutboundGasFee)(ctx, prc20Token, gl, targetChain),
877
+ }
878
+ : {}),
879
+ });
880
+ gasFee = result.gasFee;
881
+ protocolFeeSvm = result.protocolFee;
882
+ gasToken = result.gasToken;
883
+ universalCoreAddress = result.universalCoreAddress;
884
+ // When user omits gasLimit (sent as 0), the contract computes fees using
885
+ // its internal baseGasLimitByChainNamespace. But the relay reads the
886
+ // stored gasLimit from the outbound record. Store the resolved value from
887
+ // UniversalCore so the relayer has a non-zero SVM budget.
888
+ effectiveGasLimit = result.gasLimitUsed;
889
+ if (!params.gasLimit) {
890
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — resolved effectiveGasLimit: ${effectiveGasLimit} ` +
891
+ `(gasFee=${result.gasFee}, gasPrice=${result.gasPrice})`);
892
+ }
893
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}`);
894
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_02, targetChain, protocolFeeSvm, gasFee);
827
895
  }
828
896
  catch (err) {
829
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_04, err instanceof Error ? err.message : String(err));
830
- // Suppress the outer orchestrator catch's 299-02 terminal.
831
- ctx._routeTerminalEmitted = true;
832
- throw err;
897
+ throw new Error(`Failed to query outbound gas fee: ${err}`);
833
898
  }
834
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_02);
835
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_03);
836
- // R2 broadcast marker (SVM)
837
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_207, targetChain);
838
- // Add chain info to response
839
- response.chain = targetChain;
840
- response.chainNamespace = (0, helpers_1.getChainNamespace)(targetChain);
841
- return response;
899
+ }
900
+ // --- 203: Execution-account resolution (SVM has a trivial CEA — the
901
+ // target address IS the CEA, no async lookup needed). Fire the burst
902
+ // after 202 to keep the progress-hook stream in spec order. ---
903
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_01, targetChain);
904
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_02, ueaAddress, targetAddress, targetChain, true);
905
+ // --- Build outbound request ---
906
+ // targetBytes: for execute = program id (resolver output); for withdraw = to.address
907
+ const outboundReq = (0, payload_builders_2.buildOutboundRequest)(pc20TargetBytes, prc20Token, burnAmount, effectiveGasLimit, pc20SvmPayload, ueaAddress, // revert recipient is the UEA
908
+ params.maxPCForGas ?? BigInt(0));
909
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — outbound request: ${JSON.stringify({
910
+ recipient: outboundReq.recipient,
911
+ token: outboundReq.token,
912
+ amount: outboundReq.amount.toString(),
913
+ gasLimit: outboundReq.gasLimit.toString(),
914
+ maxPCForGas: outboundReq.maxPCForGas.toString(),
915
+ payloadLength: (outboundReq.payload.length - 2) / 2,
916
+ revertRecipient: outboundReq.revertRecipient,
917
+ }, null, 2)}`);
918
+ // --- Pre-fetch UEA status — balance is needed for gas value calculation ---
919
+ const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
920
+ const signerChainSvm = ctx.universalSigner.account.chain;
921
+ const isNativePushEOASvm = (0, helpers_1.isPushChain)(signerChainSvm);
922
+ // Skip getCode if accountStatusCache already confirmed deployment
923
+ const deployedHintSvm = ctx.accountStatusCache?.uea?.deployed;
924
+ let ueaBalance;
925
+ let isUEADeployed;
926
+ if (deployedHintSvm || isNativePushEOASvm) {
927
+ isUEADeployed = true;
928
+ ueaBalance = await ctx.pushClient.getBalance(ueaAddress);
929
+ }
930
+ else {
931
+ const [ueaCode, balance] = await Promise.all([
932
+ ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
933
+ ctx.pushClient.getBalance(ueaAddress),
934
+ ]);
935
+ isUEADeployed = ueaCode !== undefined;
936
+ ueaBalance = balance;
937
+ }
938
+ const ueaNonce = (!isUEADeployed || isNativePushEOASvm) ? BigInt(0) : await (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
939
+ // R2 does NOT use Case A/B/C gas abstraction (scoped to R1 + R3 only).
940
+ // Size msg.value from the live WPC/gasToken Uniswap V3 pool quote. The
941
+ // 2.2x safety buffer is applied inside estimateNativeValueForSwap (was
942
+ // split as inner 2x + outer 10% — combined to keep the cap branch
943
+ // mathematically correct). swapAndBurnGas refunds the excess as PC via
944
+ // refundUnusedGas.
945
+ //
946
+ // SVM pools (WPC/pSOL) are thinner than EVM pools so the pool-quote can
947
+ // easily exceed the UEA's balance. Pre-flight (`runPreflight`) handles
948
+ // drain protection: an under-funded quote throws cleanly with
949
+ // `InsufficientUEABalanceError` and no cosmos tx is submitted. The
950
+ // SVM_NATIVE_VALUE_WARN_THRESHOLD below is telemetry-only; it does NOT
951
+ // truncate `nativeValueForGas`. v2 may add a hard cap once telemetry
952
+ // justifies a number — until then the original engineer's pool-skew
953
+ // warning still applies (capping risks false-positive truncation of
954
+ // legitimate quotes).
955
+ const SVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for outer-tx gas
956
+ let nativeValueForGas = BigInt(0);
957
+ if (universalCoreAddress && gasFee > BigInt(0)) {
958
+ // 2.2x safety buffer is applied inside estimateNativeValueForSwap so
959
+ // the function's balance-cap branch bounds the final value correctly.
960
+ nativeValueForGas = await (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, ueaBalance);
961
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — nativeValueForGas=${nativeValueForGas.toString()} (includes 2.2x buffer)`);
962
+ (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R2_SVM');
963
+ }
964
+ // Pre-flight: warns by default when UEA cannot cover (nativeValueForGas +
965
+ // SVM_GAS_RESERVE) or the PRC-20 burn amount; throws only when
966
+ // options.enforceGasCheck=true.
967
+ // PRC-20 burn-balance pre-check fires whenever `burnAmount > 0`, regardless
968
+ // of whether `prc20Token` happens to equal `gasToken`. The gateway's
969
+ // `sendUniversalTxOutbound` does `transferFrom(user, gateway, burnAmount)`
970
+ // BEFORE the gas swap, so user must pre-hold `burnAmount` of `prc20Token`
971
+ // even when it's the same asset as the gas token (verified empirically:
972
+ // EOA with 77k units of pSOL reverted on-chain when trying to burn 10M).
973
+ let prc20BalanceR2Svm;
974
+ if (burnAmount > BigInt(0)) {
975
+ prc20BalanceR2Svm = await ctx.pushClient.readContract({
976
+ address: prc20Token,
977
+ abi: erc20_evm_1.ERC20_EVM,
978
+ functionName: 'balanceOf',
979
+ args: [ueaAddress],
980
+ });
981
+ }
982
+ const requiredNativeBalanceR2Svm = getRequiredNativeBalanceForOutbound(nativeValueForGas, SVM_GAS_RESERVE);
983
+ const skipFeeLockingR2Svm = shouldSkipFeeLockingForOutbound({
984
+ isUEADeployed,
985
+ balance: ueaBalance,
986
+ nativeValueForGas,
987
+ gasReserve: SVM_GAS_RESERVE,
988
+ });
989
+ (0, preflight_1.runPreflight)({
990
+ ctx,
991
+ ueaAddress,
992
+ ueaBalance,
993
+ requiredValue: nativeValueForGas,
994
+ gasReserve: SVM_GAS_RESERVE,
995
+ pathTag: 'R2_SVM',
996
+ burnToken: prc20Token,
997
+ burnAmount,
998
+ prc20Balance: prc20BalanceR2Svm,
999
+ enforceGasCheck: params.options?.enforceGasCheck === true,
842
1000
  });
1001
+ // --- Build Push Chain multicalls (approve + sendUniversalTxOutbound) ---
1002
+ // Reuse the same builder as EVM — this part is identical
1003
+ const pushChainMulticalls = (0, payload_builders_2.buildOutboundApprovalAndCall)({
1004
+ prc20Token,
1005
+ gasToken,
1006
+ burnAmount,
1007
+ gasFee,
1008
+ nativeValueForGas,
1009
+ gatewayPcAddress,
1010
+ outboundRequest: outboundReq,
1011
+ });
1012
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — Push Chain multicall has ${pushChainMulticalls.length} operations`);
1013
+ // Sum native values from multicall entries for proper fee calculation
1014
+ const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => sum + (mc.value ?? BigInt(0)), BigInt(0));
1015
+ const executeParams = {
1016
+ to: ueaAddress,
1017
+ value: multicallNativeValue, // ensures correct requiredFunds calculation
1018
+ data: pushChainMulticalls,
1019
+ _ueaStatus: {
1020
+ isDeployed: isUEADeployed,
1021
+ nonce: ueaNonce,
1022
+ balance: ueaBalance,
1023
+ },
1024
+ _skipFeeLocking: skipFeeLockingR2Svm,
1025
+ _requiredFundsOverride: requiredNativeBalanceR2Svm,
1026
+ };
1027
+ // Signature request — user is prompted to sign the universal payload.
1028
+ // Emit spec-ordered burst after executeFn returns (sign → verify → broadcast).
1029
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_01);
1030
+ let response;
1031
+ try {
1032
+ response = await executeFn(executeParams);
1033
+ }
1034
+ catch (err) {
1035
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_04, err instanceof Error ? err.message : String(err));
1036
+ // Suppress the outer orchestrator catch's 299-02 terminal.
1037
+ ctx._routeTerminalEmitted = true;
1038
+ throw err;
1039
+ }
1040
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_02);
1041
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_03);
1042
+ // R2 broadcast marker (SVM)
1043
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_207, targetChain);
1044
+ // Add chain info to response
1045
+ response.chain = targetChain;
1046
+ response.chainNamespace = (0, helpers_1.getChainNamespace)(targetChain);
1047
+ return response;
843
1048
  }
844
1049
  // ---------------------------------------------------------------------------
845
1050
  // executeCeaToPush (Route 3 — EVM)
@@ -855,319 +1060,381 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
855
1060
  * 2. Execute via Route 2 (UOA -> CEA) with PAYLOAD-only (CEA uses its own funds)
856
1061
  * 3. CEA executes multicall, Gateway locks funds, relayer mints PRC-20 on Push Chain
857
1062
  */
858
- function executeCeaToPush(ctx, params, executeFn) {
859
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
860
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
861
- // 1. Validate and extract source chain
862
- if (!((_a = params.from) === null || _a === void 0 ? void 0 : _a.chain)) {
863
- throw new Error('Route 3 (CEA -> Push) requires from.chain to specify the source CEA chain');
864
- }
865
- const sourceChain = params.from.chain;
866
- // SVM chains use a fundamentally different flow (gateway self-call, not CEA multicall)
867
- if ((0, payload_builders_1.isSvmChain)(sourceChain)) {
868
- return executeCeaToPushSvm(ctx, params, sourceChain, executeFn);
869
- }
870
- // 2. Extract destination on Push Chain
871
- // For Route 3, 'to' is a Push Chain address (string), not a ChainTarget
872
- const pushDestination = params.to;
873
- if (typeof params.to !== 'string') {
874
- throw new Error('Route 3 (CEA -> Push): to must be a Push Chain address (string), not a ChainTarget');
875
- }
876
- // 3. Get UEA address (will be recipient on Push Chain from CEA's perspective)
877
- // 4. Get CEA address on source chain
878
- // Both happen before the 301 hook fires — wrap them so a pre-sign RPC
879
- // failure produces a descriptive message in the subsequent 399-02 body
880
- // instead of a bare "failed to fetch" surface.
881
- let ueaAddress;
882
- let ceaAddress;
883
- let ceaDeployed;
884
- try {
885
- ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
886
- const ceaResult = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_b = ctx.rpcUrls[sourceChain]) === null || _b === void 0 ? void 0 : _b[0]);
887
- ceaAddress = ceaResult.cea;
888
- ceaDeployed = ceaResult.isDeployed;
889
- }
890
- catch (err) {
891
- throw new Error(`Route 3 setup failed: could not resolve CEA on ${sourceChain}: ${err instanceof Error ? err.message : String(err)}`);
892
- }
893
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_301, sourceChain, ceaAddress);
894
- (0, context_1.printLog)(ctx, `executeCeaToPush — sourceChain: ${sourceChain}, CEA: ${ceaAddress}, deployed: ${ceaDeployed}`);
895
- // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
896
- // if CEA doesn't exist yet. No SDK-side blocking needed.
897
- // 6. Build multicall for CEA to execute on source chain (self-calls via sendUniversalTxToUEA)
898
- const ceaMulticalls = [];
899
- // Determine token and amount for sendUniversalTxToUEA
900
- let tokenAddress = selectors_1.ZERO_ADDRESS;
901
- let amount = BigInt(0);
902
- if ((_c = params.funds) === null || _c === void 0 ? void 0 : _c.amount) {
903
- // ERC20 token transfer from CEA
904
- const token = params.funds.token;
905
- if (token) {
906
- if (token.mechanism === 'native') {
907
- // Native token (e.g., BNB on BSC)
908
- tokenAddress = selectors_1.ZERO_ADDRESS;
909
- amount = params.funds.amount;
910
- }
911
- else {
912
- // ERC20 token. CEA handles gateway approval internally around sendUniversalTxToUEA.
913
- tokenAddress = token.address;
914
- amount = params.funds.amount;
915
- }
916
- }
917
- }
918
- else if (params.value && params.value > BigInt(0)) {
919
- // Native value transfer (e.g., BNB, ETH)
920
- tokenAddress = selectors_1.ZERO_ADDRESS;
921
- amount = params.value;
922
- }
923
- // bridgeAmount = only the burn amount (what the Vault will actually deposit to CEA).
924
- // Previously this included ceaExistingBalance (CEA's pre-existing balance on the
925
- // external chain), but that approach is racy: the balance can change between the SDK
926
- // query and relay execution, causing sendUniversalTxToUEA to revert with
927
- // InsufficientBalance. Pre-existing CEA funds remain parked and can be swept separately.
928
- const bridgeAmount = amount;
929
- // The post-audit CEA allows amount=0 for payload-only sendUniversalTxToUEA.
930
- // Pre-fetch UEA nonce — needed for the inbound UniversalPayload struct
931
- // Skip getCode if accountStatusCache already confirmed deployment
932
- let isUEADeployed;
933
- let ueaNonce;
934
- const deployedHintR3 = (_e = (_d = ctx.accountStatusCache) === null || _d === void 0 ? void 0 : _d.uea) === null || _e === void 0 ? void 0 : _e.deployed;
935
- if (deployedHintR3) {
936
- isUEADeployed = true;
937
- ueaNonce = yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
938
- }
939
- else {
940
- const ueaCode = yield ctx.pushClient.publicClient.getCode({ address: ueaAddress });
941
- isUEADeployed = ueaCode !== undefined;
942
- ueaNonce = isUEADeployed ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress) : BigInt(0);
943
- }
944
- // Build payload for Push Chain execution (if any)
945
- // This is what happens AFTER funds arrive on Push Chain.
946
- // The relay expects a full UniversalPayload struct (to, value, data, gasLimit, ...),
947
- // where `data` contains the multicall payload (with UEA_MULTICALL_SELECTOR prefix).
948
- let pushPayload = '0x';
949
- if ((0, data_utils_1.hasExecutablePayloadData)(params.data) ||
950
- (((_f = params.funds) === null || _f === void 0 ? void 0 : _f.amount) && ((_g = params.value) !== null && _g !== void 0 ? _g : BigInt(0)) > BigInt(0))) {
951
- const multicallData = (0, payload_builders_1.buildExecuteMulticall)({
952
- execute: {
953
- to: pushDestination,
954
- value: params.value,
955
- data: params.data,
956
- },
957
- ueaAddress,
958
- allowSelfValueCall: true,
959
- });
960
- const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, pushDestination, multicallData);
961
- // Use ueaNonce + 1: the outbound tx itself consumes one nonce via execute(),
962
- // so the inbound will arrive when the UEA nonce is already incremented.
963
- pushPayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: ueaNonce + BigInt(1) });
964
- }
965
- // Build sendUniversalTxToUEA self-call on CEA. CEA self-calls must always
966
- // use value=0; native sends spend from the CEA's existing balance.
967
- // CEA internally calls gateway.sendUniversalTxFromCEA(...)
968
- // Uses bridgeAmount (= burn amount deposited by Vault)
969
- const sendUniversalTxCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, // to: CEA address (self-call)
970
- tokenAddress, // token: address(0) for native, ERC20 address otherwise
971
- bridgeAmount, // amount: burn amount only (Vault-deposited)
972
- pushPayload, // payload: Push Chain execution payload
973
- ueaAddress // revertRecipient: UEA on Push Chain (receives refund if inbound fails)
974
- );
975
- ceaMulticalls.push(sendUniversalTxCall);
976
- // 7. Encode CEA multicalls into outbound payload
977
- // CEA will self-execute this multicall (to=CEA, value=0)
978
- const ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
979
- (0, context_1.printLog)(ctx, `executeCeaToPush — CEA payload (first 100 chars): ${ceaPayload.slice(0, 100)}...`);
980
- // 8. Determine PRC-20 token for the outbound relay.
981
- // Route 3: ALWAYS use native PRC-20 for chain namespace lookup + gas fees.
982
- // The CEA uses its OWN pre-existing balance on the external chain to bridge
983
- // tokens back — no PRC-20 burn is needed on Push Chain.
984
- // burnAmount = 0 makes this a payload-only outbound relay.
985
- //
986
- // Previously this burned pUSDT/pToken equal to the bridge amount, which failed
987
- // because the UEA never holds pUSDT — the whole point of Route 3 is to get
988
- // tokens FROM the CEA back to Push Chain, not to round-trip them.
989
- const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
990
- const burnAmount = BigInt(0);
991
- const outboundGasLimit = (_h = params.gasLimit) !== null && _h !== void 0 ? _h : selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
992
- (0, context_1.printLog)(ctx, `executeCeaToPush — prc20Token: ${prc20Token}, burnAmount: ${burnAmount.toString()}`);
993
- // 9. Build outbound request (same structure as Route 2)
994
- // recipient = CEA address (for self-execution), value = 0 in payload
995
- let outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, // recipient: CEA address (to=CEA for self-execution)
996
- prc20Token, // token: native PRC-20 for source chain (for namespace lookup)
997
- burnAmount, // amount: 0 (payload-only, CEA uses its own balance)
998
- outboundGasLimit, ceaPayload, // payload: ABI-encoded CEA multicall
999
- ueaAddress, // revertRecipient: UEA
1000
- (_j = params.maxPCForGas) !== null && _j !== void 0 ? _j : BigInt(0));
1001
- (0, context_1.printLog)(ctx, `executeCeaToPush — outbound request: ${JSON.stringify({
1002
- recipient: outboundReq.recipient,
1003
- token: outboundReq.token,
1004
- amount: outboundReq.amount.toString(),
1005
- gasLimit: outboundReq.gasLimit.toString(),
1006
- maxPCForGas: outboundReq.maxPCForGas.toString(),
1007
- payloadLength: outboundReq.payload.length,
1008
- revertRecipient: outboundReq.revertRecipient,
1009
- }, null, 2)}`);
1010
- // 10. Fetch UEA balance — needed for gas value calculation
1011
- // (UEA code + nonce already fetched above for the inbound UniversalPayload)
1012
- const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
1013
- const ueaBalance = yield ctx.pushClient.getBalance(ueaAddress);
1014
- // 11. Query gas fees from UniversalCore
1015
- let gasFee = BigInt(0);
1016
- let protocolFeeR3 = BigInt(0);
1017
- let nativeValueForGas = BigInt(0);
1018
- let gasToken = selectors_1.ZERO_ADDRESS;
1019
- let sizingDecisionR3;
1020
- if (prc20Token !== selectors_1.ZERO_ADDRESS) {
1021
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01, sourceChain);
1022
- try {
1023
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundGasLimit, sourceChain);
1024
- gasToken = result.gasToken;
1025
- gasFee = result.gasFee;
1026
- protocolFeeR3 = result.protocolFee;
1027
- nativeValueForGas = result.nativeValueForGas;
1028
- sizingDecisionR3 = result.sizing;
1029
- outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, outboundGasLimit, ceaPayload, ueaAddress, (_k = params.maxPCForGas) !== null && _k !== void 0 ? _k : BigInt(0));
1030
- (0, context_1.printLog)(ctx, `executeCeaToPush — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}, sizing=${(_m = (_l = result.sizing) === null || _l === void 0 ? void 0 : _l.category) !== null && _m !== void 0 ? _m : 'legacy'}`);
1031
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02, sourceChain, protocolFeeR3, gasFee);
1032
- }
1033
- catch (err) {
1034
- throw new Error(`Failed to query outbound gas fee for Route 3: ${err}`);
1035
- }
1036
- }
1037
- // Account-resolution checkpoint fires before the gas-sizing sub-hook so
1038
- // consumers see the full account-resolution phase (303-xx) complete before
1039
- // the sizing decision is surfaced.
1040
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_01, sourceChain);
1041
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_02, ueaAddress, ceaAddress, sourceChain);
1042
- if (sizingDecisionR3) {
1043
- fireSizingHook(ctx, 'R3', sourceChain, sizingDecisionR3);
1044
- }
1045
- // R3 Case C: unlike R2, R3 has no destination funds-delivery semantic
1046
- // (CEA self-executes and bridges funds BACK to Push Chain). When the
1047
- // sizer returns Case C, we simply top up msg.value with the overflow so
1048
- // `swapAndBurnGas` can afford the full gas cost on destination. No
1049
- // bridge-swap entries, no fold-in — contract refunds any excess.
1050
- if ((sizingDecisionR3 === null || sizingDecisionR3 === void 0 ? void 0 : sizingDecisionR3.category) === 'C' &&
1051
- sizingDecisionR3.overflowNativePc > BigInt(0)) {
1052
- const bumped = nativeValueForGas + sizingDecisionR3.overflowNativePc;
1053
- (0, context_1.printLog)(ctx, `executeCeaToPush — Case C: bumping nativeValueForGas from ${nativeValueForGas} to ${bumped} (overflow=${sizingDecisionR3.overflowNativePc})`);
1054
- nativeValueForGas = bumped;
1055
- }
1056
- // Adjust nativeValueForGas using UEA balance (contract refunds excess)
1057
- // Reuse ueaBalance from line 978 — no tx sent between, balance is stable
1058
- const currentBalance = ueaBalance;
1059
- // Cosmos-EVM tx overhead costs ~1 PC per operation; 3 PC covers approve(s) + buffer.
1060
- const OUTBOUND_GAS_RESERVE_R3 = BigInt(3e18);
1061
- const ROUTE3_MINIMUM_DEPOSIT_USD = utils_1.Utils.helpers.parseUnits('10', 8); // $10
1062
- // For fresh wallets: predict post-fee-locking UPC balance using the same
1063
- // Uniswap V3 quoter the chain uses (pETH → WPC swap), matching Route 2 logic.
1064
- let effectiveBalance = currentBalance;
1065
- if (!isUEADeployed && effectiveBalance <= OUTBOUND_GAS_RESERVE_R3) {
1066
- const signerChain = ctx.universalSigner.account.chain;
1067
- const ethPrice = yield new price_fetch_1.PriceFetch(ctx.rpcUrls).getPrice(signerChain);
1068
- const nativeAmountETH = ethPrice > BigInt(0)
1069
- ? (ROUTE3_MINIMUM_DEPOSIT_USD * BigInt(1e18) + (ethPrice - BigInt(1))) / ethPrice + BigInt(1)
1070
- : BigInt(0);
1071
- if (nativeAmountETH > BigInt(0)) {
1072
- const originPrc20 = (0, helpers_1.getNativePRC20ForChain)(signerChain, ctx.pushNetwork);
1073
- const predictedUPC = yield (0, gas_calculator_1.estimateDepositFromLockedNative)(ctx, nativeAmountETH, originPrc20);
1074
- if (predictedUPC > BigInt(0)) {
1075
- effectiveBalance = (predictedUPC * BigInt(90)) / BigInt(100);
1076
- (0, context_1.printLog)(ctx, `executeCeaToPush — fresh wallet: Uniswap quote predicts ${predictedUPC.toString()} UPC ` +
1077
- `from ${nativeAmountETH.toString()} wei pETH, using ${effectiveBalance.toString()} (90%)`);
1078
- }
1063
+ async function executeCeaToPush(ctx, params, executeFn) {
1064
+ // 1. Validate and extract source chain
1065
+ if (!params.from?.chain) {
1066
+ throw new Error('Route 3 (CEA -> Push) requires from.chain to specify the source CEA chain');
1067
+ }
1068
+ const sourceChain = params.from.chain;
1069
+ // SVM chains use a fundamentally different flow (gateway self-call, not CEA multicall)
1070
+ if ((0, payload_builders_2.isSvmChain)(sourceChain)) {
1071
+ return executeCeaToPushSvm(ctx, params, sourceChain, executeFn);
1072
+ }
1073
+ // 2. Extract destination on Push Chain
1074
+ // For Route 3, 'to' is a Push Chain address (string), not a ChainTarget
1075
+ const pushDestination = params.to;
1076
+ if (typeof params.to !== 'string') {
1077
+ throw new Error('Route 3 (CEA -> Push): to must be a Push Chain address (string), not a ChainTarget');
1078
+ }
1079
+ // 3. Get UEA address (will be recipient on Push Chain from CEA's perspective)
1080
+ // 4. Get CEA address on source chain
1081
+ // Both happen before the 301 hook fires — wrap them so a pre-sign RPC
1082
+ // failure produces a descriptive message in the subsequent 399-02 body
1083
+ // instead of a bare "failed to fetch" surface.
1084
+ let ueaAddress;
1085
+ let ceaAddress;
1086
+ let ceaDeployed;
1087
+ try {
1088
+ ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
1089
+ const ceaResult = await (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, ctx.rpcUrls[sourceChain]?.[0]);
1090
+ ceaAddress = ceaResult.cea;
1091
+ ceaDeployed = ceaResult.isDeployed;
1092
+ }
1093
+ catch (err) {
1094
+ throw new Error(`Route 3 setup failed: could not resolve CEA on ${sourceChain}: ${err instanceof Error ? err.message : String(err)}`);
1095
+ }
1096
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_301, sourceChain, ceaAddress);
1097
+ (0, context_1.printLog)(ctx, `executeCeaToPush — sourceChain: ${sourceChain}, CEA: ${ceaAddress}, deployed: ${ceaDeployed}`);
1098
+ // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
1099
+ // if CEA doesn't exist yet. No SDK-side blocking needed.
1100
+ // 6. Build multicall for CEA to execute on source chain (self-calls via sendUniversalTxToUEA)
1101
+ const ceaMulticalls = [];
1102
+ // Determine token and amount for sendUniversalTxToUEA
1103
+ let tokenAddress = selectors_1.ZERO_ADDRESS;
1104
+ let amount = BigInt(0);
1105
+ if (params.funds?.amount) {
1106
+ // ERC20 token transfer from CEA
1107
+ const token = params.funds.token;
1108
+ if (token) {
1109
+ if (token.mechanism === 'native') {
1110
+ // Native token (e.g., BNB on BSC)
1111
+ tokenAddress = selectors_1.ZERO_ADDRESS;
1112
+ amount = params.funds.amount;
1079
1113
  }
1080
- }
1081
- // Balance-aware *upward* allocation. The previous behaviour silently
1082
- // clamped to a 200 PC target whenever balance had headroom — which was
1083
- // a downward clamp when the calibrated value already exceeded 200 PC,
1084
- // masking under-funded swaps with opaque ExecutionFailed reverts. The
1085
- // new logic preserves the "consume extra to reduce STF risk" intent for
1086
- // small calibrated values while never reducing a calibrated value that
1087
- // happened to land above 200 PC. Down-clamp + zero-balance throws live
1088
- // in `runPreflight` below.
1089
- const EVM_NATIVE_VALUE_TARGET_R3 = BigInt(200e18); // 200 UPC target floor
1090
- if (effectiveBalance > EVM_NATIVE_VALUE_TARGET_R3 + OUTBOUND_GAS_RESERVE_R3) {
1091
- // Balance can cover the target. Bump nativeValueForGas UP to 200 PC,
1092
- // but never reduce a calibrated value already above 200.
1093
- const newValue = nativeValueForGas > EVM_NATIVE_VALUE_TARGET_R3
1094
- ? nativeValueForGas
1095
- : EVM_NATIVE_VALUE_TARGET_R3;
1096
- if (newValue !== nativeValueForGas) {
1097
- (0, context_1.printLog)(ctx, `executeCeaToPush — adjusting nativeValueForGas UP from ${nativeValueForGas.toString()} to ${newValue.toString()} (EVM_NATIVE_VALUE_TARGET_R3 floor)`);
1098
- nativeValueForGas = newValue;
1114
+ else {
1115
+ // ERC20 token. CEA handles gateway approval internally around sendUniversalTxToUEA.
1116
+ tokenAddress = token.address;
1117
+ amount = params.funds.amount;
1099
1118
  }
1100
1119
  }
1101
- // Pre-flight (R3 EVM): no PRC-20 burn check — `burnAmount = 0` makes this
1102
- // payload-only (UEA never holds the source-chain PRC-20; see plan §9 #4).
1103
- // Native UPC check still applies for the gas swap. It warns by default and
1104
- // throws only when options.enforceGasCheck=true.
1105
- const requiredNativeBalanceR3Evm = getRequiredNativeBalanceForOutbound(nativeValueForGas, OUTBOUND_GAS_RESERVE_R3);
1106
- const skipFeeLockingR3Evm = shouldSkipFeeLockingForOutbound({
1107
- isUEADeployed,
1108
- balance: effectiveBalance,
1109
- nativeValueForGas,
1110
- gasReserve: OUTBOUND_GAS_RESERVE_R3,
1120
+ }
1121
+ else if (params.value && params.value > BigInt(0)) {
1122
+ // Native value transfer (e.g., BNB, ETH)
1123
+ tokenAddress = selectors_1.ZERO_ADDRESS;
1124
+ amount = params.value;
1125
+ }
1126
+ // bridgeAmount = only the burn amount (what the Vault will actually deposit to CEA).
1127
+ // Previously this included ceaExistingBalance (CEA's pre-existing balance on the
1128
+ // external chain), but that approach is racy: the balance can change between the SDK
1129
+ // query and relay execution, causing sendUniversalTxToUEA to revert with
1130
+ // InsufficientBalance. Pre-existing CEA funds remain parked and can be swept separately.
1131
+ const bridgeAmount = amount;
1132
+ // The post-audit CEA allows amount=0 for payload-only sendUniversalTxToUEA.
1133
+ // Pre-fetch UEA nonce — needed for the inbound UniversalPayload struct
1134
+ // Skip getCode if accountStatusCache already confirmed deployment
1135
+ let isUEADeployed;
1136
+ let ueaNonce;
1137
+ const isNativePushEOAR3 = (0, helpers_1.isPushChain)(ctx.universalSigner.account.chain);
1138
+ const deployedHintR3 = ctx.accountStatusCache?.uea?.deployed;
1139
+ if (isNativePushEOAR3) {
1140
+ // Native Push accounts execute directly as EOAs. EIP-7702 delegation code
1141
+ // is not a UEA proxy and its executor does not expose the UEA nonce ABI.
1142
+ // Treat the execution account as ready while using no UEA nonce.
1143
+ isUEADeployed = true;
1144
+ ueaNonce = BigInt(0);
1145
+ }
1146
+ else if (deployedHintR3) {
1147
+ isUEADeployed = true;
1148
+ ueaNonce = await (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
1149
+ }
1150
+ else {
1151
+ const ueaCode = await ctx.pushClient.publicClient.getCode({
1152
+ address: ueaAddress,
1111
1153
  });
1112
- (0, preflight_1.runPreflight)({
1113
- ctx,
1154
+ isUEADeployed = ueaCode !== undefined;
1155
+ ueaNonce = isUEADeployed ? await (0, uea_manager_1.getUEANonce)(ctx, ueaAddress) : BigInt(0);
1156
+ }
1157
+ // Build payload for Push Chain execution (if any)
1158
+ // This is what happens AFTER funds arrive on Push Chain.
1159
+ // The relay expects a full UniversalPayload struct (to, value, data, gasLimit, ...),
1160
+ // where `data` contains the multicall payload (with UEA_MULTICALL_SELECTOR prefix).
1161
+ let pushPayload = '0x';
1162
+ // PC20: the chain unlocks the Push-native token to the UEA (unlockPC20 →
1163
+ // in.Recipient, which the CEA sets to its bound UEA), so a funds-only R3
1164
+ // whose destination is NOT the UEA needs a Push-side transfer to forward
1165
+ // it. PRC20 R3 has the same land-on-UEA semantics; PC20 just makes the
1166
+ // forwarding explicit here because the resolved descriptor lets us.
1167
+ const r3Pc20 = params._pc20;
1168
+ // ALWAYS build the forward for PC20 funds — even when the destination is the
1169
+ // UEA itself (an explicit self-transfer). Two reasons:
1170
+ // 1. semantics: the transfer states where funds go instead of relying on
1171
+ // the unlock's implicit landing spot;
1172
+ // 2. survival: a payload-less PC20 burn produces a selector-only
1173
+ // raw_payload, which the chain classifies FUNDS_AND_PAYLOAD and then
1174
+ // hard-fails decoding ("raw_payload decoded to nil" —
1175
+ // StripSelector yields "0x", the != "" guard admits it). Until that is
1176
+ // fixed chain-side, an empty R3 PC20 payload bricks the inbound with
1177
+ // the wrapper already burned.
1178
+ const r3Pc20NeedsForward = r3Pc20?.direction === 'import' && Boolean(params.funds?.amount);
1179
+ if ((0, data_utils_1.hasExecutablePayloadData)(params.data) ||
1180
+ (params.funds?.amount && (params.value ?? BigInt(0)) > BigInt(0)) ||
1181
+ r3Pc20NeedsForward) {
1182
+ const multicallData = (0, payload_builders_2.buildExecuteMulticall)({
1183
+ execute: {
1184
+ to: pushDestination,
1185
+ value: params.value,
1186
+ data: params.data,
1187
+ // Funds + descriptor let the builder emit the Push-native PC20
1188
+ // transfer (its _pc20 branch); omitted for non-PC20 to keep the
1189
+ // existing PRC20 payload byte-identical.
1190
+ ...(r3Pc20NeedsForward
1191
+ ? { funds: params.funds, _pc20: r3Pc20 }
1192
+ : {}),
1193
+ },
1114
1194
  ueaAddress,
1115
- ueaBalance: effectiveBalance,
1116
- requiredValue: nativeValueForGas,
1117
- gasReserve: OUTBOUND_GAS_RESERVE_R3,
1118
- pathTag: 'R3_EVM',
1119
- enforceGasCheck: ((_o = params.options) === null || _o === void 0 ? void 0 : _o.enforceGasCheck) === true,
1195
+ allowSelfValueCall: true,
1120
1196
  });
1121
- // 12. Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1122
- const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
1123
- prc20Token,
1124
- gasToken,
1125
- burnAmount,
1126
- gasFee,
1127
- nativeValueForGas,
1128
- gatewayPcAddress,
1129
- outboundRequest: outboundReq,
1130
- });
1131
- (0, context_1.printLog)(ctx, `executeCeaToPush — Push Chain multicall has ${pushChainMulticalls.length} operations`);
1132
- // Sum native values from multicall entries for proper fee calculation
1133
- const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => { var _a; return sum + ((_a = mc.value) !== null && _a !== void 0 ? _a : BigInt(0)); }, BigInt(0));
1134
- // 13. Execute through the normal execute() flow
1135
- const executeParams = Object.assign({ to: ueaAddress, value: multicallNativeValue, data: pushChainMulticalls, _ueaStatus: {
1136
- isDeployed: isUEADeployed,
1137
- nonce: ueaNonce,
1138
- balance: ueaBalance,
1139
- }, _skipFeeLocking: skipFeeLockingR3Evm, _requiredFundsOverride: requiredNativeBalanceR3Evm }, (!skipFeeLockingR3Evm ? { _minimumDepositUsd: ROUTE3_MINIMUM_DEPOSIT_USD } : {}));
1140
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_01);
1141
- let response;
1197
+ const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, pushDestination, multicallData);
1198
+ // Use ueaNonce + 1: the outbound tx itself consumes one nonce via execute(),
1199
+ // so the inbound will arrive when the UEA nonce is already incremented.
1200
+ pushPayload = (0, payload_builders_2.buildInboundUniversalPayload)(multicallPayload, { nonce: ueaNonce + BigInt(1) });
1201
+ }
1202
+ (0, gate_1.assertPC20ImportHasPayload)(params, pushPayload !== '0x', `Route 3 EVM ${sourceChain}`);
1203
+ // Build sendUniversalTxToUEA self-call on CEA. CEA self-calls must always
1204
+ // use value=0; native sends spend from the CEA's existing balance.
1205
+ // CEA internally calls gateway.sendUniversalTxFromCEA(...)
1206
+ // Uses bridgeAmount (= burn amount deposited by Vault)
1207
+ const sendUniversalTxCall = (0, payload_builders_2.buildSendUniversalTxToUEA)(ceaAddress, // to: CEA address (self-call)
1208
+ tokenAddress, // token: address(0) for native, ERC20 address otherwise
1209
+ bridgeAmount, // amount: burn amount only (Vault-deposited)
1210
+ pushPayload, // payload: Push Chain execution payload
1211
+ ueaAddress // revertRecipient: UEA on Push Chain (receives refund if inbound fails)
1212
+ );
1213
+ ceaMulticalls.push(sendUniversalTxCall);
1214
+ // 7. Encode CEA multicalls into outbound payload
1215
+ // CEA will self-execute this multicall (to=CEA, value=0)
1216
+ const ceaPayload = (0, payload_builders_2.buildCeaMulticallPayload)(ceaMulticalls);
1217
+ (0, context_1.printLog)(ctx, `executeCeaToPush — CEA payload (first 100 chars): ${ceaPayload.slice(0, 100)}...`);
1218
+ // 8. Determine PRC-20 token for the outbound relay.
1219
+ // Route 3: ALWAYS use native PRC-20 for chain namespace lookup + gas fees.
1220
+ // The CEA uses its OWN pre-existing balance on the external chain to bridge
1221
+ // tokens back — no PRC-20 burn is needed on Push Chain.
1222
+ // burnAmount = 0 makes this a payload-only outbound relay.
1223
+ //
1224
+ // Previously this burned pUSDT/pToken equal to the bridge amount, which failed
1225
+ // because the UEA never holds pUSDT — the whole point of Route 3 is to get
1226
+ // tokens FROM the CEA back to Push Chain, not to round-trip them.
1227
+ const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
1228
+ const burnAmount = BigInt(0);
1229
+ const outboundGasLimit = params.gasLimit ?? selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
1230
+ (0, context_1.printLog)(ctx, `executeCeaToPush — prc20Token: ${prc20Token}, burnAmount: ${burnAmount.toString()}`);
1231
+ // 9. Build outbound request (same structure as Route 2)
1232
+ // recipient = CEA address (for self-execution), value = 0 in payload
1233
+ let outboundReq = (0, payload_builders_2.buildOutboundRequest)(ceaAddress, // recipient: CEA address (to=CEA for self-execution)
1234
+ prc20Token, // token: native PRC-20 for source chain (for namespace lookup)
1235
+ burnAmount, // amount: 0 (payload-only, CEA uses its own balance)
1236
+ outboundGasLimit, ceaPayload, // payload: ABI-encoded CEA multicall
1237
+ ueaAddress, // revertRecipient: UEA
1238
+ params.maxPCForGas ?? BigInt(0));
1239
+ (0, context_1.printLog)(ctx, `executeCeaToPush — outbound request: ${JSON.stringify({
1240
+ recipient: outboundReq.recipient,
1241
+ token: outboundReq.token,
1242
+ amount: outboundReq.amount.toString(),
1243
+ gasLimit: outboundReq.gasLimit.toString(),
1244
+ maxPCForGas: outboundReq.maxPCForGas.toString(),
1245
+ payloadLength: outboundReq.payload.length,
1246
+ revertRecipient: outboundReq.revertRecipient,
1247
+ }, null, 2)}`);
1248
+ // 10. Fetch UEA balance — needed for gas value calculation
1249
+ // (UEA code + nonce already fetched above for the inbound UniversalPayload)
1250
+ const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
1251
+ const ueaBalance = await ctx.pushClient.getBalance(ueaAddress);
1252
+ // 11. Query gas fees from UniversalCore
1253
+ let gasFee = BigInt(0);
1254
+ let protocolFeeR3 = BigInt(0);
1255
+ let nativeValueForGas = BigInt(0);
1256
+ let gasToken = selectors_1.ZERO_ADDRESS;
1257
+ let universalCoreAddress;
1258
+ let sizingDecisionR3;
1259
+ if (prc20Token !== selectors_1.ZERO_ADDRESS) {
1260
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01, sourceChain);
1142
1261
  try {
1143
- response = yield executeFn(executeParams);
1262
+ const result = await (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundGasLimit, sourceChain);
1263
+ gasToken = result.gasToken;
1264
+ gasFee = result.gasFee;
1265
+ protocolFeeR3 = result.protocolFee;
1266
+ nativeValueForGas = result.nativeValueForGas;
1267
+ universalCoreAddress = result.universalCoreAddress;
1268
+ sizingDecisionR3 = result.sizing;
1269
+ outboundReq = (0, payload_builders_2.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, outboundGasLimit, ceaPayload, ueaAddress, params.maxPCForGas ?? BigInt(0));
1270
+ (0, context_1.printLog)(ctx, `executeCeaToPush — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}, sizing=${result.sizing?.category ?? 'legacy'}`);
1271
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02, sourceChain, protocolFeeR3, gasFee);
1144
1272
  }
1145
1273
  catch (err) {
1146
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_04, err instanceof Error ? err.message : String(err));
1147
- // Suppress the outer orchestrator catch's 399-02 terminal — 304-04
1148
- // already surfaced the signature/broadcast failure to the consumer.
1149
- ctx._routeTerminalEmitted = true;
1150
- throw err;
1274
+ throw new Error(`Failed to query outbound gas fee for Route 3: ${err}`);
1275
+ }
1276
+ }
1277
+ // Account-resolution checkpoint fires before the gas-sizing sub-hook so
1278
+ // consumers see the full account-resolution phase (303-xx) complete before
1279
+ // the sizing decision is surfaced.
1280
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_01, sourceChain);
1281
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_02, ueaAddress, ceaAddress, sourceChain);
1282
+ if (sizingDecisionR3) {
1283
+ fireSizingHook(ctx, 'R3', sourceChain, sizingDecisionR3);
1284
+ }
1285
+ // R3 Case C: unlike R2, R3 has no destination funds-delivery semantic
1286
+ // (CEA self-executes and bridges funds BACK to Push Chain). When the
1287
+ // sizer returns Case C, we simply top up msg.value with the overflow so
1288
+ // `swapAndBurnGas` can afford the full gas cost on destination. No
1289
+ // bridge-swap entries, no fold-in — contract refunds any excess.
1290
+ if (sizingDecisionR3?.category === 'C' &&
1291
+ sizingDecisionR3.overflowNativePc > BigInt(0)) {
1292
+ const bumped = nativeValueForGas + sizingDecisionR3.overflowNativePc;
1293
+ (0, context_1.printLog)(ctx, `executeCeaToPush — Case C: bumping nativeValueForGas from ${nativeValueForGas} to ${bumped} (overflow=${sizingDecisionR3.overflowNativePc})`);
1294
+ nativeValueForGas = bumped;
1295
+ }
1296
+ // The USD sizer can be a few basis points below the executable pool quote
1297
+ // (for example when native Push falls back to the fixed $0.10 PC price).
1298
+ // Because swapAndBurnGas buys an exact gas-token output, even a small
1299
+ // shortfall reverts the entire atomic Push transaction. Floor the category
1300
+ // result with the same live exact-output quote used by Route 2. The quote's
1301
+ // 2.2x buffer is automatically capped to balance - 3 PC when that still
1302
+ // covers the unbuffered swap minimum, and excess is refunded on-chain.
1303
+ if (universalCoreAddress && gasFee > BigInt(0)) {
1304
+ const balanceAvailableForSwap = ueaBalance > protocolFeeR3
1305
+ ? ueaBalance - protocolFeeR3
1306
+ : BigInt(0);
1307
+ const bufferedSwapValue = await (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, balanceAvailableForSwap);
1308
+ const poolFlooredValue = floorR3NativeValueForGas(nativeValueForGas, bufferedSwapValue, protocolFeeR3);
1309
+ if (poolFlooredValue > nativeValueForGas) {
1310
+ (0, context_1.printLog)(ctx, `executeCeaToPush — bumping nativeValueForGas ${nativeValueForGas.toString()} → ${poolFlooredValue.toString()} ` +
1311
+ `(live exact-output pool floor, bufferedSwap=${bufferedSwapValue.toString()}, protocolFee=${protocolFeeR3.toString()})`);
1312
+ nativeValueForGas = poolFlooredValue;
1151
1313
  }
1152
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_02);
1153
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_03);
1154
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_307, sourceChain);
1155
- // Add Route 3 context to response
1156
- response.chain = sourceChain;
1157
- const chainInfo = chain_1.CHAIN_INFO[sourceChain];
1158
- response.chainNamespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
1159
- // R3 inbound round-trip tracking: enable only when funds actually flow
1160
- // back (amount > 0). Payload-only R3 has no child inbound UTX on Push
1161
- // Chain, so polling would hang until timeout — skip the inbound block
1162
- // for amount == 0.
1163
- //
1164
- // Child-utxId correlation uses the universal-tx-detector's deterministic
1165
- // sha256(caip:txHash:logIndex) derivation (see inbound-tracker.ts
1166
- // findChildUtxIdFromExternalTx), with the original cosmos
1167
- // `universal_tx_created.inbound_tx_hash` search kept as a fallback.
1168
- response._expectsInboundRoundTrip = amount > BigInt(0);
1169
- return response;
1314
+ }
1315
+ // Adjust nativeValueForGas using UEA balance (contract refunds excess)
1316
+ // Reuse ueaBalance from line 978 — no tx sent between, balance is stable
1317
+ const currentBalance = ueaBalance;
1318
+ // Cosmos-EVM tx overhead costs ~1 PC per operation; 3 PC covers approve(s) + buffer.
1319
+ const OUTBOUND_GAS_RESERVE_R3 = BigInt(3e18);
1320
+ const ROUTE3_MINIMUM_DEPOSIT_USD = utils_1.Utils.helpers.parseUnits('10', 8); // $10
1321
+ // For fresh wallets: predict post-fee-locking UPC balance using the same
1322
+ // Uniswap V3 quoter the chain uses (pETH → WPC swap), matching Route 2 logic.
1323
+ let effectiveBalance = currentBalance;
1324
+ if (!isUEADeployed && effectiveBalance <= OUTBOUND_GAS_RESERVE_R3) {
1325
+ const signerChain = ctx.universalSigner.account.chain;
1326
+ const ethPrice = await new price_fetch_1.PriceFetch(ctx.rpcUrls).getPrice(signerChain);
1327
+ const nativeAmountETH = ethPrice > BigInt(0)
1328
+ ? (ROUTE3_MINIMUM_DEPOSIT_USD * BigInt(1e18) + (ethPrice - BigInt(1))) / ethPrice + BigInt(1)
1329
+ : BigInt(0);
1330
+ if (nativeAmountETH > BigInt(0)) {
1331
+ const originPrc20 = (0, helpers_1.getNativePRC20ForChain)(signerChain, ctx.pushNetwork);
1332
+ const predictedUPC = await (0, gas_calculator_1.estimateDepositFromLockedNative)(ctx, nativeAmountETH, originPrc20);
1333
+ if (predictedUPC > BigInt(0)) {
1334
+ effectiveBalance = (predictedUPC * BigInt(90)) / BigInt(100);
1335
+ (0, context_1.printLog)(ctx, `executeCeaToPush — fresh wallet: Uniswap quote predicts ${predictedUPC.toString()} UPC ` +
1336
+ `from ${nativeAmountETH.toString()} wei pETH, using ${effectiveBalance.toString()} (90%)`);
1337
+ }
1338
+ }
1339
+ }
1340
+ // Balance-aware *upward* allocation. The previous behaviour silently
1341
+ // clamped to a 200 PC target whenever balance had headroom — which was
1342
+ // a downward clamp when the calibrated value already exceeded 200 PC,
1343
+ // masking under-funded swaps with opaque ExecutionFailed reverts. The
1344
+ // new logic preserves the "consume extra to reduce STF risk" intent for
1345
+ // small calibrated values while never reducing a calibrated value that
1346
+ // happened to land above 200 PC. Down-clamp + zero-balance throws live
1347
+ // in `runPreflight` below.
1348
+ const EVM_NATIVE_VALUE_TARGET_R3 = BigInt(200e18); // 200 UPC target floor
1349
+ if (effectiveBalance > EVM_NATIVE_VALUE_TARGET_R3 + OUTBOUND_GAS_RESERVE_R3) {
1350
+ // Balance can cover the target. Bump nativeValueForGas UP to 200 PC,
1351
+ // but never reduce a calibrated value already above 200.
1352
+ const newValue = nativeValueForGas > EVM_NATIVE_VALUE_TARGET_R3
1353
+ ? nativeValueForGas
1354
+ : EVM_NATIVE_VALUE_TARGET_R3;
1355
+ if (newValue !== nativeValueForGas) {
1356
+ (0, context_1.printLog)(ctx, `executeCeaToPush — adjusting nativeValueForGas UP from ${nativeValueForGas.toString()} to ${newValue.toString()} (EVM_NATIVE_VALUE_TARGET_R3 floor)`);
1357
+ nativeValueForGas = newValue;
1358
+ }
1359
+ }
1360
+ // Pre-flight (R3 EVM): no PRC-20 burn check — `burnAmount = 0` makes this
1361
+ // payload-only (UEA never holds the source-chain PRC-20; see plan §9 #4).
1362
+ // Native UPC check still applies for the gas swap. It warns by default and
1363
+ // throws only when options.enforceGasCheck=true.
1364
+ const requiredNativeBalanceR3Evm = getRequiredNativeBalanceForOutbound(nativeValueForGas, OUTBOUND_GAS_RESERVE_R3);
1365
+ const skipFeeLockingR3Evm = shouldSkipFeeLockingForOutbound({
1366
+ isUEADeployed,
1367
+ balance: effectiveBalance,
1368
+ nativeValueForGas,
1369
+ gasReserve: OUTBOUND_GAS_RESERVE_R3,
1170
1370
  });
1371
+ (0, preflight_1.runPreflight)({
1372
+ ctx,
1373
+ ueaAddress,
1374
+ ueaBalance: effectiveBalance,
1375
+ requiredValue: nativeValueForGas,
1376
+ gasReserve: OUTBOUND_GAS_RESERVE_R3,
1377
+ pathTag: 'R3_EVM',
1378
+ enforceGasCheck: params.options?.enforceGasCheck === true,
1379
+ });
1380
+ // 12. Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1381
+ const pushChainMulticalls = (0, payload_builders_2.buildOutboundApprovalAndCall)({
1382
+ prc20Token,
1383
+ gasToken,
1384
+ burnAmount,
1385
+ gasFee,
1386
+ nativeValueForGas,
1387
+ gatewayPcAddress,
1388
+ outboundRequest: outboundReq,
1389
+ });
1390
+ (0, context_1.printLog)(ctx, `executeCeaToPush — Push Chain multicall has ${pushChainMulticalls.length} operations`);
1391
+ // Sum native values from multicall entries for proper fee calculation
1392
+ const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => sum + (mc.value ?? BigInt(0)), BigInt(0));
1393
+ // 13. Execute through the normal execute() flow
1394
+ const executeParams = {
1395
+ to: ueaAddress,
1396
+ value: multicallNativeValue, // ensures correct requiredFunds calculation
1397
+ data: pushChainMulticalls,
1398
+ _ueaStatus: {
1399
+ isDeployed: isUEADeployed,
1400
+ nonce: ueaNonce,
1401
+ balance: ueaBalance,
1402
+ },
1403
+ _skipFeeLocking: skipFeeLockingR3Evm,
1404
+ _requiredFundsOverride: requiredNativeBalanceR3Evm,
1405
+ // Ensure fee-locking deposits enough for the outbound swap whenever it is needed.
1406
+ ...(!skipFeeLockingR3Evm ? { _minimumDepositUsd: ROUTE3_MINIMUM_DEPOSIT_USD } : {}),
1407
+ };
1408
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_01);
1409
+ let response;
1410
+ try {
1411
+ response = await executeFn(executeParams);
1412
+ }
1413
+ catch (err) {
1414
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_04, err instanceof Error ? err.message : String(err));
1415
+ // Suppress the outer orchestrator catch's 399-02 terminal — 304-04
1416
+ // already surfaced the signature/broadcast failure to the consumer.
1417
+ ctx._routeTerminalEmitted = true;
1418
+ throw err;
1419
+ }
1420
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_02);
1421
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_03);
1422
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_307, sourceChain);
1423
+ // Add Route 3 context to response
1424
+ response.chain = sourceChain;
1425
+ const chainInfo = chain_1.CHAIN_INFO[sourceChain];
1426
+ response.chainNamespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
1427
+ // R3 inbound round-trip tracking: enable only when funds actually flow
1428
+ // back (amount > 0). Payload-only R3 has no child inbound UTX on Push
1429
+ // Chain, so polling would hang until timeout — skip the inbound block
1430
+ // for amount == 0.
1431
+ //
1432
+ // Child-utxId correlation uses the universal-tx-detector's deterministic
1433
+ // sha256(caip:txHash:logIndex) derivation (see inbound-tracker.ts
1434
+ // findChildUtxIdFromExternalTx), with the original cosmos
1435
+ // `universal_tx_created.inbound_tx_hash` search kept as a fallback.
1436
+ response._expectsInboundRoundTrip = amount > BigInt(0);
1437
+ return response;
1171
1438
  }
1172
1439
  // ---------------------------------------------------------------------------
1173
1440
  // executeCeaToPushSvm (Route 3 — SVM)
@@ -1180,205 +1447,250 @@ function executeCeaToPush(ctx, params, executeFn) {
1180
1447
  * the SVM gateway program (self-call). For funds-bearing calls the outbound
1181
1448
  * request amount must match the drain amount embedded in the instruction data.
1182
1449
  */
1183
- function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1184
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
1185
- var _a, _b, _c, _d, _e, _f;
1186
- if (typeof params.to !== 'string') {
1187
- throw new Error('Route 3 SVM (CEA -> Push): to must be a Push Chain address (string), not a ChainTarget');
1188
- }
1189
- const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
1190
- // Get gateway program ID from chain config and convert to 0x-hex 32 bytes
1191
- const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
1192
- if (!lockerContract) {
1193
- throw new Error(`No SVM gateway program configured for chain ${sourceChain}`);
1194
- }
1195
- const programPk = new web3_js_1.PublicKey(lockerContract);
1196
- const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
1197
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_301, sourceChain, lockerContract);
1198
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — sourceChain: ${sourceChain}, gateway: ${lockerContract}`);
1199
- // Route 3 SVM drains pre-existing funds from the Solana CEA. The Push-side
1200
- // outbound is a payload relay; do not burn pSOL/pSPL from the UEA for the
1201
- // drain amount.
1202
- const { prc20Token, drainAmount, tokenMintHex } = resolveR3SvmDrain(params, sourceChain, ctx.pushNetwork);
1203
- // Derive CEA PDA as revert recipient: ["push_identity", ueaAddress_20_bytes]
1204
- const ueaBytes = Buffer.from(ueaAddress.slice(2), 'hex'); // 20 bytes
1205
- const [ceaPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes], programPk);
1206
- const ceaPdaHex = ('0x' + Buffer.from(ceaPda.toBytes()).toString('hex'));
1207
- // Pre-fetch UEA status before payload construction. The inbound
1208
- // UniversalPayload must use the nonce after this outbound execution, because
1209
- // the Push-side execute() below consumes one UEA nonce before the Solana
1210
- // round-trip payload returns.
1211
- const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
1212
- const [ueaCode, ueaBalance] = yield Promise.all([
1450
+ async function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1451
+ if (typeof params.to !== 'string') {
1452
+ throw new Error('Route 3 SVM (CEA -> Push): to must be a Push Chain address (string), not a ChainTarget');
1453
+ }
1454
+ const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
1455
+ // Get gateway program ID from chain config and convert to 0x-hex 32 bytes
1456
+ const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
1457
+ if (!lockerContract) {
1458
+ throw new Error(`No SVM gateway program configured for chain ${sourceChain}`);
1459
+ }
1460
+ const programPk = new web3_js_1.PublicKey(lockerContract);
1461
+ const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
1462
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_301, sourceChain, lockerContract);
1463
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — sourceChain: ${sourceChain}, gateway: ${lockerContract}`);
1464
+ // Route 3 SVM drains pre-existing funds from the Solana CEA. The Push-side
1465
+ // outbound is a payload relay; do not burn pSOL/pSPL from the UEA for the
1466
+ // drain amount.
1467
+ const { prc20Token, drainAmount, tokenMintHex } = resolveR3SvmDrain(params, sourceChain, ctx.pushNetwork);
1468
+ // Derive CEA PDA as revert recipient: ["push_identity", ueaAddress_20_bytes]
1469
+ const ueaBytes = Buffer.from(ueaAddress.slice(2), 'hex'); // 20 bytes
1470
+ const [ceaPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes], programPk);
1471
+ const ceaPdaHex = ('0x' + Buffer.from(ceaPda.toBytes()).toString('hex'));
1472
+ // Pre-fetch UEA status before payload construction. The inbound
1473
+ // UniversalPayload must use the nonce after this outbound execution, because
1474
+ // the Push-side execute() below consumes one UEA nonce before the Solana
1475
+ // round-trip payload returns.
1476
+ const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
1477
+ const isNativePushEOAR3Svm = (0, helpers_1.isPushChain)(ctx.universalSigner.account.chain);
1478
+ let isUEADeployed;
1479
+ let ueaNonce;
1480
+ let ueaBalance;
1481
+ if (isNativePushEOAR3Svm) {
1482
+ isUEADeployed = true;
1483
+ ueaNonce = BigInt(0);
1484
+ ueaBalance = await ctx.pushClient.getBalance(ueaAddress);
1485
+ }
1486
+ else {
1487
+ const [ueaCode, balance] = await Promise.all([
1213
1488
  ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
1214
1489
  ctx.pushClient.getBalance(ueaAddress),
1215
1490
  ]);
1216
- const isUEADeployed = ueaCode !== undefined;
1217
- const ueaNonce = isUEADeployed ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress) : BigInt(0);
1218
- const svmPayload = (0, payload_builders_1.encodeSvmCeaToUeaPayload)({
1491
+ isUEADeployed = ueaCode !== undefined;
1492
+ ueaNonce = isUEADeployed ? await (0, uea_manager_1.getUEANonce)(ctx, ueaAddress) : BigInt(0);
1493
+ ueaBalance = balance;
1494
+ }
1495
+ // PC20 CEA burn uses a different self-call than the SPL drain: the payload
1496
+ // targets `send_universal_tx` (not `send_universal_tx_to_uea`) with the
1497
+ // program-enforced account set, and the finalize dispatcher routes it to
1498
+ // route_pc20_burn_from_finalize_cea. The unlocked Push-native token always
1499
+ // lands on the UEA (recipient == push_account is enforced on-chain);
1500
+ // forwarding to a different destination rides in the push payload.
1501
+ const r3SvmPc20 = params._pc20;
1502
+ let svmPayload;
1503
+ if (r3SvmPc20?.direction === 'import' && drainAmount > BigInt(0)) {
1504
+ const mintPk = new web3_js_1.PublicKey(r3SvmPc20.originAddress);
1505
+ const ceaAta = (0, svm_rent_2.deriveAtaPubkey)(ceaPda, mintPk);
1506
+ const extra = buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, ueaNonce + BigInt(1));
1507
+ (0, gate_1.assertPC20ImportHasPayload)(params, Boolean(extra?.length), `Route 3 SVM ${sourceChain}`);
1508
+ const fields = (0, svm_1.buildPC20SvmCeaBurnPayload)({
1509
+ gatewayProgram: programPk,
1510
+ mint: mintPk,
1511
+ ceaAuthority: ceaPda,
1512
+ ceaAta,
1513
+ ueaAddress,
1514
+ amount: drainAmount,
1515
+ pushPayload: extra ?? new Uint8Array(0),
1516
+ });
1517
+ svmPayload = (0, payload_builders_1.encodeSvmExecutePayload)(fields);
1518
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — PC20 burn: mint ${mintPk.toBase58()}, ceaAta ${ceaAta.toBase58()}`);
1519
+ }
1520
+ else {
1521
+ svmPayload = (0, payload_builders_2.encodeSvmCeaToUeaPayload)({
1219
1522
  gatewayProgramHex,
1220
1523
  drainAmount,
1221
1524
  tokenMintHex,
1222
1525
  extraPayload: buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, ueaNonce + BigInt(1)),
1223
1526
  revertRecipientHex: ceaPdaHex,
1224
1527
  });
1225
- (0, payload_builders_1.assertSvmPayloadWithinRelayLimit)(svmPayload, `Route 3 SVM ${sourceChain}`);
1226
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — drainAmount: ${drainAmount.toString()}, payload length: ${(svmPayload.length - 2) / 2} bytes`);
1227
- const burnAmount = BigInt(0);
1228
- let effectiveGasLimit = (_a = params.gasLimit) !== null && _a !== void 0 ? _a : BigInt(0);
1229
- // Build outbound request: recipient = gateway program (self-call)
1230
- let outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress, (_b = params.maxPCForGas) !== null && _b !== void 0 ? _b : BigInt(0));
1231
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — outbound request: recipient=${outboundReq.recipient.slice(0, 20)}..., token=${outboundReq.token}, maxPCForGas=${outboundReq.maxPCForGas}`);
1232
- // Query gas fees
1233
- let gasFee = BigInt(0);
1234
- let protocolFeeR3Svm = BigInt(0);
1235
- let nativeValueForGas = BigInt(0);
1236
- let gasToken = selectors_1.ZERO_ADDRESS;
1237
- let sizingDecisionR3Svm;
1238
- if (prc20Token !== selectors_1.ZERO_ADDRESS) {
1239
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01, sourceChain);
1240
- try {
1241
- let result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundReq.gasLimit, sourceChain);
1242
- result = yield (0, svm_rent_1.ensureSvmFinalizeGasBudgetQuote)({
1243
- ctx,
1244
- ueaAddress,
1245
- targetChain: sourceChain,
1246
- prc20Token,
1247
- quote: result,
1248
- splMintBase58: undefined,
1249
- burnAmount,
1250
- pathTag: 'executeCeaToPushSvm',
1251
- });
1252
- gasToken = result.gasToken;
1253
- gasFee = result.gasFee;
1254
- protocolFeeR3Svm = result.protocolFee;
1255
- nativeValueForGas = result.nativeValueForGas;
1256
- sizingDecisionR3Svm = result.sizing;
1257
- effectiveGasLimit = result.gasLimitUsed;
1258
- outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress, (_c = params.maxPCForGas) !== null && _c !== void 0 ? _c : BigInt(0));
1259
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — resolved effectiveGasLimit: ${effectiveGasLimit} ` +
1260
- `(gasFee=${result.gasFee}, gasPrice=${result.gasPrice})`);
1261
- (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'}`);
1262
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02, sourceChain, protocolFeeR3Svm, gasFee);
1263
- }
1264
- catch (err) {
1265
- throw new Error(`Failed to query outbound gas fee for SVM Route 3: ${err}`);
1266
- }
1267
- }
1268
- // Account-resolution checkpoint fires before the gas-sizing sub-hook so
1269
- // consumers see the full account-resolution phase (303-xx) complete before
1270
- // the sizing decision is surfaced.
1271
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_01, sourceChain);
1272
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_02, ueaAddress, lockerContract, sourceChain);
1273
- if (sizingDecisionR3Svm) {
1274
- fireSizingHook(ctx, 'R3', sourceChain, sizingDecisionR3Svm);
1275
- }
1276
- // R3 SVM Case C: same minimal interpretation as R3 EVM — bump msg.value
1277
- // by the overflow so swapAndBurnGas can afford the full gas cost. No
1278
- // bridge-swap entries (R3 has no destination funds-delivery semantic).
1279
- if ((sizingDecisionR3Svm === null || sizingDecisionR3Svm === void 0 ? void 0 : sizingDecisionR3Svm.category) === 'C' &&
1280
- sizingDecisionR3Svm.overflowNativePc > BigInt(0)) {
1281
- const bumped = nativeValueForGas + sizingDecisionR3Svm.overflowNativePc;
1282
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — Case C: bumping nativeValueForGas from ${nativeValueForGas} to ${bumped} (overflow=${sizingDecisionR3Svm.overflowNativePc})`);
1283
- nativeValueForGas = bumped;
1284
- }
1285
- // Re-fetch balance to minimize staleness from gas fee query RPC roundtrips.
1286
- const currentBalance = yield ctx.pushClient.getBalance(ueaAddress);
1287
- // Cosmos-EVM tx overhead costs ~1 PC per operation; 3 PC covers approve(s) + buffer.
1288
- const OUTBOUND_GAS_RESERVE_R3_SVM = BigInt(3e18);
1289
- // Upward-allocation ceiling: cap the upward adjust at max(200 PC, 5x calibrated)
1290
- // so a UEA with a giant balance doesn't submit msg.value=balance for a small
1291
- // gas swap. The contract refunds excess via swapAndBurnGas, but pushing
1292
- // hundreds of thousands of PC into a Uniswap pool moves the price for other
1293
- // users. 200 PC mirrors EVM's safety cap; 5x calibrated covers pool-skew
1294
- // scenarios where the calibrated value itself is > 40 PC (5x40 = 200).
1295
- const UPWARD_BASE_CEILING_R3_SVM = BigInt(200) * BigInt(1e18);
1296
- const calibratedCeiling = nativeValueForGas * BigInt(5);
1297
- const upwardCeiling = calibratedCeiling > UPWARD_BASE_CEILING_R3_SVM
1298
- ? calibratedCeiling
1299
- : UPWARD_BASE_CEILING_R3_SVM;
1300
- // Upward-only allocation: when balance has headroom over the calibrated
1301
- // value, consume more of it (up to the ceiling above). Down-clamp +
1302
- // zero-balance handling lives in pre-flight.
1303
- // Ordering note: pre-flight runs AFTER the upward adjust below; the
1304
- // semantics are equivalent to running it before — when upward adjust
1305
- // applies it sets nativeValueForGas to balance-reserve (capped), and the
1306
- // post-adjust pre-flight check is always satisfied. When upward adjust is
1307
- // skipped (balance too low), pre-flight runs against the calibrated value
1308
- // and either warns or throws based on options.enforceGasCheck.
1309
- if (currentBalance > OUTBOUND_GAS_RESERVE_R3_SVM &&
1310
- currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM > nativeValueForGas) {
1311
- const balanceDriven = currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM;
1312
- const adjustedValue = balanceDriven > upwardCeiling ? upwardCeiling : balanceDriven;
1313
- if (adjustedValue !== nativeValueForGas) {
1314
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${adjustedValue.toString()} ` +
1315
- `(UEA balance: ${currentBalance.toString()}, upwardCeiling: ${upwardCeiling.toString()})`);
1316
- nativeValueForGas = adjustedValue;
1317
- }
1318
- }
1319
- // SVM warn-threshold telemetry (no truncation — pre-flight handles drain protection).
1320
- (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R3_SVM');
1321
- const requiredNativeBalanceR3Svm = getRequiredNativeBalanceForOutbound(nativeValueForGas, OUTBOUND_GAS_RESERVE_R3_SVM);
1322
- const skipFeeLockingR3Svm = shouldSkipFeeLockingForOutbound({
1323
- isUEADeployed,
1324
- balance: currentBalance,
1325
- nativeValueForGas,
1326
- gasReserve: OUTBOUND_GAS_RESERVE_R3_SVM,
1327
- });
1328
- // Pre-flight (R3 SVM): no PRC-20 burn check. The drain amount lives in the
1329
- // embedded Solana instruction and is withdrawn from the external CEA.
1330
- (0, preflight_1.runPreflight)({
1331
- ctx,
1332
- ueaAddress,
1333
- ueaBalance: currentBalance,
1334
- requiredValue: nativeValueForGas,
1335
- gasReserve: OUTBOUND_GAS_RESERVE_R3_SVM,
1336
- pathTag: 'R3_SVM',
1337
- enforceGasCheck: ((_f = params.options) === null || _f === void 0 ? void 0 : _f.enforceGasCheck) === true,
1338
- });
1339
- // Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1340
- const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
1341
- prc20Token,
1342
- gasToken,
1343
- burnAmount,
1344
- gasFee,
1345
- nativeValueForGas,
1346
- gatewayPcAddress,
1347
- outboundRequest: outboundReq,
1348
- });
1349
- // Sum native values from multicall entries for proper fee calculation
1350
- const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => { var _a; return sum + ((_a = mc.value) !== null && _a !== void 0 ? _a : BigInt(0)); }, BigInt(0));
1351
- // Execute through the normal execute() flow
1352
- const executeParams = Object.assign({ to: ueaAddress, value: multicallNativeValue, data: pushChainMulticalls, _ueaStatus: {
1353
- isDeployed: isUEADeployed,
1354
- nonce: ueaNonce,
1355
- balance: currentBalance,
1356
- }, _skipFeeLocking: skipFeeLockingR3Svm, _requiredFundsOverride: requiredNativeBalanceR3Svm }, (!skipFeeLockingR3Svm ? { _minimumDepositUsd: utils_1.Utils.helpers.parseUnits('10', 8) } : {}));
1357
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_01);
1358
- let response;
1528
+ }
1529
+ (0, payload_builders_2.assertSvmPayloadWithinRelayLimit)(svmPayload, `Route 3 SVM ${sourceChain}`);
1530
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — drainAmount: ${drainAmount.toString()}, payload length: ${(svmPayload.length - 2) / 2} bytes`);
1531
+ const burnAmount = BigInt(0);
1532
+ let effectiveGasLimit = params.gasLimit ?? BigInt(0);
1533
+ // Build outbound request: recipient = gateway program (self-call)
1534
+ let outboundReq = (0, payload_builders_2.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress, params.maxPCForGas ?? BigInt(0));
1535
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — outbound request: recipient=${outboundReq.recipient.slice(0, 20)}..., token=${outboundReq.token}, maxPCForGas=${outboundReq.maxPCForGas}`);
1536
+ // Query gas fees
1537
+ let gasFee = BigInt(0);
1538
+ let protocolFeeR3Svm = BigInt(0);
1539
+ let nativeValueForGas = BigInt(0);
1540
+ let gasToken = selectors_1.ZERO_ADDRESS;
1541
+ let sizingDecisionR3Svm;
1542
+ if (prc20Token !== selectors_1.ZERO_ADDRESS) {
1543
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01, sourceChain);
1359
1544
  try {
1360
- response = yield executeFn(executeParams);
1545
+ let result = await (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundReq.gasLimit, sourceChain);
1546
+ result = await (0, svm_rent_1.ensureSvmFinalizeGasBudgetQuote)({
1547
+ ctx,
1548
+ ueaAddress,
1549
+ targetChain: sourceChain,
1550
+ prc20Token,
1551
+ quote: result,
1552
+ splMintBase58: undefined,
1553
+ burnAmount,
1554
+ pathTag: 'executeCeaToPushSvm',
1555
+ });
1556
+ gasToken = result.gasToken;
1557
+ gasFee = result.gasFee;
1558
+ protocolFeeR3Svm = result.protocolFee;
1559
+ nativeValueForGas = result.nativeValueForGas;
1560
+ sizingDecisionR3Svm = result.sizing;
1561
+ effectiveGasLimit = result.gasLimitUsed;
1562
+ outboundReq = (0, payload_builders_2.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress, params.maxPCForGas ?? BigInt(0));
1563
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — resolved effectiveGasLimit: ${effectiveGasLimit} ` +
1564
+ `(gasFee=${result.gasFee}, gasPrice=${result.gasPrice})`);
1565
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — gasFee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}, sizing=${result.sizing?.category ?? 'legacy'}`);
1566
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02, sourceChain, protocolFeeR3Svm, gasFee);
1361
1567
  }
1362
1568
  catch (err) {
1363
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_04, err instanceof Error ? err.message : String(err));
1364
- // Suppress the outer orchestrator catch's 399-02 terminal.
1365
- ctx._routeTerminalEmitted = true;
1366
- throw err;
1569
+ throw new Error(`Failed to query outbound gas fee for SVM Route 3: ${err}`);
1367
1570
  }
1368
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_02);
1369
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_03);
1370
- (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_307, sourceChain);
1371
- // Add Route 3 SVM context to response
1372
- response.chain = sourceChain;
1373
- const chainInfo = chain_1.CHAIN_INFO[sourceChain];
1374
- response.chainNamespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
1375
- // R3 SVM mirrors EVM R3: when funds drain back to Push, the Solana-side
1376
- // send_universal_tx_to_uea emits an inbound leg whose payload can execute on
1377
- // Push. Payload-only R3 has no child inbound UTX, so skip round-trip tracking
1378
- // when no amount is being drained.
1379
- response._expectsInboundRoundTrip = drainAmount > BigInt(0);
1380
- return response;
1571
+ }
1572
+ // Account-resolution checkpoint fires before the gas-sizing sub-hook so
1573
+ // consumers see the full account-resolution phase (303-xx) complete before
1574
+ // the sizing decision is surfaced.
1575
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_01, sourceChain);
1576
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_02, ueaAddress, lockerContract, sourceChain);
1577
+ if (sizingDecisionR3Svm) {
1578
+ fireSizingHook(ctx, 'R3', sourceChain, sizingDecisionR3Svm);
1579
+ }
1580
+ // R3 SVM Case C: same minimal interpretation as R3 EVM — bump msg.value
1581
+ // by the overflow so swapAndBurnGas can afford the full gas cost. No
1582
+ // bridge-swap entries (R3 has no destination funds-delivery semantic).
1583
+ if (sizingDecisionR3Svm?.category === 'C' &&
1584
+ sizingDecisionR3Svm.overflowNativePc > BigInt(0)) {
1585
+ const bumped = nativeValueForGas + sizingDecisionR3Svm.overflowNativePc;
1586
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — Case C: bumping nativeValueForGas from ${nativeValueForGas} to ${bumped} (overflow=${sizingDecisionR3Svm.overflowNativePc})`);
1587
+ nativeValueForGas = bumped;
1588
+ }
1589
+ // Re-fetch balance to minimize staleness from gas fee query RPC roundtrips.
1590
+ const currentBalance = await ctx.pushClient.getBalance(ueaAddress);
1591
+ // Cosmos-EVM tx overhead costs ~1 PC per operation; 3 PC covers approve(s) + buffer.
1592
+ const OUTBOUND_GAS_RESERVE_R3_SVM = BigInt(3e18);
1593
+ // Upward-allocation ceiling: cap the upward adjust at max(200 PC, 5x calibrated)
1594
+ // so a UEA with a giant balance doesn't submit msg.value=balance for a small
1595
+ // gas swap. The contract refunds excess via swapAndBurnGas, but pushing
1596
+ // hundreds of thousands of PC into a Uniswap pool moves the price for other
1597
+ // users. 200 PC mirrors EVM's safety cap; 5x calibrated covers pool-skew
1598
+ // scenarios where the calibrated value itself is > 40 PC (5x40 = 200).
1599
+ const UPWARD_BASE_CEILING_R3_SVM = BigInt(200) * BigInt(1e18);
1600
+ const calibratedCeiling = nativeValueForGas * BigInt(5);
1601
+ const upwardCeiling = calibratedCeiling > UPWARD_BASE_CEILING_R3_SVM
1602
+ ? calibratedCeiling
1603
+ : UPWARD_BASE_CEILING_R3_SVM;
1604
+ // Upward-only allocation: when balance has headroom over the calibrated
1605
+ // value, consume more of it (up to the ceiling above). Down-clamp +
1606
+ // zero-balance handling lives in pre-flight.
1607
+ // Ordering note: pre-flight runs AFTER the upward adjust below; the
1608
+ // semantics are equivalent to running it before — when upward adjust
1609
+ // applies it sets nativeValueForGas to balance-reserve (capped), and the
1610
+ // post-adjust pre-flight check is always satisfied. When upward adjust is
1611
+ // skipped (balance too low), pre-flight runs against the calibrated value
1612
+ // and either warns or throws based on options.enforceGasCheck.
1613
+ if (currentBalance > OUTBOUND_GAS_RESERVE_R3_SVM &&
1614
+ currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM > nativeValueForGas) {
1615
+ const balanceDriven = currentBalance - OUTBOUND_GAS_RESERVE_R3_SVM;
1616
+ const adjustedValue = balanceDriven > upwardCeiling ? upwardCeiling : balanceDriven;
1617
+ if (adjustedValue !== nativeValueForGas) {
1618
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${adjustedValue.toString()} ` +
1619
+ `(UEA balance: ${currentBalance.toString()}, upwardCeiling: ${upwardCeiling.toString()})`);
1620
+ nativeValueForGas = adjustedValue;
1621
+ }
1622
+ }
1623
+ // SVM warn-threshold telemetry (no truncation — pre-flight handles drain protection).
1624
+ (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R3_SVM');
1625
+ const requiredNativeBalanceR3Svm = getRequiredNativeBalanceForOutbound(nativeValueForGas, OUTBOUND_GAS_RESERVE_R3_SVM);
1626
+ const skipFeeLockingR3Svm = shouldSkipFeeLockingForOutbound({
1627
+ isUEADeployed,
1628
+ balance: currentBalance,
1629
+ nativeValueForGas,
1630
+ gasReserve: OUTBOUND_GAS_RESERVE_R3_SVM,
1381
1631
  });
1632
+ // Pre-flight (R3 SVM): no PRC-20 burn check. The drain amount lives in the
1633
+ // embedded Solana instruction and is withdrawn from the external CEA.
1634
+ (0, preflight_1.runPreflight)({
1635
+ ctx,
1636
+ ueaAddress,
1637
+ ueaBalance: currentBalance,
1638
+ requiredValue: nativeValueForGas,
1639
+ gasReserve: OUTBOUND_GAS_RESERVE_R3_SVM,
1640
+ pathTag: 'R3_SVM',
1641
+ enforceGasCheck: params.options?.enforceGasCheck === true,
1642
+ });
1643
+ // Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1644
+ const pushChainMulticalls = (0, payload_builders_2.buildOutboundApprovalAndCall)({
1645
+ prc20Token,
1646
+ gasToken,
1647
+ burnAmount,
1648
+ gasFee,
1649
+ nativeValueForGas,
1650
+ gatewayPcAddress,
1651
+ outboundRequest: outboundReq,
1652
+ });
1653
+ // Sum native values from multicall entries for proper fee calculation
1654
+ const multicallNativeValue = pushChainMulticalls.reduce((sum, mc) => sum + (mc.value ?? BigInt(0)), BigInt(0));
1655
+ // Execute through the normal execute() flow
1656
+ const executeParams = {
1657
+ to: ueaAddress,
1658
+ value: multicallNativeValue, // ensures correct requiredFunds calculation
1659
+ data: pushChainMulticalls,
1660
+ _ueaStatus: {
1661
+ isDeployed: isUEADeployed,
1662
+ nonce: ueaNonce,
1663
+ balance: currentBalance,
1664
+ },
1665
+ _skipFeeLocking: skipFeeLockingR3Svm,
1666
+ _requiredFundsOverride: requiredNativeBalanceR3Svm,
1667
+ // Ensure fee-locking deposits enough for the outbound swap whenever it is needed.
1668
+ ...(!skipFeeLockingR3Svm ? { _minimumDepositUsd: utils_1.Utils.helpers.parseUnits('10', 8) } : {}),
1669
+ };
1670
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_01);
1671
+ let response;
1672
+ try {
1673
+ response = await executeFn(executeParams);
1674
+ }
1675
+ catch (err) {
1676
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_04, err instanceof Error ? err.message : String(err));
1677
+ // Suppress the outer orchestrator catch's 399-02 terminal.
1678
+ ctx._routeTerminalEmitted = true;
1679
+ throw err;
1680
+ }
1681
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_02);
1682
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_03);
1683
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_307, sourceChain);
1684
+ // Add Route 3 SVM context to response
1685
+ response.chain = sourceChain;
1686
+ const chainInfo = chain_1.CHAIN_INFO[sourceChain];
1687
+ response.chainNamespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
1688
+ // R3 SVM mirrors EVM R3: when funds drain back to Push, the Solana-side
1689
+ // send_universal_tx_to_uea emits an inbound leg whose payload can execute on
1690
+ // Push. Payload-only R3 has no child inbound UTX, so skip round-trip tracking
1691
+ // when no amount is being drained.
1692
+ response._expectsInboundRoundTrip = drainAmount > BigInt(0);
1693
+ return response;
1382
1694
  }
1383
1695
  // ---------------------------------------------------------------------------
1384
1696
  // executeCeaToCea (Route 4 — stub)
@@ -1386,13 +1698,11 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1386
1698
  /**
1387
1699
  * Route 4: Execute CEA to CEA transaction via Push Chain.
1388
1700
  */
1389
- function executeCeaToCea(ctx, params, executeFn) {
1390
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
1391
- // CEA -> CEA requires chaining Route 3 (CEA -> Push) and Route 2 (Push -> CEA)
1392
- // This is a complex flow that requires coordination
1393
- throw new Error('CEA -> CEA transactions are not yet fully implemented. ' +
1394
- 'Use prepareTransaction() and executeTransactions() to chain Route 3 -> Route 2 manually.');
1395
- });
1701
+ async function executeCeaToCea(ctx, params, executeFn) {
1702
+ // CEA -> CEA requires chaining Route 3 (CEA -> Push) and Route 2 (Push -> CEA)
1703
+ // This is a complex flow that requires coordination
1704
+ throw new Error('CEA -> CEA transactions are not yet fully implemented. ' +
1705
+ 'Use prepareTransaction() and executeTransactions() to chain Route 3 -> Route 2 manually.');
1396
1706
  }
1397
1707
  // ---------------------------------------------------------------------------
1398
1708
  // buildPayloadForRoute
@@ -1400,163 +1710,187 @@ function executeCeaToCea(ctx, params, executeFn) {
1400
1710
  /**
1401
1711
  * Build payload for a specific route.
1402
1712
  */
1403
- function buildPayloadForRoute(ctx, params, route, nonce) {
1404
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
1405
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
1406
- const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
1407
- switch (route) {
1408
- case route_detector_1.TransactionRoute.UOA_TO_PUSH: {
1409
- // Build standard Push Chain payload
1410
- const executeParams = (0, helpers_1.toExecuteParams)(params);
1411
- const multicallData = (0, payload_builders_1.buildExecuteMulticall)({
1412
- execute: executeParams,
1413
- ueaAddress,
1414
- });
1415
- const payload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, executeParams.to, multicallData);
1416
- const req = (0, payload_builder_1.buildUniversalTxRequest)(ctx.universalSigner.account.address, {
1417
- recipient: viem_1.zeroAddress,
1418
- token: viem_1.zeroAddress,
1419
- amount: BigInt(0),
1420
- payload,
1713
+ async function buildPayloadForRoute(ctx, params, route, nonce) {
1714
+ const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
1715
+ switch (route) {
1716
+ case route_detector_1.TransactionRoute.UOA_TO_PUSH: {
1717
+ // Build standard Push Chain payload
1718
+ const executeParams = (0, helpers_1.toExecuteParams)(params);
1719
+ const multicallData = (0, payload_builders_2.buildExecuteMulticall)({
1720
+ execute: (0, gate_1.assertLegacyFunds)(executeParams),
1721
+ ueaAddress,
1722
+ });
1723
+ const payload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, executeParams.to, multicallData);
1724
+ const req = (0, payload_builder_1.buildUniversalTxRequest)(ctx.universalSigner.account.address, {
1725
+ recipient: viem_1.zeroAddress,
1726
+ token: viem_1.zeroAddress,
1727
+ amount: BigInt(0),
1728
+ payload,
1729
+ });
1730
+ return { payload, gatewayRequest: req };
1731
+ }
1732
+ case route_detector_1.TransactionRoute.UOA_TO_CEA: {
1733
+ const target = params.to;
1734
+ // Branch: SVM vs EVM
1735
+ if ((0, payload_builders_2.isSvmChain)(target.chain)) {
1736
+ // SVM: build SVM payload (binary or empty for withdraw) via IDL resolver
1737
+ const { svmPayload: payload, targetBytes, hasExecute, } = (0, build_payload_1.buildSvmPayloadFromParams)({
1738
+ data: params.data,
1739
+ to: target,
1740
+ senderUea: ueaAddress,
1421
1741
  });
1422
- return { payload, gatewayRequest: req };
1423
- }
1424
- case route_detector_1.TransactionRoute.UOA_TO_CEA: {
1425
- const target = params.to;
1426
- // Branch: SVM vs EVM
1427
- if ((0, payload_builders_1.isSvmChain)(target.chain)) {
1428
- // SVM: build SVM payload (binary or empty for withdraw) via IDL resolver
1429
- const { svmPayload: payload, targetBytes, hasExecute, } = (0, build_payload_1.buildSvmPayloadFromParams)({
1430
- data: params.data,
1431
- to: target,
1432
- senderUea: ueaAddress,
1433
- });
1434
- (0, payload_builders_1.assertSvmPayloadWithinRelayLimit)(payload, `Route 2 SVM ${target.chain}`);
1435
- let prc20Token = selectors_1.ZERO_ADDRESS;
1436
- let burnAmount = BigInt(0);
1437
- if ((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) {
1438
- const token = params.funds.token;
1439
- if (token) {
1440
- prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token).address;
1441
- burnAmount = params.funds.amount;
1442
- }
1443
- }
1444
- else if (params.value && params.value > BigInt(0)) {
1445
- prc20Token = (0, helpers_1.getNativePRC20ForChain)(target.chain, ctx.pushNetwork);
1446
- burnAmount = params.value;
1447
- }
1448
- else if (hasExecute) {
1449
- prc20Token = (0, helpers_1.getNativePRC20ForChain)(target.chain, ctx.pushNetwork);
1450
- burnAmount = BigInt(0);
1742
+ (0, payload_builders_2.assertSvmPayloadWithinRelayLimit)(payload, `Route 2 SVM ${target.chain}`);
1743
+ let prc20Token = selectors_1.ZERO_ADDRESS;
1744
+ let burnAmount = BigInt(0);
1745
+ if (params.funds?.amount) {
1746
+ const token = params.funds.token;
1747
+ if (token) {
1748
+ prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token).address;
1749
+ burnAmount = params.funds.amount;
1451
1750
  }
1452
- 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));
1453
- return { payload, gatewayRequest: outboundReq };
1454
1751
  }
1455
- // EVM path: Resolve CEA address first (needed for self-transfer check)
1456
- 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]);
1457
- const payload = buildR2CeaPayloadEvm(params, ceaAddress, target.address, target.chain, ctx.pushNetwork);
1458
- const { prc20Token, burnAmount } = resolveR2Prc20TokenEvm(params, target.chain, ctx.pushNetwork);
1459
- const targetBytes = payload === '0x'
1460
- ? selectors_1.ZERO_ADDRESS
1461
- : ceaAddress;
1462
- (0, payload_builders_1.assertCeaFundsParkingInvariant)(targetBytes, payload);
1463
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_e = params.gasLimit) !== null && _e !== void 0 ? _e : BigInt(0), payload, ueaAddress, (_f = params.maxPCForGas) !== null && _f !== void 0 ? _f : BigInt(0));
1752
+ else if (params.value && params.value > BigInt(0)) {
1753
+ prc20Token = (0, helpers_1.getNativePRC20ForChain)(target.chain, ctx.pushNetwork);
1754
+ burnAmount = params.value;
1755
+ }
1756
+ else if (hasExecute) {
1757
+ prc20Token = (0, helpers_1.getNativePRC20ForChain)(target.chain, ctx.pushNetwork);
1758
+ burnAmount = BigInt(0);
1759
+ }
1760
+ const outboundReq = (0, payload_builders_2.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, params.gasLimit ?? BigInt(0), payload, ueaAddress, params.maxPCForGas ?? BigInt(0));
1464
1761
  return { payload, gatewayRequest: outboundReq };
1465
1762
  }
1466
- case route_detector_1.TransactionRoute.CEA_TO_PUSH: {
1467
- // Route 3: CEA -> Push Chain
1468
- // Build CEA multicall (sendUniversalTxToUEA self-call) and wrap in outbound
1469
- if (!((_g = params.from) === null || _g === void 0 ? void 0 : _g.chain)) {
1470
- throw new Error('Route 3 (CEA -> Push) requires from.chain');
1763
+ // EVM path: Resolve CEA address first (needed for self-transfer check)
1764
+ const { cea: ceaAddress } = await (0, cea_utils_1.getCEAAddress)(ueaAddress, target.chain, ctx.rpcUrls[target.chain]?.[0]);
1765
+ const payload = buildR2CeaPayloadEvm(params, ceaAddress, target.address, target.chain, ctx.pushNetwork);
1766
+ const { prc20Token, burnAmount } = resolveR2Prc20TokenEvm(params, target.chain, ctx.pushNetwork);
1767
+ const targetBytes = payload === '0x'
1768
+ ? selectors_1.ZERO_ADDRESS
1769
+ : ceaAddress;
1770
+ (0, payload_builders_2.assertCeaFundsParkingInvariant)(targetBytes, payload);
1771
+ const outboundReq = (0, payload_builders_2.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, params.gasLimit ?? BigInt(0), payload, ueaAddress, params.maxPCForGas ?? BigInt(0));
1772
+ return { payload, gatewayRequest: outboundReq };
1773
+ }
1774
+ case route_detector_1.TransactionRoute.CEA_TO_PUSH: {
1775
+ // Route 3: CEA -> Push Chain
1776
+ // Build CEA multicall (sendUniversalTxToUEA self-call) and wrap in outbound
1777
+ if (!params.from?.chain) {
1778
+ throw new Error('Route 3 (CEA -> Push) requires from.chain');
1779
+ }
1780
+ const sourceChain = params.from.chain;
1781
+ const pushDestination = params.to;
1782
+ // SVM chains use gateway self-call, not CEA multicall
1783
+ if ((0, payload_builders_2.isSvmChain)(sourceChain)) {
1784
+ const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
1785
+ if (!lockerContract) {
1786
+ throw new Error(`No SVM gateway program configured for chain ${sourceChain}`);
1471
1787
  }
1472
- const sourceChain = params.from.chain;
1473
- const pushDestination = params.to;
1474
- // SVM chains use gateway self-call, not CEA multicall
1475
- if ((0, payload_builders_1.isSvmChain)(sourceChain)) {
1476
- const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
1477
- if (!lockerContract) {
1478
- throw new Error(`No SVM gateway program configured for chain ${sourceChain}`);
1479
- }
1480
- const programPk = new web3_js_1.PublicKey(lockerContract);
1481
- const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
1482
- const { prc20Token, drainAmount, tokenMintHex } = resolveR3SvmDrain(params, sourceChain, ctx.pushNetwork);
1483
- // Derive CEA PDA as revert recipient
1484
- const ueaBytes2 = Buffer.from(ueaAddress.slice(2), 'hex');
1485
- const [ceaPda2] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes2], programPk);
1486
- const ceaPdaHex2 = ('0x' + Buffer.from(ceaPda2.toBytes()).toString('hex'));
1487
- const svmPayload = (0, payload_builders_1.encodeSvmCeaToUeaPayload)({
1788
+ const programPk = new web3_js_1.PublicKey(lockerContract);
1789
+ const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
1790
+ const { prc20Token, drainAmount, tokenMintHex } = resolveR3SvmDrain(params, sourceChain, ctx.pushNetwork);
1791
+ // Derive CEA PDA as revert recipient
1792
+ const ueaBytes2 = Buffer.from(ueaAddress.slice(2), 'hex');
1793
+ const [ceaPda2] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes2], programPk);
1794
+ const ceaPdaHex2 = ('0x' + Buffer.from(ceaPda2.toBytes()).toString('hex'));
1795
+ const extraPayload = buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, nonce + BigInt(1));
1796
+ (0, gate_1.assertPC20ImportHasPayload)(params, Boolean(extraPayload?.length), `Prepared Route 3 SVM ${sourceChain}`);
1797
+ const r3SvmPc20 = params._pc20;
1798
+ let svmPayload;
1799
+ if ((0, gate_1.isPC20ImportWithFunds)(params) && r3SvmPc20) {
1800
+ const mintPk = new web3_js_1.PublicKey(r3SvmPc20.originAddress);
1801
+ const fields = (0, svm_1.buildPC20SvmCeaBurnPayload)({
1802
+ gatewayProgram: programPk,
1803
+ mint: mintPk,
1804
+ ceaAuthority: ceaPda2,
1805
+ ceaAta: (0, svm_rent_2.deriveAtaPubkey)(ceaPda2, mintPk),
1806
+ ueaAddress,
1807
+ amount: drainAmount,
1808
+ pushPayload: extraPayload ?? new Uint8Array(0),
1809
+ });
1810
+ svmPayload = (0, payload_builders_1.encodeSvmExecutePayload)(fields);
1811
+ }
1812
+ else {
1813
+ svmPayload = (0, payload_builders_2.encodeSvmCeaToUeaPayload)({
1488
1814
  gatewayProgramHex,
1489
1815
  drainAmount,
1490
1816
  tokenMintHex,
1491
- extraPayload: buildR3SvmExtraPayload(ctx, params, ueaAddress, sourceChain, nonce + BigInt(1)),
1817
+ extraPayload,
1492
1818
  revertRecipientHex: ceaPdaHex2,
1493
1819
  });
1494
- (0, payload_builders_1.assertSvmPayloadWithinRelayLimit)(svmPayload, `Route 3 SVM ${sourceChain}`);
1495
- // Payload-only outbound: drain amount lives inside ixData.
1496
- const burnAmount = BigInt(0);
1497
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, (_h = params.gasLimit) !== null && _h !== void 0 ? _h : BigInt(0), svmPayload, ueaAddress, (_j = params.maxPCForGas) !== null && _j !== void 0 ? _j : BigInt(0));
1498
- return { payload: svmPayload, gatewayRequest: outboundReq };
1499
1820
  }
1500
- const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_k = ctx.rpcUrls[sourceChain]) === null || _k === void 0 ? void 0 : _k[0]);
1501
- // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
1502
- // if CEA doesn't exist yet. No SDK-side blocking needed.
1503
- // Build CEA multicalls (self-calls via sendUniversalTxToUEA)
1504
- const ceaMulticalls = [];
1505
- let tokenAddress = selectors_1.ZERO_ADDRESS;
1506
- let amount = BigInt(0);
1507
- if ((_l = params.funds) === null || _l === void 0 ? void 0 : _l.amount) {
1508
- const token = params.funds.token;
1509
- if (token) {
1510
- if (token.mechanism === 'native') {
1511
- tokenAddress = selectors_1.ZERO_ADDRESS;
1512
- amount = params.funds.amount;
1513
- }
1514
- else {
1515
- tokenAddress = token.address;
1516
- amount = params.funds.amount;
1517
- }
1821
+ (0, payload_builders_2.assertSvmPayloadWithinRelayLimit)(svmPayload, `Route 3 SVM ${sourceChain}`);
1822
+ // Payload-only outbound: drain amount lives inside ixData.
1823
+ const burnAmount = BigInt(0);
1824
+ const outboundReq = (0, payload_builders_2.buildOutboundRequest)(gatewayProgramHex, prc20Token, burnAmount, params.gasLimit ?? BigInt(0), svmPayload, ueaAddress, params.maxPCForGas ?? BigInt(0));
1825
+ return { payload: svmPayload, gatewayRequest: outboundReq };
1826
+ }
1827
+ const { cea: ceaAddress } = await (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, ctx.rpcUrls[sourceChain]?.[0]);
1828
+ // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
1829
+ // if CEA doesn't exist yet. No SDK-side blocking needed.
1830
+ // Build CEA multicalls (self-calls via sendUniversalTxToUEA)
1831
+ const ceaMulticalls = [];
1832
+ let tokenAddress = selectors_1.ZERO_ADDRESS;
1833
+ let amount = BigInt(0);
1834
+ if (params.funds?.amount) {
1835
+ const token = params.funds.token;
1836
+ if (token) {
1837
+ if (token.mechanism === 'native') {
1838
+ tokenAddress = selectors_1.ZERO_ADDRESS;
1839
+ amount = params.funds.amount;
1840
+ }
1841
+ else {
1842
+ tokenAddress = token.address;
1843
+ amount = params.funds.amount;
1518
1844
  }
1519
1845
  }
1520
- else if (params.value && params.value > BigInt(0)) {
1521
- tokenAddress = selectors_1.ZERO_ADDRESS;
1522
- amount = params.value;
1523
- }
1524
- // Fetch UEA nonce for inbound UniversalPayload
1525
- const ueaCodeHop = yield ctx.pushClient.publicClient.getCode({ address: ueaAddress });
1526
- const ueaNonceHop = ueaCodeHop !== undefined ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress) : BigInt(0);
1527
- // Build Push Chain payload (what executes after inbound arrives)
1528
- // Wrap in UniversalPayload struct for the relay.
1529
- let pushPayload = '0x';
1530
- if ((0, data_utils_1.hasExecutablePayloadData)(params.data) ||
1531
- (((_m = params.funds) === null || _m === void 0 ? void 0 : _m.amount) && ((_o = params.value) !== null && _o !== void 0 ? _o : BigInt(0)) > BigInt(0))) {
1532
- const multicallData = (0, payload_builders_1.buildExecuteMulticall)({
1533
- execute: {
1534
- to: pushDestination,
1535
- value: params.value,
1536
- data: params.data,
1537
- },
1538
- ueaAddress,
1539
- allowSelfValueCall: true,
1540
- });
1541
- const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, pushDestination, multicallData);
1542
- pushPayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: ueaNonceHop + BigInt(1) });
1543
- }
1544
- // Build sendUniversalTxToUEA self-call on CEA. CEA self-calls must
1545
- // always use value=0; native sends spend from the CEA balance.
1546
- const sendCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, tokenAddress, amount, pushPayload, ceaAddress);
1547
- ceaMulticalls.push(sendCall);
1548
- const ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
1549
- const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
1550
- // Route 3: CEA uses its own pre-existing balance — no PRC-20 burn needed.
1551
- // burnAmount = 0 makes this a payload-only outbound relay.
1552
- const burnAmount = BigInt(0);
1553
- const outboundGasLimit = (_p = params.gasLimit) !== null && _p !== void 0 ? _p : selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
1554
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, outboundGasLimit, ceaPayload, ueaAddress, (_q = params.maxPCForGas) !== null && _q !== void 0 ? _q : BigInt(0));
1555
- return { payload: ceaPayload, gatewayRequest: outboundReq };
1556
1846
  }
1557
- default:
1558
- throw new Error(`Cannot build payload for route: ${route}`);
1847
+ else if (params.value && params.value > BigInt(0)) {
1848
+ tokenAddress = selectors_1.ZERO_ADDRESS;
1849
+ amount = params.value;
1850
+ }
1851
+ // prepareTransaction already resolved the correct nonce semantics. For a
1852
+ // native Push EOA this is zero; for an external-origin account it is the
1853
+ // current UEA nonce. Do not reclassify EIP-7702 code as a deployed UEA.
1854
+ const ueaNonceHop = nonce;
1855
+ // Build Push Chain payload (what executes after inbound arrives)
1856
+ // Wrap in UniversalPayload struct for the relay.
1857
+ let pushPayload = '0x';
1858
+ const r3Pc20 = params._pc20;
1859
+ const r3Pc20NeedsForward = (0, gate_1.isPC20ImportWithFunds)(params);
1860
+ if ((0, data_utils_1.hasExecutablePayloadData)(params.data) ||
1861
+ (params.funds?.amount && (params.value ?? BigInt(0)) > BigInt(0)) ||
1862
+ r3Pc20NeedsForward) {
1863
+ const multicallData = (0, payload_builders_2.buildExecuteMulticall)({
1864
+ execute: {
1865
+ to: pushDestination,
1866
+ value: params.value,
1867
+ data: params.data,
1868
+ ...(r3Pc20NeedsForward
1869
+ ? { funds: params.funds, _pc20: r3Pc20 }
1870
+ : {}),
1871
+ },
1872
+ ueaAddress,
1873
+ allowSelfValueCall: true,
1874
+ });
1875
+ const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, pushDestination, multicallData);
1876
+ pushPayload = (0, payload_builders_2.buildInboundUniversalPayload)(multicallPayload, { nonce: ueaNonceHop + BigInt(1) });
1877
+ }
1878
+ (0, gate_1.assertPC20ImportHasPayload)(params, pushPayload !== '0x', `Prepared Route 3 EVM ${sourceChain}`);
1879
+ // Build sendUniversalTxToUEA self-call on CEA. CEA self-calls must
1880
+ // always use value=0; native sends spend from the CEA balance.
1881
+ const sendCall = (0, payload_builders_2.buildSendUniversalTxToUEA)(ceaAddress, tokenAddress, amount, pushPayload, ceaAddress);
1882
+ ceaMulticalls.push(sendCall);
1883
+ const ceaPayload = (0, payload_builders_2.buildCeaMulticallPayload)(ceaMulticalls);
1884
+ const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
1885
+ // Route 3: CEA uses its own pre-existing balance — no PRC-20 burn needed.
1886
+ // burnAmount = 0 makes this a payload-only outbound relay.
1887
+ const burnAmount = BigInt(0);
1888
+ const outboundGasLimit = params.gasLimit ?? selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
1889
+ const outboundReq = (0, payload_builders_2.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, outboundGasLimit, ceaPayload, ueaAddress, params.maxPCForGas ?? BigInt(0));
1890
+ return { payload: ceaPayload, gatewayRequest: outboundReq };
1559
1891
  }
1560
- });
1892
+ default:
1893
+ throw new Error(`Cannot build payload for route: ${route}`);
1894
+ }
1561
1895
  }
1562
1896
  //# sourceMappingURL=route-handlers.js.map