@pushchain/core 5.1.5-alpha.1 → 5.1.6

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 (86) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +1 -1
  3. package/src/lib/constants/abi/index.d.ts +2 -0
  4. package/src/lib/constants/abi/index.js +6 -1
  5. package/src/lib/constants/abi/uniswap-v3-router.evm.d.ts +58 -0
  6. package/src/lib/constants/abi/uniswap-v3-router.evm.js +41 -0
  7. package/src/lib/constants/abi/wpc.evm.d.ts +24 -0
  8. package/src/lib/constants/abi/wpc.evm.js +27 -0
  9. package/src/lib/constants/chain.d.ts +1 -0
  10. package/src/lib/constants/chain.js +8 -6
  11. package/src/lib/constants/tokens.js +3 -3
  12. package/src/lib/generated/uexecutor/v2/types.d.ts +14 -0
  13. package/src/lib/generated/uexecutor/v2/types.js +96 -3
  14. package/src/lib/orchestrator/internals/cascade.d.ts +1 -1
  15. package/src/lib/orchestrator/internals/cascade.js +521 -239
  16. package/src/lib/orchestrator/internals/confirmation.js +60 -15
  17. package/src/lib/orchestrator/internals/context.d.ts +23 -0
  18. package/src/lib/orchestrator/internals/context.js +58 -0
  19. package/src/lib/orchestrator/internals/execute-funds-only.js +47 -21
  20. package/src/lib/orchestrator/internals/execute-funds-payload.js +70 -37
  21. package/src/lib/orchestrator/internals/execute-standard.js +137 -26
  22. package/src/lib/orchestrator/internals/gas-calculator.d.ts +5 -2
  23. package/src/lib/orchestrator/internals/gas-calculator.js +59 -11
  24. package/src/lib/orchestrator/internals/gas-usd-sizer.d.ts +53 -0
  25. package/src/lib/orchestrator/internals/gas-usd-sizer.js +165 -0
  26. package/src/lib/orchestrator/internals/helpers.js +2 -0
  27. package/src/lib/orchestrator/internals/inbound-tracker.d.ts +83 -0
  28. package/src/lib/orchestrator/internals/inbound-tracker.js +322 -0
  29. package/src/lib/orchestrator/internals/index.d.ts +3 -1
  30. package/src/lib/orchestrator/internals/index.js +8 -2
  31. package/src/lib/orchestrator/internals/outbound-sync.d.ts +32 -1
  32. package/src/lib/orchestrator/internals/outbound-sync.js +154 -17
  33. package/src/lib/orchestrator/internals/pc-usd-oracle.d.ts +28 -0
  34. package/src/lib/orchestrator/internals/pc-usd-oracle.js +285 -0
  35. package/src/lib/orchestrator/internals/progress-route-hooks.d.ts +23 -0
  36. package/src/lib/orchestrator/internals/progress-route-hooks.js +72 -0
  37. package/src/lib/orchestrator/internals/push-chain-tx.d.ts +27 -1
  38. package/src/lib/orchestrator/internals/push-chain-tx.js +111 -15
  39. package/src/lib/orchestrator/internals/rescue.js +1 -1
  40. package/src/lib/orchestrator/internals/response-builder.d.ts +5 -0
  41. package/src/lib/orchestrator/internals/response-builder.js +242 -66
  42. package/src/lib/orchestrator/internals/route-handlers.js +480 -206
  43. package/src/lib/orchestrator/internals/tx-transformer.js +293 -35
  44. package/src/lib/orchestrator/orchestrator.d.ts +7 -1
  45. package/src/lib/orchestrator/orchestrator.js +108 -56
  46. package/src/lib/orchestrator/orchestrator.types.d.ts +111 -5
  47. package/src/lib/orchestrator/payload-builders.d.ts +16 -0
  48. package/src/lib/orchestrator/payload-builders.js +6 -5
  49. package/src/lib/orchestrator/route-detector.js +20 -8
  50. package/src/lib/orchestrator/svm-idl/normalize-address.d.ts +19 -0
  51. package/src/lib/orchestrator/svm-idl/normalize-address.js +56 -0
  52. package/src/lib/orchestrator/svm-idl/registry.d.ts +13 -4
  53. package/src/lib/orchestrator/svm-idl/registry.js +15 -14
  54. package/src/lib/orchestrator/svm-idl/resolve.js +5 -2
  55. package/src/lib/progress-hook/progress-hook.d.ts +28 -0
  56. package/src/lib/progress-hook/progress-hook.js +552 -133
  57. package/src/lib/progress-hook/progress-hook.types.d.ts +77 -29
  58. package/src/lib/progress-hook/progress-hook.types.js +75 -35
  59. package/src/lib/push-client/push-client.d.ts +7 -0
  60. package/src/lib/push-client/push-client.js +18 -1
  61. package/src/lib/universal-tx-detector/__tests__/fixtures/builders.d.ts +32 -0
  62. package/src/lib/universal-tx-detector/__tests__/fixtures/builders.js +65 -0
  63. package/src/lib/universal-tx-detector/__tests__/fixtures/scenarios.d.ts +72 -0
  64. package/src/lib/universal-tx-detector/__tests__/fixtures/scenarios.js +669 -0
  65. package/src/lib/universal-tx-detector/auto-detect.d.ts +37 -0
  66. package/src/lib/universal-tx-detector/auto-detect.js +112 -0
  67. package/src/lib/universal-tx-detector/cascade.d.ts +64 -0
  68. package/src/lib/universal-tx-detector/cascade.js +254 -0
  69. package/src/lib/universal-tx-detector/child-inbounds.d.ts +58 -0
  70. package/src/lib/universal-tx-detector/child-inbounds.js +145 -0
  71. package/src/lib/universal-tx-detector/classify.d.ts +33 -0
  72. package/src/lib/universal-tx-detector/classify.js +228 -0
  73. package/src/lib/universal-tx-detector/detector-svm.d.ts +46 -0
  74. package/src/lib/universal-tx-detector/detector-svm.js +324 -0
  75. package/src/lib/universal-tx-detector/detector.d.ts +13 -0
  76. package/src/lib/universal-tx-detector/detector.js +233 -0
  77. package/src/lib/universal-tx-detector/events.d.ts +765 -0
  78. package/src/lib/universal-tx-detector/events.js +241 -0
  79. package/src/lib/universal-tx-detector/index.d.ts +18 -0
  80. package/src/lib/universal-tx-detector/index.js +36 -0
  81. package/src/lib/universal-tx-detector/svm-events.d.ts +41 -0
  82. package/src/lib/universal-tx-detector/svm-events.js +68 -0
  83. package/src/lib/universal-tx-detector/types.d.ts +100 -0
  84. package/src/lib/universal-tx-detector/types.js +3 -0
  85. package/src/lib/utils.d.ts +30 -13
  86. package/src/lib/utils.js +47 -21
@@ -26,6 +26,7 @@ const tslib_1 = require("tslib");
26
26
  const web3_js_1 = require("@solana/web3.js");
27
27
  const viem_1 = require("viem");
28
28
  const context_1 = require("./context");
29
+ const progress_hook_types_1 = require("../../progress-hook/progress-hook.types");
29
30
  const helpers_1 = require("./helpers");
30
31
  const payload_builder_1 = require("./payload-builder");
31
32
  const uea_manager_1 = require("./uea-manager");
@@ -40,6 +41,152 @@ const payload_builders_1 = require("../payload-builders");
40
41
  const selectors_1 = require("../../constants/selectors");
41
42
  const erc20_evm_1 = require("../../constants/abi/erc20.evm");
42
43
  const push_chain_1 = require("../../push-chain/push-chain");
44
+ // =============================================================================
45
+ // Route 2 (UEA → CEA) phase helpers
46
+ // =============================================================================
47
+ /**
48
+ * Decide which PRC-20 token to burn on Push Chain and how much of it to burn
49
+ * for a Route 2 EVM outbound call. Chain-local — does NOT require a resolved
50
+ * CEA address, so it can run before the 203 account-resolution phase.
51
+ */
52
+ function resolveR2Prc20TokenEvm(params, targetChain, pushNetwork) {
53
+ var _a;
54
+ let prc20Token = selectors_1.ZERO_ADDRESS;
55
+ let burnAmount = BigInt(0);
56
+ if (params.migration) {
57
+ // Migration is logic-only — no funds. CEA rejects msg.value != 0.
58
+ prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, pushNetwork);
59
+ burnAmount = BigInt(0);
60
+ }
61
+ else if ((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) {
62
+ // User explicitly specified funds with token
63
+ const token = params.funds.token;
64
+ if (token) {
65
+ prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
66
+ burnAmount = params.funds.amount;
67
+ }
68
+ }
69
+ else if (params.value && params.value > BigInt(0)) {
70
+ // Native value transfer: auto-select the PRC-20 token for target chain
71
+ prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, pushNetwork);
72
+ burnAmount = params.value;
73
+ }
74
+ else if (params.data) {
75
+ // PAYLOAD-only (no value transfer): still need native token for chain namespace + gas fees
76
+ prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, pushNetwork);
77
+ burnAmount = BigInt(0);
78
+ }
79
+ return { prc20Token, burnAmount };
80
+ }
81
+ /**
82
+ * SVM counterpart to `resolveR2Prc20TokenEvm`. SVM adds an EXECUTE-only
83
+ * branch (no value, no funds, but `hasSvmExecute === true`) that still needs
84
+ * a PRC-20 token for the chain-namespace lookup.
85
+ */
86
+ function resolveR2Prc20TokenSvm(params, targetChain, pushNetwork, hasSvmExecute) {
87
+ var _a;
88
+ let prc20Token = selectors_1.ZERO_ADDRESS;
89
+ let burnAmount = BigInt(0);
90
+ if ((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) {
91
+ const token = params.funds.token;
92
+ if (token) {
93
+ prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
94
+ burnAmount = params.funds.amount;
95
+ }
96
+ }
97
+ else if (params.value && params.value > BigInt(0)) {
98
+ prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, pushNetwork);
99
+ burnAmount = params.value;
100
+ }
101
+ else if (hasSvmExecute) {
102
+ const token = params.funds && params.funds.token;
103
+ if (token) {
104
+ prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
105
+ }
106
+ else {
107
+ prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, pushNetwork);
108
+ }
109
+ burnAmount = BigInt(0);
110
+ }
111
+ return { prc20Token, burnAmount };
112
+ }
113
+ /**
114
+ * Build the CEA-side multicall payload for a Route 2 EVM outbound. Needs the
115
+ * resolved `ceaAddress` because the native-value self-call branch skips the
116
+ * multicall when the target IS the CEA (avoids a CEA._handleMulticall revert).
117
+ */
118
+ function buildR2CeaPayloadEvm(params, ceaAddress, targetAddress) {
119
+ var _a, _b;
120
+ if (params.migration) {
121
+ return (0, payload_builders_1.buildMigrationPayload)();
122
+ }
123
+ const ceaMulticalls = [];
124
+ if (params.data) {
125
+ if (Array.isArray(params.data)) {
126
+ ceaMulticalls.push(...params.data);
127
+ }
128
+ else {
129
+ // When ERC-20 funds are provided with a single payload, auto-prepend a
130
+ // transfer() call so the tokens minted to the CEA are forwarded to the
131
+ // target address. Mirrors the Route 1 behavior in buildExecuteMulticall.
132
+ if ((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) {
133
+ const token = params.funds.token;
134
+ if (token && token.mechanism !== 'native') {
135
+ const erc20Transfer = (0, viem_1.encodeFunctionData)({
136
+ abi: erc20_evm_1.ERC20_EVM,
137
+ functionName: 'transfer',
138
+ args: [targetAddress, params.funds.amount],
139
+ });
140
+ ceaMulticalls.push({
141
+ to: token.address,
142
+ value: BigInt(0),
143
+ data: erc20Transfer,
144
+ });
145
+ }
146
+ }
147
+ // Single call with data. Forward native value (if any) so the target
148
+ // contract receives it alongside the payload call.
149
+ ceaMulticalls.push({
150
+ to: targetAddress,
151
+ value: (_b = params.value) !== null && _b !== void 0 ? _b : BigInt(0),
152
+ data: params.data,
153
+ });
154
+ }
155
+ }
156
+ else if (params.value) {
157
+ // Native value transfer only. Skip the multicall when sending to the CEA
158
+ // itself — a value-bearing self-call would revert in CEA._handleMulticall.
159
+ if (targetAddress.toLowerCase() !== ceaAddress.toLowerCase()) {
160
+ ceaMulticalls.push({
161
+ to: targetAddress,
162
+ value: params.value,
163
+ data: '0x',
164
+ });
165
+ }
166
+ }
167
+ return (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
168
+ }
169
+ const SIZER_HOOK_BY_ROUTE = {
170
+ R3: {
171
+ A: progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_03_01,
172
+ B: progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_03_02,
173
+ C: progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_03_03,
174
+ },
175
+ };
176
+ /**
177
+ * Fire the route-scoped sizing progress hook based on the sizer's decision.
178
+ * Emits the consumer-facing shape { gasRequired, extraDepositPC, totalDepositUSD, chain }.
179
+ * - gasRequired = sizer's gas leg in native PC wei
180
+ * - extraDepositPC = overflow in native PC wei (> 0 only for Case C)
181
+ * - totalDepositUSD = sized total in USD (8-dec): $1 for Case A (padded),
182
+ * raw gasUsd for Case B/C (which for Case C equals gasLegUsd + overflowUsd).
183
+ */
184
+ function fireSizingHook(ctx, route, chain, sizing) {
185
+ const hook = SIZER_HOOK_BY_ROUTE[route][sizing.category];
186
+ const ONE_USD_8D = BigInt(100000000);
187
+ const totalDepositUSD = sizing.category === 'A' ? ONE_USD_8D : sizing.gasUsd;
188
+ (0, context_1.fireProgressHook)(ctx, hook, chain, sizing.gasLegNativePc, sizing.overflowNativePc, totalDepositUSD);
189
+ }
43
190
  // ---------------------------------------------------------------------------
44
191
  // executeMultiChain
45
192
  // ---------------------------------------------------------------------------
@@ -100,7 +247,7 @@ function executeMultiChain(ctx, params, executeFn) {
100
247
  */
101
248
  function executeUoaToCea(ctx, params, executeFn) {
102
249
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
103
- var _a, _b, _c, _d, _e;
250
+ var _a, _b, _c;
104
251
  const target = params.to;
105
252
  const targetChain = target.chain;
106
253
  const targetAddress = target.address;
@@ -137,100 +284,63 @@ function executeUoaToCea(ctx, params, executeFn) {
137
284
  return executeUoaToCeaSvm(ctx, params, target, executeFn);
138
285
  }
139
286
  // ===== EVM path (existing logic) =====
140
- // Get UEA address
287
+ // R2 pre-broadcast progress: external chain detected
288
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_201, targetChain, targetAddress);
289
+ // Compute UEA address (sync, offchain derivation — no RPC).
141
290
  const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
142
291
  (0, context_1.printLog)(ctx, `executeUoaToCea — target chain: ${targetChain}, target address: ${targetAddress}, UEA: ${ueaAddress}`);
143
- // Get CEA address for this UEA on target chain
144
- const { cea: ceaAddress, isDeployed: ceaDeployed } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_a = ctx.rpcUrls[targetChain]) === null || _a === void 0 ? void 0 : _a[0]);
145
- (0, context_1.printLog)(ctx, `executeUoaToCea — CEA address: ${ceaAddress}, deployed: ${ceaDeployed}`);
146
- // Migration path: raw MIGRATION_SELECTOR payload, no multicall wrapping
147
- let ceaPayload;
148
- let prc20Token = selectors_1.ZERO_ADDRESS;
149
- let burnAmount = BigInt(0);
150
- if (params.migration) {
151
- ceaPayload = (0, payload_builders_1.buildMigrationPayload)();
152
- prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, ctx.pushNetwork);
153
- burnAmount = BigInt(0); // Migration is logic-only — no funds. CEA rejects msg.value != 0.
154
- (0, context_1.printLog)(ctx, `executeUoaToCea — MIGRATION: using raw MIGRATION_SELECTOR payload (${ceaPayload}), native PRC-20 ${prc20Token}`);
292
+ // --- Token resolution (chain-local, before account resolution) ---
293
+ // Resolve the PRC-20 burn token + burn amount ahead of CEA resolution.
294
+ // Chain-local — see `resolveR2Prc20TokenEvm` above.
295
+ const { prc20Token, burnAmount } = resolveR2Prc20TokenEvm(params, targetChain, ctx.pushNetwork);
296
+ if (params.value && params.value > BigInt(0)) {
297
+ (0, context_1.printLog)(ctx, `executeUoaToCea — auto-selected native PRC-20 ${prc20Token} for chain ${targetChain}, amount: ${burnAmount.toString()}`);
155
298
  }
156
- else {
157
- // Build multicall for CEA execution on target chain
158
- const ceaMulticalls = [];
159
- // If there's data to execute on target
160
- if (params.data) {
161
- if (Array.isArray(params.data)) {
162
- // User provided explicit multicall array
163
- ceaMulticalls.push(...params.data);
164
- }
165
- else {
166
- // When ERC-20 funds are provided with a single payload, auto-prepend a
167
- // transfer() call so the tokens minted to the CEA are forwarded to the
168
- // target address. This mirrors the Route 1 behavior in buildExecuteMulticall.
169
- if ((_b = params.funds) === null || _b === void 0 ? void 0 : _b.amount) {
170
- const token = params.funds.token;
171
- if (token && token.mechanism !== 'native') {
172
- const erc20Transfer = (0, viem_1.encodeFunctionData)({
173
- abi: erc20_evm_1.ERC20_EVM,
174
- functionName: 'transfer',
175
- args: [targetAddress, params.funds.amount],
176
- });
177
- ceaMulticalls.push({
178
- to: token.address,
179
- value: BigInt(0),
180
- data: erc20Transfer,
181
- });
182
- }
183
- }
184
- // Single call with data. Forward native value (if any) so the target
185
- // contract receives it alongside the payload call. The vault deposits
186
- // native value to the CEA, and the multicall forwards it to the target.
187
- ceaMulticalls.push({
188
- to: targetAddress,
189
- value: (_c = params.value) !== null && _c !== void 0 ? _c : BigInt(0),
190
- data: params.data,
191
- });
192
- }
193
- }
194
- else if (params.value) {
195
- // Native value transfer only.
196
- // If sending to the CEA itself, skip the multicall — the gateway deposits native
197
- // value directly to CEA. A self-call with value would revert (CEA._handleMulticall
198
- // rejects value-bearing self-calls).
199
- if (targetAddress.toLowerCase() !== ceaAddress.toLowerCase()) {
200
- ceaMulticalls.push({
201
- to: targetAddress,
202
- value: params.value,
203
- data: '0x',
204
- });
205
- }
206
- }
207
- // Build CEA multicall payload (this is what gets executed on the external chain)
208
- ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
209
- // Determine token to burn on Push Chain
210
- // NOTE: Even for PAYLOAD-only (no value), we need a valid PRC-20 token to:
211
- // 1. Look up the target chain namespace in the gateway
212
- // 2. Query and pay gas fees for the relay
213
- if ((_d = params.funds) === null || _d === void 0 ? void 0 : _d.amount) {
214
- // User explicitly specified funds with token
215
- const token = params.funds.token;
216
- if (token) {
217
- prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
218
- burnAmount = params.funds.amount;
219
- }
220
- }
221
- else if (params.value && params.value > BigInt(0)) {
222
- // Native value transfer: auto-select the PRC-20 token for target chain
223
- prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, ctx.pushNetwork);
224
- burnAmount = params.value;
225
- (0, context_1.printLog)(ctx, `executeUoaToCea — auto-selected native PRC-20 ${prc20Token} for chain ${targetChain}, amount: ${burnAmount.toString()}`);
299
+ else if (!params.migration && !((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) && params.data) {
300
+ (0, context_1.printLog)(ctx, `executeUoaToCea — PAYLOAD-only: using native PRC-20 ${prc20Token} for chain ${targetChain} with zero burn amount`);
301
+ }
302
+ const gasLimitForQuery = (_b = params.gasLimit) !== null && _b !== void 0 ? _b : BigInt(0);
303
+ // --- 202: Gas estimation (spec-ordered before 203) ---
304
+ let gasFee = BigInt(0);
305
+ let protocolFee = BigInt(0);
306
+ let gasToken = selectors_1.ZERO_ADDRESS;
307
+ let universalCoreAddress;
308
+ if (prc20Token !== selectors_1.ZERO_ADDRESS) {
309
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_01, targetChain);
310
+ try {
311
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimitForQuery, targetChain);
312
+ gasFee = result.gasFee;
313
+ protocolFee = result.protocolFee;
314
+ gasToken = result.gasToken;
315
+ universalCoreAddress = result.universalCoreAddress;
316
+ (0, context_1.printLog)(ctx, `executeUoaToCea — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}`);
317
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_02, targetChain, protocolFee, gasFee);
226
318
  }
227
- else if (params.data) {
228
- // PAYLOAD-only (no value transfer): still need native token for chain namespace + gas fees
229
- prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, ctx.pushNetwork);
230
- burnAmount = BigInt(0);
231
- (0, context_1.printLog)(ctx, `executeUoaToCea — PAYLOAD-only: using native PRC-20 ${prc20Token} for chain ${targetChain} with zero burn amount`);
319
+ catch (err) {
320
+ throw new Error(`Failed to query outbound gas fee: ${err}`);
232
321
  }
233
322
  }
323
+ // --- 203: CEA resolution + UEA status (single round-trip burst) ---
324
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_01, targetChain);
325
+ const signerChain = ctx.universalSigner.account.chain;
326
+ const isNativePushEOA = (0, helpers_1.isPushChain)(signerChain);
327
+ const [{ cea: ceaAddress, isDeployed: ceaDeployed }, [ueaCode, ueaBalance],] = yield Promise.all([
328
+ (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_c = ctx.rpcUrls[targetChain]) === null || _c === void 0 ? void 0 : _c[0]),
329
+ Promise.all([
330
+ ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
331
+ ctx.pushClient.getBalance(ueaAddress),
332
+ ]),
333
+ ]);
334
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_02, ueaAddress, ceaAddress, targetChain, ceaDeployed);
335
+ (0, context_1.printLog)(ctx, `executeUoaToCea — CEA address: ${ceaAddress}, deployed: ${ceaDeployed}`);
336
+ const isUEADeployed = isNativePushEOA || ueaCode !== undefined;
337
+ const ueaNonce = (!isUEADeployed || isNativePushEOA) ? BigInt(0) : yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
338
+ // --- Build CEA payload (requires ceaAddress for self-call check) ---
339
+ // Delegates to `buildR2CeaPayloadEvm` — see helper definition above.
340
+ const ceaPayload = buildR2CeaPayloadEvm(params, ceaAddress, targetAddress);
341
+ if (params.migration) {
342
+ (0, context_1.printLog)(ctx, `executeUoaToCea — MIGRATION: using raw MIGRATION_SELECTOR payload (${ceaPayload}), native PRC-20 ${prc20Token}`);
343
+ }
234
344
  // Build outbound request struct for the gateway
235
345
  // NOTE: `target` is a LEGACY/DUMMY parameter for contract compatibility.
236
346
  // The deployed UniversalGatewayPC still expects this field, but the relay does NOT use it
@@ -238,7 +348,7 @@ function executeUoaToCea(ctx, params, executeFn) {
238
348
  // SOURCE_CHAIN_NAMESPACE. We pass the CEA address as a non-zero placeholder.
239
349
  // This field will be removed in future contract upgrades.
240
350
  const targetBytes = ceaAddress; // Dummy value - any non-zero address works
241
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_e = params.gasLimit) !== null && _e !== void 0 ? _e : BigInt(0), ceaPayload, ueaAddress // revert recipient is the UEA
351
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, gasLimitForQuery, ceaPayload, ueaAddress // revert recipient is the UEA
242
352
  );
243
353
  (0, context_1.printLog)(ctx, `executeUoaToCea — outbound request: ${JSON.stringify({
244
354
  target: outboundReq.target,
@@ -250,55 +360,32 @@ function executeUoaToCea(ctx, params, executeFn) {
250
360
  }, null, 2)}`);
251
361
  // Get UniversalGatewayPC address
252
362
  const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
253
- // Pre-fetch UEA status early — balance is needed for gas value calculation
254
- const signerChain = ctx.universalSigner.account.chain;
255
- const isNativePushEOA = (0, helpers_1.isPushChain)(signerChain);
256
- const [ueaCode, ueaBalance] = yield Promise.all([
257
- ctx.pushClient.publicClient.getCode({ address: ueaAddress }),
258
- ctx.pushClient.getBalance(ueaAddress),
259
- ]);
260
- const isUEADeployed = isNativePushEOA || ueaCode !== undefined;
261
- const ueaNonce = (!isUEADeployed || isNativePushEOA) ? BigInt(0) : yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
262
363
  // Build the multicall that will execute ON Push Chain from UEA context
263
364
  // This includes: 1) approve PRC-20 (if needed), 2) call sendUniversalTxOutbound
264
365
  const pushChainMulticalls = [];
265
- // Query gas fee from UniversalCore contract (needed for approval amount)
266
- let gasFee = BigInt(0);
267
- let nativeValueForGas = BigInt(0);
268
- let gasToken = selectors_1.ZERO_ADDRESS;
269
- let universalCoreAddress;
270
- if (prc20Token !== selectors_1.ZERO_ADDRESS) {
271
- try {
272
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundReq.gasLimit);
273
- gasFee = result.gasFee;
274
- gasToken = result.gasToken;
275
- nativeValueForGas = result.nativeValueForGas;
276
- universalCoreAddress = result.universalCoreAddress;
277
- (0, context_1.printLog)(ctx, `executeUoaToCea — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}`);
278
- }
279
- catch (err) {
280
- throw new Error(`Failed to query outbound gas fee: ${err}`);
281
- }
282
- }
283
- // Balance-aware adjustment: MAXIMIZE nativeValueForGas within available balance.
284
- // The contract's swapAndBurnGas does exactOutputSingle and refunds excess PC,
285
- // so overshooting within balance is safe. Undershooting causes the swap to revert.
286
- const EVM_NATIVE_VALUE_TARGET = BigInt(200e18); // 200 UPC
287
- const EVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for tx overhead
366
+ // ---------------------------------------------------------------------
367
+ // Native-value selection — R2 does NOT use Case A/B/C gas abstraction.
368
+ // That feature is scoped to R1 (fee-lock USD caps) and R3 (CEA → Push
369
+ // outbound msg.value). For R2 we size `nativeValueForGas` from the
370
+ // live WPC/gasToken Uniswap V3 pool quote plus a 10% safety buffer;
371
+ // `swapAndBurnGas` refunds any excess as PC back to UEA via
372
+ // refundUnusedGas.
373
+ //
374
+ // Fresh-wallet prediction (below) is retained: it estimates post-fee-lock
375
+ // UPC balance for UEAs that haven't been funded yet, independent of
376
+ // Case A/B/C sizing.
377
+ // ---------------------------------------------------------------------
378
+ const EVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for outer-tx gas
379
+ const EVM_NATIVE_VALUE_SAFETY_CAP = BigInt(200e18); // 200 UPC absolute ceiling
288
380
  const ROUTE2_MINIMUM_DEPOSIT_USD = utils_1.Utils.helpers.parseUnits('10', 8); // $10
289
- // Determine effective balance for the adjustment.
290
- // For deployed UEAs: use real balance.
291
- // For fresh wallets: predict the post-fee-locking balance by querying the same
292
- // Uniswap V3 quoter the chain uses (pETH → WPC swap in execute_inbound_gas.go).
381
+ // Effective balance: real for deployed UEAs; predicted post-fee-lock for fresh.
293
382
  let effectiveBalance = ueaBalance;
294
383
  if (!isUEADeployed && effectiveBalance <= EVM_GAS_RESERVE) {
295
- // Calculate the ETH amount lockFee will send (same formula as lockFee)
296
384
  const ethPrice = yield new price_fetch_1.PriceFetch(ctx.rpcUrls).getPrice(signerChain);
297
385
  const nativeAmountETH = ethPrice > BigInt(0)
298
386
  ? (ROUTE2_MINIMUM_DEPOSIT_USD * BigInt(1e18) + (ethPrice - BigInt(1))) / ethPrice + BigInt(1)
299
387
  : BigInt(0);
300
388
  if (nativeAmountETH > BigInt(0)) {
301
- // Query the real swap quote: how much WPC (UPC) for this pETH amount?
302
389
  const originPrc20 = (0, helpers_1.getNativePRC20ForChain)(signerChain, ctx.pushNetwork);
303
390
  const predictedUPC = yield (0, gas_calculator_1.estimateDepositFromLockedNative)(ctx, nativeAmountETH, originPrc20);
304
391
  if (predictedUPC > BigInt(0)) {
@@ -309,15 +396,35 @@ function executeUoaToCea(ctx, params, executeFn) {
309
396
  }
310
397
  }
311
398
  }
399
+ // Pool-quote + 10% safety buffer. Over-send is refunded by the contract.
400
+ let nativeValueForGas = BigInt(0);
401
+ if (universalCoreAddress && gasFee > BigInt(0)) {
402
+ const estimated = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, effectiveBalance);
403
+ nativeValueForGas = (estimated * BigInt(110)) / BigInt(100);
404
+ (0, context_1.printLog)(ctx, `executeUoaToCea — nativeValueForGas: pool-quote=${estimated.toString()}, with 10% buffer=${nativeValueForGas.toString()}`);
405
+ }
406
+ // Hard safety cap so a broken pool price can't drain the UEA.
407
+ if (nativeValueForGas > EVM_NATIVE_VALUE_SAFETY_CAP) {
408
+ (0, context_1.printLog)(ctx, `executeUoaToCea — capping nativeValueForGas at 200 UPC ceiling (was ${nativeValueForGas.toString()})`);
409
+ nativeValueForGas = EVM_NATIVE_VALUE_SAFETY_CAP;
410
+ }
312
411
  let adjustedValue;
313
- if (effectiveBalance > EVM_NATIVE_VALUE_TARGET + EVM_GAS_RESERVE) {
314
- adjustedValue = EVM_NATIVE_VALUE_TARGET;
412
+ if (effectiveBalance >= nativeValueForGas + EVM_GAS_RESERVE) {
413
+ adjustedValue = nativeValueForGas;
315
414
  }
316
415
  else if (effectiveBalance > EVM_GAS_RESERVE) {
416
+ // Balance-starved: send everything we can afford. swapAndBurnGas
417
+ // refunds unused; undershoot will revert with a concrete error.
317
418
  adjustedValue = effectiveBalance - EVM_GAS_RESERVE;
318
419
  }
420
+ else if (effectiveBalance > BigInt(0)) {
421
+ // Drained UEA: overshoot would return (false, "") and surface as an
422
+ // opaque ExecutionFailed() — clamp to what we have.
423
+ adjustedValue = effectiveBalance < nativeValueForGas ? effectiveBalance : nativeValueForGas;
424
+ }
319
425
  else {
320
- adjustedValue = nativeValueForGas;
426
+ throw new Error(`UEA ${ueaAddress} has zero UPC balance; cannot fund outbound gas swap. ` +
427
+ `Bridge UPC to the UEA before retrying.`);
321
428
  }
322
429
  if (adjustedValue !== nativeValueForGas) {
323
430
  (0, context_1.printLog)(ctx, `executeUoaToCea — adjusting nativeValueForGas from ${nativeValueForGas.toString()} ` +
@@ -370,7 +477,27 @@ function executeUoaToCea(ctx, params, executeFn) {
370
477
  nonce: ueaNonce,
371
478
  balance: ueaBalance,
372
479
  }, _skipFeeLocking: isUEADeployed }, (!isUEADeployed ? { _minimumDepositUsd: ROUTE2_MINIMUM_DEPOSIT_USD } : {}));
373
- const response = yield executeFn(executeParams);
480
+ // Signature request — user is prompted to sign the universal payload.
481
+ // executeFn is monolithic (sign → broadcast), so we emit the logical
482
+ // post-sign / pre-broadcast markers as a burst after it returns so
483
+ // consumers see the spec-ordered stream: 204-01 → 204-02 → 204-03 → 207.
484
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_01);
485
+ let response;
486
+ try {
487
+ response = yield executeFn(executeParams);
488
+ }
489
+ catch (err) {
490
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_04, err instanceof Error ? err.message : String(err));
491
+ // Suppress the outer orchestrator catch's 299-02 terminal — 204-04
492
+ // already surfaced the signature/broadcast failure to the consumer.
493
+ ctx._routeTerminalEmitted = true;
494
+ throw err;
495
+ }
496
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_02);
497
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_03);
498
+ // R2 broadcast marker — Push Chain tx has been built, signed, and
499
+ // accepted by the node. Replaces the suppressed 107 emission for this route.
500
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_207, targetChain);
374
501
  // Add chain info to response
375
502
  response.chain = targetChain;
376
503
  response.chainNamespace = (0, helpers_1.getChainNamespace)(targetChain);
@@ -394,6 +521,8 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
394
521
  const targetChain = target.chain;
395
522
  const targetAddress = target.address; // 0x-prefixed, 32 bytes
396
523
  const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
524
+ // R2 pre-broadcast progress (SVM) — external chain detected
525
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_201, targetChain, targetAddress);
397
526
  const { svmPayload, targetBytes, hasExecute: hasSvmExecute, } = (0, build_payload_1.buildSvmPayloadFromParams)({
398
527
  data: params.data,
399
528
  to: target,
@@ -404,38 +533,55 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
404
533
  if (hasSvmExecute) {
405
534
  (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — encoded execute payload: ${(svmPayload.length - 2) / 2} bytes`);
406
535
  }
407
- // --- Determine PRC-20 token and burn amount ---
408
- let prc20Token = selectors_1.ZERO_ADDRESS;
409
- let burnAmount = BigInt(0);
410
- if ((_c = params.funds) === null || _c === void 0 ? void 0 : _c.amount) {
411
- // User explicitly specified funds with token
412
- const token = params.funds.token;
413
- if (token) {
414
- prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
415
- burnAmount = params.funds.amount;
416
- }
417
- }
418
- else if (params.value && params.value > BigInt(0)) {
419
- // Native value transfer: auto-select pSOL for Solana chains
420
- prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, ctx.pushNetwork);
421
- burnAmount = params.value;
536
+ // --- Determine PRC-20 token and burn amount (chain-local, pre-203) ---
537
+ const { prc20Token, burnAmount } = resolveR2Prc20TokenSvm(params, targetChain, ctx.pushNetwork, hasSvmExecute);
538
+ if (params.value && params.value > BigInt(0)) {
422
539
  (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — auto-selected native PRC-20 ${prc20Token} for chain ${targetChain}, amount: ${burnAmount.toString()}`);
423
540
  }
424
- else if (hasSvmExecute) {
425
- // Execute-only (no value): check if user specified an SPL token context
426
- const token = params.funds && params.funds.token;
427
- if (token) {
428
- prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token);
541
+ else if (!((_c = params.funds) === null || _c === void 0 ? void 0 : _c.amount) && hasSvmExecute) {
542
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — EXECUTE-only: using PRC-20 ${prc20Token} with zero burn amount`);
543
+ }
544
+ // --- 202: Gas estimation (spec-ordered before 203) ---
545
+ let gasFee = BigInt(0);
546
+ let protocolFeeSvm = BigInt(0);
547
+ let gasToken = selectors_1.ZERO_ADDRESS;
548
+ let universalCoreAddress;
549
+ // Effective gas limit seeded from the user param. When the user omits
550
+ // gasLimit, it'll be derived from the gasFee/gasPrice response so the
551
+ // on-chain record carries a non-zero compute budget the relay can use.
552
+ let effectiveGasLimit = (_d = params.gasLimit) !== null && _d !== void 0 ? _d : BigInt(0);
553
+ if (prc20Token !== selectors_1.ZERO_ADDRESS) {
554
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_01, targetChain);
555
+ try {
556
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, effectiveGasLimit, targetChain);
557
+ gasFee = result.gasFee;
558
+ protocolFeeSvm = result.protocolFee;
559
+ gasToken = result.gasToken;
560
+ universalCoreAddress = result.universalCoreAddress;
561
+ // When user omits gasLimit (sent as 0), the contract computes fees using its internal
562
+ // baseGasLimitByChainNamespace. But the relay reads the stored gasLimit=0 from the
563
+ // on-chain outbound record and uses it as the Solana compute budget — 0 CU means the
564
+ // relay cannot execute the tx. Derive the effective limit from gasFee/gasPrice so
565
+ // the stored record has a non-zero compute budget the relay can use.
566
+ if (!params.gasLimit && result.gasPrice > BigInt(0)) {
567
+ effectiveGasLimit = result.gasFee / result.gasPrice;
568
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — derived effectiveGasLimit: ${effectiveGasLimit} (gasFee=${result.gasFee} / gasPrice=${result.gasPrice})`);
569
+ }
570
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}`);
571
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_02, targetChain, protocolFeeSvm, gasFee);
429
572
  }
430
- else {
431
- prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, ctx.pushNetwork);
573
+ catch (err) {
574
+ throw new Error(`Failed to query outbound gas fee: ${err}`);
432
575
  }
433
- burnAmount = BigInt(0);
434
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — EXECUTE-only: using PRC-20 ${prc20Token} with zero burn amount`);
435
576
  }
577
+ // --- 203: Execution-account resolution (SVM has a trivial CEA — the
578
+ // target address IS the CEA, no async lookup needed). Fire the burst
579
+ // after 202 to keep the progress-hook stream in spec order. ---
580
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_01, targetChain);
581
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_02, ueaAddress, targetAddress, targetChain, true);
436
582
  // --- Build outbound request ---
437
583
  // targetBytes: for execute = program id (resolver output); for withdraw = to.address
438
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, (_d = params.gasLimit) !== null && _d !== void 0 ? _d : BigInt(0), svmPayload, ueaAddress // revert recipient is the UEA
584
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, effectiveGasLimit, svmPayload, ueaAddress // revert recipient is the UEA
439
585
  );
440
586
  (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — outbound request: ${JSON.stringify({
441
587
  target: outboundReq.target,
@@ -445,7 +591,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
445
591
  payloadLength: (outboundReq.payload.length - 2) / 2,
446
592
  revertRecipient: outboundReq.revertRecipient,
447
593
  }, null, 2)}`);
448
- // --- Pre-fetch UEA status early — balance is needed for gas value calculation ---
594
+ // --- Pre-fetch UEA status — balance is needed for gas value calculation ---
449
595
  const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
450
596
  const signerChainSvm = ctx.universalSigner.account.chain;
451
597
  const isNativePushEOASvm = (0, helpers_1.isPushChain)(signerChainSvm);
@@ -466,45 +612,43 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
466
612
  ueaBalance = balance;
467
613
  }
468
614
  const ueaNonce = (!isUEADeployed || isNativePushEOASvm) ? BigInt(0) : yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
469
- // --- Query gas fee (identical to EVM path) ---
470
- let gasFee = BigInt(0);
471
- let gasToken = selectors_1.ZERO_ADDRESS;
472
- let universalCoreAddress;
615
+ // R2 does NOT use Case A/B/C gas abstraction (scoped to R1 + R3 only).
616
+ // Size msg.value from the live WPC/gasToken Uniswap V3 pool quote plus
617
+ // a 10% safety buffer. swapAndBurnGas refunds the excess as PC via
618
+ // refundUnusedGas.
619
+ //
620
+ // SVM pools (WPC/pSOL) are thinner than EVM pools so the pool-quote can
621
+ // easily exceed the UEA's balance. Apply a balance-aware clamp so the
622
+ // outer tx doesn't fail with InsufficientFunds before the swap runs.
623
+ // No hard safety cap here — pSOL pool skew means even legitimate swaps
624
+ // can need hundreds of PC; capping would trigger Uniswap STF reverts.
625
+ const SVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for outer-tx gas
473
626
  let nativeValueForGas = BigInt(0);
474
- if (prc20Token !== selectors_1.ZERO_ADDRESS) {
475
- const gasLimit = outboundReq.gasLimit;
476
- try {
477
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
478
- gasFee = result.gasFee;
479
- gasToken = result.gasToken;
480
- nativeValueForGas = result.nativeValueForGas;
481
- universalCoreAddress = result.universalCoreAddress;
482
- // When user omits gasLimit (sent as 0), the contract computes fees using its internal
483
- // baseGasLimitByChainNamespace. But the relay reads the stored gasLimit=0 from the
484
- // on-chain outbound record and uses it as the Solana compute budget — 0 CU means the
485
- // relay cannot execute the tx. Derive the effective limit from gasFee/gasPrice so
486
- // the stored record has a non-zero compute budget the relay can use.
487
- if (!params.gasLimit && result.gasPrice > BigInt(0)) {
488
- outboundReq.gasLimit = result.gasFee / result.gasPrice;
489
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — derived effectiveGasLimit: ${outboundReq.gasLimit} (gasFee=${result.gasFee} / gasPrice=${result.gasPrice})`);
490
- }
491
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}`);
492
- }
493
- catch (err) {
494
- throw new Error(`Failed to query outbound gas fee: ${err}`);
495
- }
496
- }
497
- // Estimate the actual WPC needed for the swap using on-chain pool price.
498
- // The static 1Mx multiplier from queryOutboundGasFee doesn't reflect the real
499
- // WPC/gasToken exchange rate. estimateNativeValueForSwap reads the Uniswap V3
500
- // pool's slot0, computes the true cost, and adds a 2x buffer.
501
- // Excess msg.value is refunded by the contract's swapAndBurnGas.
502
627
  if (universalCoreAddress && gasFee > BigInt(0)) {
503
628
  const estimated = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, ueaBalance);
504
- if (estimated > nativeValueForGas) {
505
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${estimated.toString()} (pool-price estimate, UEA balance: ${ueaBalance.toString()})`);
506
- nativeValueForGas = estimated;
507
- }
629
+ nativeValueForGas = (estimated * BigInt(110)) / BigInt(100);
630
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — nativeValueForGas: pool-quote=${estimated.toString()}, with 10% buffer=${nativeValueForGas.toString()}`);
631
+ }
632
+ // Balance-aware clamp: if UEA balance can't cover target + gas reserve,
633
+ // send only what we can afford. swapAndBurnGas refunds any excess.
634
+ let adjustedValueSvm;
635
+ if (ueaBalance >= nativeValueForGas + SVM_GAS_RESERVE) {
636
+ adjustedValueSvm = nativeValueForGas;
637
+ }
638
+ else if (ueaBalance > SVM_GAS_RESERVE) {
639
+ adjustedValueSvm = ueaBalance - SVM_GAS_RESERVE;
640
+ }
641
+ else if (ueaBalance > BigInt(0)) {
642
+ adjustedValueSvm = ueaBalance < nativeValueForGas ? ueaBalance : nativeValueForGas;
643
+ }
644
+ else {
645
+ throw new Error(`UEA ${ueaAddress} has zero UPC balance; cannot fund outbound gas swap. ` +
646
+ `Bridge UPC to the UEA before retrying.`);
647
+ }
648
+ if (adjustedValueSvm !== nativeValueForGas) {
649
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — adjusting nativeValueForGas from ${nativeValueForGas.toString()} ` +
650
+ `to ${adjustedValueSvm.toString()} (UEA balance: ${ueaBalance.toString()})`);
651
+ nativeValueForGas = adjustedValueSvm;
508
652
  }
509
653
  // --- Build Push Chain multicalls (approve + sendUniversalTxOutbound) ---
510
654
  // Reuse the same builder as EVM — this part is identical
@@ -531,7 +675,23 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
531
675
  },
532
676
  _skipFeeLocking: isUEADeployed, // skip fee-locking only if UEA is already deployed
533
677
  };
534
- const response = yield executeFn(executeParams);
678
+ // Signature request — user is prompted to sign the universal payload.
679
+ // Emit spec-ordered burst after executeFn returns (sign → verify → broadcast).
680
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_01);
681
+ let response;
682
+ try {
683
+ response = yield executeFn(executeParams);
684
+ }
685
+ catch (err) {
686
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_04, err instanceof Error ? err.message : String(err));
687
+ // Suppress the outer orchestrator catch's 299-02 terminal.
688
+ ctx._routeTerminalEmitted = true;
689
+ throw err;
690
+ }
691
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_02);
692
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_03);
693
+ // R2 broadcast marker (SVM)
694
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_207, targetChain);
535
695
  // Add chain info to response
536
696
  response.chain = targetChain;
537
697
  response.chainNamespace = (0, helpers_1.getChainNamespace)(targetChain);
@@ -554,7 +714,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
554
714
  */
555
715
  function executeCeaToPush(ctx, params, executeFn) {
556
716
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
557
- var _a, _b, _c, _d, _e, _f;
717
+ var _a, _b, _c, _d, _e, _f, _g, _h;
558
718
  // 1. Validate and extract source chain
559
719
  if (!((_a = params.from) === null || _a === void 0 ? void 0 : _a.chain)) {
560
720
  throw new Error('Route 3 (CEA -> Push) requires from.chain to specify the source CEA chain');
@@ -571,9 +731,23 @@ function executeCeaToPush(ctx, params, executeFn) {
571
731
  throw new Error('Route 3 (CEA -> Push): to must be a Push Chain address (string), not a ChainTarget');
572
732
  }
573
733
  // 3. Get UEA address (will be recipient on Push Chain from CEA's perspective)
574
- const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
575
734
  // 4. Get CEA address on source chain
576
- const { cea: ceaAddress, isDeployed: ceaDeployed } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_b = ctx.rpcUrls[sourceChain]) === null || _b === void 0 ? void 0 : _b[0]);
735
+ // Both happen before the 301 hook fires — wrap them so a pre-sign RPC
736
+ // failure produces a descriptive message in the subsequent 399-02 body
737
+ // instead of a bare "failed to fetch" surface.
738
+ let ueaAddress;
739
+ let ceaAddress;
740
+ let ceaDeployed;
741
+ try {
742
+ ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
743
+ const ceaResult = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_b = ctx.rpcUrls[sourceChain]) === null || _b === void 0 ? void 0 : _b[0]);
744
+ ceaAddress = ceaResult.cea;
745
+ ceaDeployed = ceaResult.isDeployed;
746
+ }
747
+ catch (err) {
748
+ throw new Error(`Route 3 setup failed: could not resolve CEA on ${sourceChain}: ${err instanceof Error ? err.message : String(err)}`);
749
+ }
750
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_301, sourceChain, ceaAddress);
577
751
  (0, context_1.printLog)(ctx, `executeCeaToPush — sourceChain: ${sourceChain}, CEA: ${ceaAddress}, deployed: ${ceaDeployed}`);
578
752
  // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
579
753
  // if CEA doesn't exist yet. No SDK-side blocking needed.
@@ -723,21 +897,46 @@ function executeCeaToPush(ctx, params, executeFn) {
723
897
  const ueaBalance = yield ctx.pushClient.getBalance(ueaAddress);
724
898
  // 11. Query gas fees from UniversalCore
725
899
  let gasFee = BigInt(0);
900
+ let protocolFeeR3 = BigInt(0);
726
901
  let nativeValueForGas = BigInt(0);
727
902
  let gasToken = selectors_1.ZERO_ADDRESS;
903
+ let sizingDecisionR3;
728
904
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
729
905
  const gasLimit = outboundReq.gasLimit;
906
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01, sourceChain);
730
907
  try {
731
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
908
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, sourceChain);
732
909
  gasToken = result.gasToken;
733
910
  gasFee = result.gasFee;
911
+ protocolFeeR3 = result.protocolFee;
734
912
  nativeValueForGas = result.nativeValueForGas;
735
- (0, context_1.printLog)(ctx, `executeCeaToPush — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}`);
913
+ sizingDecisionR3 = result.sizing;
914
+ (0, context_1.printLog)(ctx, `executeCeaToPush — queried gas fee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}, sizing=${(_h = (_g = result.sizing) === null || _g === void 0 ? void 0 : _g.category) !== null && _h !== void 0 ? _h : 'legacy'}`);
915
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02, sourceChain, protocolFeeR3, gasFee);
736
916
  }
737
917
  catch (err) {
738
918
  throw new Error(`Failed to query outbound gas fee for Route 3: ${err}`);
739
919
  }
740
920
  }
921
+ // Account-resolution checkpoint fires before the gas-sizing sub-hook so
922
+ // consumers see the full account-resolution phase (303-xx) complete before
923
+ // the sizing decision is surfaced.
924
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_01, sourceChain);
925
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_02, ueaAddress, ceaAddress, sourceChain);
926
+ if (sizingDecisionR3) {
927
+ fireSizingHook(ctx, 'R3', sourceChain, sizingDecisionR3);
928
+ }
929
+ // R3 Case C: unlike R2, R3 has no destination funds-delivery semantic
930
+ // (CEA self-executes and bridges funds BACK to Push Chain). When the
931
+ // sizer returns Case C, we simply top up msg.value with the overflow so
932
+ // `swapAndBurnGas` can afford the full gas cost on destination. No
933
+ // bridge-swap entries, no fold-in — contract refunds any excess.
934
+ if ((sizingDecisionR3 === null || sizingDecisionR3 === void 0 ? void 0 : sizingDecisionR3.category) === 'C' &&
935
+ sizingDecisionR3.overflowNativePc > BigInt(0)) {
936
+ const bumped = nativeValueForGas + sizingDecisionR3.overflowNativePc;
937
+ (0, context_1.printLog)(ctx, `executeCeaToPush — Case C: bumping nativeValueForGas from ${nativeValueForGas} to ${bumped} (overflow=${sizingDecisionR3.overflowNativePc})`);
938
+ nativeValueForGas = bumped;
939
+ }
741
940
  // Adjust nativeValueForGas using UEA balance (contract refunds excess)
742
941
  // Reuse ueaBalance from line 978 — no tx sent between, balance is stable
743
942
  const currentBalance = ueaBalance;
@@ -775,8 +974,15 @@ function executeCeaToPush(ctx, params, executeFn) {
775
974
  else if (effectiveBalance > OUTBOUND_GAS_RESERVE_R3) {
776
975
  adjustedValue = effectiveBalance - OUTBOUND_GAS_RESERVE_R3;
777
976
  }
977
+ else if (effectiveBalance > BigInt(0)) {
978
+ // Drained UEA: overshoot would make the inner .call{value:...} return (false, "")
979
+ // and the UEA reverts with opaque ExecutionFailed(). Clamp to whatever we have so
980
+ // the gateway swap either succeeds or surfaces a concrete revert reason.
981
+ adjustedValue = effectiveBalance < nativeValueForGas ? effectiveBalance : nativeValueForGas;
982
+ }
778
983
  else {
779
- adjustedValue = nativeValueForGas;
984
+ throw new Error(`UEA ${ueaAddress} has zero UPC balance; cannot fund outbound gas swap. ` +
985
+ `Bridge UPC to the UEA before retrying.`);
780
986
  }
781
987
  if (adjustedValue !== nativeValueForGas) {
782
988
  (0, context_1.printLog)(ctx, `executeCeaToPush — adjusting nativeValueForGas from ${nativeValueForGas.toString()} to ${adjustedValue.toString()} (effective balance: ${effectiveBalance.toString()})`);
@@ -801,11 +1007,35 @@ function executeCeaToPush(ctx, params, executeFn) {
801
1007
  nonce: ueaNonce,
802
1008
  balance: ueaBalance,
803
1009
  }, _skipFeeLocking: isUEADeployed }, (!isUEADeployed ? { _minimumDepositUsd: ROUTE3_MINIMUM_DEPOSIT_USD } : {}));
804
- const response = yield executeFn(executeParams);
1010
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_01);
1011
+ let response;
1012
+ try {
1013
+ response = yield executeFn(executeParams);
1014
+ }
1015
+ catch (err) {
1016
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_04, err instanceof Error ? err.message : String(err));
1017
+ // Suppress the outer orchestrator catch's 399-02 terminal — 304-04
1018
+ // already surfaced the signature/broadcast failure to the consumer.
1019
+ ctx._routeTerminalEmitted = true;
1020
+ throw err;
1021
+ }
1022
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_02);
1023
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_03);
1024
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_307, sourceChain);
805
1025
  // Add Route 3 context to response
806
1026
  response.chain = sourceChain;
807
1027
  const chainInfo = chain_1.CHAIN_INFO[sourceChain];
808
1028
  response.chainNamespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
1029
+ // R3 inbound round-trip tracking: enable only when funds actually flow
1030
+ // back (amount > 0). Payload-only R3 has no child inbound UTX on Push
1031
+ // Chain, so polling would hang until timeout — skip the inbound block
1032
+ // for amount == 0.
1033
+ //
1034
+ // Child-utxId correlation uses the universal-tx-detector's deterministic
1035
+ // sha256(caip:txHash:logIndex) derivation (see inbound-tracker.ts
1036
+ // findChildUtxIdFromExternalTx), with the original cosmos
1037
+ // `universal_tx_created.inbound_tx_hash` search kept as a fallback.
1038
+ response._expectsInboundRoundTrip = amount > BigInt(0);
809
1039
  return response;
810
1040
  });
811
1041
  }
@@ -822,7 +1052,7 @@ function executeCeaToPush(ctx, params, executeFn) {
822
1052
  */
823
1053
  function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
824
1054
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
825
- var _a, _b;
1055
+ var _a, _b, _c, _d;
826
1056
  if (typeof params.to !== 'string') {
827
1057
  throw new Error('Route 3 SVM (CEA -> Push): to must be a Push Chain address (string), not a ChainTarget');
828
1058
  }
@@ -834,6 +1064,7 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
834
1064
  }
835
1065
  const programPk = new web3_js_1.PublicKey(lockerContract);
836
1066
  const gatewayProgramHex = ('0x' + Buffer.from(programPk.toBytes()).toString('hex'));
1067
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_301, sourceChain, lockerContract);
837
1068
  (0, context_1.printLog)(ctx, `executeCeaToPushSvm — sourceChain: ${sourceChain}, gateway: ${lockerContract}`);
838
1069
  // Determine drain amount and token
839
1070
  let drainAmount = BigInt(0);
@@ -889,20 +1120,43 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
889
1120
  const ueaNonce = isUEADeployed ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress) : BigInt(0);
890
1121
  // Query gas fees
891
1122
  let gasFee = BigInt(0);
1123
+ let protocolFeeR3Svm = BigInt(0);
892
1124
  let nativeValueForGas = BigInt(0);
893
1125
  let gasToken = selectors_1.ZERO_ADDRESS;
1126
+ let sizingDecisionR3Svm;
894
1127
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
1128
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01, sourceChain);
895
1129
  try {
896
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundReq.gasLimit);
1130
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundReq.gasLimit, sourceChain);
897
1131
  gasToken = result.gasToken;
898
1132
  gasFee = result.gasFee;
1133
+ protocolFeeR3Svm = result.protocolFee;
899
1134
  nativeValueForGas = result.nativeValueForGas;
900
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — gasFee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}`);
1135
+ sizingDecisionR3Svm = result.sizing;
1136
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — gasFee: ${gasFee.toString()}, gasToken: ${gasToken}, nativeValueForGas: ${nativeValueForGas.toString()}, sizing=${(_d = (_c = result.sizing) === null || _c === void 0 ? void 0 : _c.category) !== null && _d !== void 0 ? _d : 'legacy'}`);
1137
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02, sourceChain, protocolFeeR3Svm, gasFee);
901
1138
  }
902
1139
  catch (err) {
903
1140
  throw new Error(`Failed to query outbound gas fee for SVM Route 3: ${err}`);
904
1141
  }
905
1142
  }
1143
+ // Account-resolution checkpoint fires before the gas-sizing sub-hook so
1144
+ // consumers see the full account-resolution phase (303-xx) complete before
1145
+ // the sizing decision is surfaced.
1146
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_01, sourceChain);
1147
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_02, ueaAddress, lockerContract, sourceChain);
1148
+ if (sizingDecisionR3Svm) {
1149
+ fireSizingHook(ctx, 'R3', sourceChain, sizingDecisionR3Svm);
1150
+ }
1151
+ // R3 SVM Case C: same minimal interpretation as R3 EVM — bump msg.value
1152
+ // by the overflow so swapAndBurnGas can afford the full gas cost. No
1153
+ // bridge-swap entries (R3 has no destination funds-delivery semantic).
1154
+ if ((sizingDecisionR3Svm === null || sizingDecisionR3Svm === void 0 ? void 0 : sizingDecisionR3Svm.category) === 'C' &&
1155
+ sizingDecisionR3Svm.overflowNativePc > BigInt(0)) {
1156
+ const bumped = nativeValueForGas + sizingDecisionR3Svm.overflowNativePc;
1157
+ (0, context_1.printLog)(ctx, `executeCeaToPushSvm — Case C: bumping nativeValueForGas from ${nativeValueForGas} to ${bumped} (overflow=${sizingDecisionR3Svm.overflowNativePc})`);
1158
+ nativeValueForGas = bumped;
1159
+ }
906
1160
  // Adjust nativeValueForGas using UEA balance (contract refunds excess)
907
1161
  // Re-fetch balance to minimize staleness from gas fee query RPC roundtrips
908
1162
  const currentBalance = yield ctx.pushClient.getBalance(ueaAddress);
@@ -931,11 +1185,31 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
931
1185
  nonce: ueaNonce,
932
1186
  balance: ueaBalance,
933
1187
  }, _skipFeeLocking: isUEADeployed }, (!isUEADeployed ? { _minimumDepositUsd: utils_1.Utils.helpers.parseUnits('10', 8) } : {}));
934
- const response = yield executeFn(executeParams);
1188
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_01);
1189
+ let response;
1190
+ try {
1191
+ response = yield executeFn(executeParams);
1192
+ }
1193
+ catch (err) {
1194
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_04, err instanceof Error ? err.message : String(err));
1195
+ // Suppress the outer orchestrator catch's 399-02 terminal.
1196
+ ctx._routeTerminalEmitted = true;
1197
+ throw err;
1198
+ }
1199
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_02);
1200
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_03);
1201
+ (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_307, sourceChain);
935
1202
  // Add Route 3 SVM context to response
936
1203
  response.chain = sourceChain;
937
1204
  const chainInfo = chain_1.CHAIN_INFO[sourceChain];
938
1205
  response.chainNamespace = `${chain_1.VM_NAMESPACE[chainInfo.vm]}:${chainInfo.chainId}`;
1206
+ // R3 SVM inbound round-trip tracking: enable only when funds flow back
1207
+ // (drainAmount > 0). See the EVM executeCeaToPush note — child-utxId
1208
+ // correlation uses the universal-tx-detector's deterministic derivation.
1209
+ // NOTE: the detector is EVM-only today; SVM source-chain logs can't be
1210
+ // decoded yet, so this branch will fall back to the (broken) cosmos
1211
+ // search. Disable if SVM R3 tests start timing out in CI.
1212
+ response._expectsInboundRoundTrip = drainAmount > BigInt(0);
939
1213
  return response;
940
1214
  });
941
1215
  }