@pushchain/core 5.1.17 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/src/lib/constants/abi/cea.evm.d.ts +52 -0
  3. package/src/lib/constants/abi/cea.evm.js +66 -1
  4. package/src/lib/constants/abi/prc20.evm.d.ts +117 -11
  5. package/src/lib/constants/abi/prc20.evm.js +38 -11
  6. package/src/lib/constants/abi/uea.evm.js +41 -0
  7. package/src/lib/constants/abi/universalGatewayPC.evm.d.ts +117 -1
  8. package/src/lib/constants/abi/universalGatewayPC.evm.js +40 -1
  9. package/src/lib/constants/abi/universalGatewayV0.json +1 -1
  10. package/src/lib/constants/chain.js +14 -14
  11. package/src/lib/constants/index.d.ts +1 -1
  12. package/src/lib/constants/index.js +2 -1
  13. package/src/lib/constants/selectors.d.ts +8 -0
  14. package/src/lib/constants/selectors.js +9 -1
  15. package/src/lib/constants/tokens.js +1 -0
  16. package/src/lib/formatters.d.ts +7 -0
  17. package/src/lib/formatters.js +46 -0
  18. package/src/lib/index.d.ts +2 -0
  19. package/src/lib/index.js +7 -1
  20. package/src/lib/orchestrator/internals/cascade.d.ts +8 -1
  21. package/src/lib/orchestrator/internals/cascade.js +593 -192
  22. package/src/lib/orchestrator/internals/error-decoder.d.ts +2 -6
  23. package/src/lib/orchestrator/internals/error-decoder.js +10 -10
  24. package/src/lib/orchestrator/internals/errors.d.ts +1 -1
  25. package/src/lib/orchestrator/internals/errors.js +8 -5
  26. package/src/lib/orchestrator/internals/execute-funds-only.js +3 -2
  27. package/src/lib/orchestrator/internals/execute-funds-payload.js +2 -1
  28. package/src/lib/orchestrator/internals/execute-standard.js +11 -10
  29. package/src/lib/orchestrator/internals/gas-calculator.d.ts +1 -0
  30. package/src/lib/orchestrator/internals/gas-calculator.js +91 -22
  31. package/src/lib/orchestrator/internals/helpers.d.ts +2 -1
  32. package/src/lib/orchestrator/internals/helpers.js +14 -11
  33. package/src/lib/orchestrator/internals/inbound-tracker.js +4 -2
  34. package/src/lib/orchestrator/internals/outbound-sync.d.ts +1 -0
  35. package/src/lib/orchestrator/internals/outbound-sync.js +140 -71
  36. package/src/lib/orchestrator/internals/pc-usd-oracle.d.ts +1 -1
  37. package/src/lib/orchestrator/internals/pc-usd-oracle.js +4 -3
  38. package/src/lib/orchestrator/internals/preflight.d.ts +2 -16
  39. package/src/lib/orchestrator/internals/preflight.js +2 -36
  40. package/src/lib/orchestrator/internals/push-chain-tx.js +5 -1
  41. package/src/lib/orchestrator/internals/response-builder.js +23 -7
  42. package/src/lib/orchestrator/internals/route-handlers.js +182 -227
  43. package/src/lib/orchestrator/internals/signing.js +7 -2
  44. package/src/lib/orchestrator/internals/svm-helpers.js +3 -3
  45. package/src/lib/orchestrator/internals/svm-rent.d.ts +42 -9
  46. package/src/lib/orchestrator/internals/svm-rent.js +148 -14
  47. package/src/lib/orchestrator/internals/tx-transformer.js +5 -2
  48. package/src/lib/orchestrator/max-pc-for-gas.d.ts +60 -0
  49. package/src/lib/orchestrator/max-pc-for-gas.js +66 -0
  50. package/src/lib/orchestrator/orchestrator.d.ts +8 -1
  51. package/src/lib/orchestrator/orchestrator.js +40 -2
  52. package/src/lib/orchestrator/orchestrator.types.d.ts +35 -23
  53. package/src/lib/orchestrator/payload-builders.d.ts +7 -6
  54. package/src/lib/orchestrator/payload-builders.js +18 -6
  55. package/src/lib/progress-hook/progress-hook.js +39 -38
  56. package/src/lib/push-chain/push-chain.d.ts +11 -1
  57. package/src/lib/push-chain/push-chain.js +2 -2
  58. package/src/lib/universal-tx-detector/cascade.js +14 -4
  59. package/src/lib/universal-tx-detector/child-inbounds.js +9 -1
  60. package/src/lib/universal-tx-detector/detector.js +9 -3
  61. package/src/lib/universal-tx-detector/events.d.ts +3 -1
  62. package/src/lib/universal-tx-detector/events.js +3 -1
  63. package/src/lib/universal-tx-detector/normalize.d.ts +42 -0
  64. package/src/lib/universal-tx-detector/normalize.js +127 -0
  65. package/src/lib/universal-tx-detector/svm-events.d.ts +1 -1
  66. package/src/lib/universal-tx-detector/svm-events.js +1 -1
  67. package/src/lib/utils/external-tx-hash.d.ts +37 -0
  68. package/src/lib/utils/external-tx-hash.js +62 -0
@@ -11,6 +11,7 @@ exports.prepareTransaction = prepareTransaction;
11
11
  exports.buildHopDescriptor = buildHopDescriptor;
12
12
  exports.classifyIntoSegments = classifyIntoSegments;
13
13
  exports.getSegmentType = getSegmentType;
14
+ exports.composeCascadeDetailed = composeCascadeDetailed;
14
15
  exports.composeCascade = composeCascade;
15
16
  exports.createCascadedBuilder = createCascadedBuilder;
16
17
  const tslib_1 = require("tslib");
@@ -38,6 +39,7 @@ const preflight_1 = require("./preflight");
38
39
  const svm_rent_1 = require("./svm-rent");
39
40
  const errors_1 = require("./errors");
40
41
  const context_2 = require("./context");
42
+ const inbound_tracker_1 = require("./inbound-tracker");
41
43
  // ============================================================================
42
44
  // dispatchCascadeProgressEvent
43
45
  // ============================================================================
@@ -120,20 +122,32 @@ function prepareTransaction(ctx, params, callbacks) {
120
122
  // ============================================================================
121
123
  function buildHopDescriptor(ctx, params, route, ueaAddress) {
122
124
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
123
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
124
- // Pass 0 when user omits gasLimit → contract uses per-chain baseGasLimitByChainNamespace
125
- const gasLimit = (_a = params.gasLimit) !== null && _a !== void 0 ? _a : BigInt(0);
125
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
126
+ // Pass 0 when user omits gasLimit so the contract uses the per-chain base,
127
+ // except EVM Route 3 where a CEA deploy + sendUniversalTxToUEA needs more
128
+ // than the generic 500k source-chain base.
129
+ const defaultGasLimit = route === route_detector_1.TransactionRoute.CEA_TO_PUSH &&
130
+ ((_a = params.from) === null || _a === void 0 ? void 0 : _a.chain) &&
131
+ !(0, payload_builders_1.isSvmChain)(params.from.chain)
132
+ ? selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT
133
+ : BigInt(0);
134
+ const gasLimit = (_b = params.gasLimit) !== null && _b !== void 0 ? _b : defaultGasLimit;
126
135
  const routeStr = route;
127
136
  const baseDescriptor = {
128
137
  params,
129
138
  route: routeStr,
130
139
  gasLimit,
140
+ maxPCForGas: (_c = params.maxPCForGas) !== null && _c !== void 0 ? _c : BigInt(0),
131
141
  ueaAddress,
132
142
  revertRecipient: ueaAddress,
133
143
  };
134
144
  switch (route) {
135
145
  case route_detector_1.TransactionRoute.UOA_TO_PUSH: {
136
146
  // Route 1: Build Push Chain multicalls
147
+ const seedAmount = (_d = params.value) !== null && _d !== void 0 ? _d : BigInt(0);
148
+ if (isValueOnlyNativeSeedToOwnUea(params, ueaAddress)) {
149
+ return Object.assign(Object.assign({}, baseDescriptor), { pushMulticalls: [], nativeSeedOnly: true, nativeSeedAmount: seedAmount });
150
+ }
137
151
  const executeParams = (0, helpers_1.toExecuteParams)(params);
138
152
  const pushMulticalls = (0, payload_builders_1.buildExecuteMulticall)({
139
153
  execute: executeParams,
@@ -155,7 +169,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
155
169
  });
156
170
  let prc20Token = selectors_1.ZERO_ADDRESS;
157
171
  let burnAmount = BigInt(0);
158
- if ((_b = params.funds) === null || _b === void 0 ? void 0 : _b.amount) {
172
+ if ((_e = params.funds) === null || _e === void 0 ? void 0 : _e.amount) {
159
173
  const token = params.funds.token;
160
174
  if (token) {
161
175
  prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token).address;
@@ -177,53 +191,70 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
177
191
  }
178
192
  let gasToken = selectors_1.ZERO_ADDRESS;
179
193
  let gasFee = BigInt(0);
194
+ let gasPrice = BigInt(0);
180
195
  let sizing;
181
- // Conditional CEA-ATA rent bump (SPL-only). Mirrors single-route
182
- // executeUoaToCeaSvm path. See svm-rent.ts for rationale.
183
- const splMintBase58 = burnAmount > BigInt(0)
184
- ? (_d = (_c = params.funds) === null || _c === void 0 ? void 0 : _c.token) === null || _d === void 0 ? void 0 : _d.address
196
+ // SVM finalize budget: compare the quoted gasFee in lamports against
197
+ // the gateway finalize minimum, then re-query with a bumped gasLimit
198
+ // if the default quote is too low.
199
+ const svmFundsToken = burnAmount > BigInt(0)
200
+ ? (_f = params.funds) === null || _f === void 0 ? void 0 : _f.token
185
201
  : undefined;
186
- const effectiveGasLimit = yield (0, svm_rent_1.maybeBumpForCeaAtaRent)({
187
- ctx,
188
- ueaAddress,
189
- targetChain,
190
- splMintBase58,
191
- burnAmount,
192
- effectiveGasLimit: gasLimit,
193
- });
202
+ const splMintBase58 = (svmFundsToken === null || svmFundsToken === void 0 ? void 0 : svmFundsToken.mechanism) === 'approve'
203
+ ? svmFundsToken.address
204
+ : undefined;
205
+ let effectiveGasLimit = gasLimit;
194
206
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
195
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, effectiveGasLimit, targetChain);
207
+ let result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, effectiveGasLimit, targetChain);
208
+ result = yield (0, svm_rent_1.ensureSvmFinalizeGasBudgetQuote)({
209
+ ctx,
210
+ ueaAddress,
211
+ targetChain,
212
+ prc20Token,
213
+ quote: result,
214
+ splMintBase58,
215
+ burnAmount,
216
+ pathTag: 'buildHopDescriptor',
217
+ });
196
218
  gasToken = result.gasToken;
197
219
  gasFee = result.gasFee;
220
+ gasPrice = result.gasPrice;
198
221
  sizing = result.sizing;
222
+ effectiveGasLimit = result.gasLimitUsed;
223
+ if (!params.gasLimit) {
224
+ (0, context_1.printLog)(ctx, `buildHopDescriptor — SVM resolved effectiveGasLimit: ${effectiveGasLimit} ` +
225
+ `(gasFee=${result.gasFee}, gasPrice=${result.gasPrice})`);
226
+ }
199
227
  }
200
228
  return Object.assign(Object.assign({}, baseDescriptor), { targetChain, isSvmTarget: true, svmPayload,
201
229
  prc20Token,
202
230
  burnAmount,
203
231
  gasToken,
204
232
  gasFee,
205
- sizing });
233
+ gasPrice, gasLimit: effectiveGasLimit, sizing });
206
234
  }
207
235
  // EVM path: Resolve CEA address + build CEA multicalls
208
- const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_e = ctx.rpcUrls[targetChain]) === null || _e === void 0 ? void 0 : _e[0]);
236
+ const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_g = ctx.rpcUrls[targetChain]) === null || _g === void 0 ? void 0 : _g[0]);
209
237
  // Migration path: raw MIGRATION_SELECTOR payload, no multicall wrapping
210
238
  if (params.migration) {
211
239
  const prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, ctx.pushNetwork);
212
240
  const burnAmount = BigInt(0); // Migration is logic-only — no funds. CEA rejects msg.value != 0.
213
241
  let gasToken = selectors_1.ZERO_ADDRESS;
214
242
  let gasFee = BigInt(0);
243
+ let gasPrice = BigInt(0);
215
244
  let sizing;
216
245
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
217
246
  const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, targetChain);
218
247
  gasToken = result.gasToken;
219
248
  gasFee = result.gasFee;
249
+ gasPrice = result.gasPrice;
220
250
  sizing = result.sizing;
221
251
  }
222
252
  return Object.assign(Object.assign({}, baseDescriptor), { targetChain,
223
253
  ceaAddress, ceaMulticalls: [], prc20Token,
224
254
  burnAmount,
225
255
  gasToken,
226
- gasFee, isMigration: true, sizing });
256
+ gasFee,
257
+ gasPrice, isMigration: true, sizing });
227
258
  }
228
259
  // Build CEA multicalls
229
260
  const ceaMulticalls = [];
@@ -235,7 +266,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
235
266
  // When ERC-20 funds are provided with a single payload, auto-prepend a
236
267
  // transfer() call so the tokens minted to the CEA are forwarded to the
237
268
  // target address. This mirrors the Route 1 behavior in buildExecuteMulticall.
238
- if ((_f = params.funds) === null || _f === void 0 ? void 0 : _f.amount) {
269
+ if ((_h = params.funds) === null || _h === void 0 ? void 0 : _h.amount) {
239
270
  const token = params.funds.token;
240
271
  if (token && token.mechanism !== 'native') {
241
272
  const erc20Transfer = (0, viem_1.encodeFunctionData)({
@@ -255,7 +286,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
255
286
  // native value to the CEA, and the multicall forwards it to the target.
256
287
  ceaMulticalls.push({
257
288
  to: target.address,
258
- value: (_g = params.value) !== null && _g !== void 0 ? _g : BigInt(0),
289
+ value: (_j = params.value) !== null && _j !== void 0 ? _j : BigInt(0),
259
290
  data: params.data,
260
291
  });
261
292
  }
@@ -271,7 +302,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
271
302
  });
272
303
  }
273
304
  }
274
- else if ((_h = params.funds) === null || _h === void 0 ? void 0 : _h.amount) {
305
+ else if ((_k = params.funds) === null || _k === void 0 ? void 0 : _k.amount) {
275
306
  // Funds-only transfer (no data, no value). Without this branch the
276
307
  // CEA multicall is empty and relayed funds stay in the CEA instead
277
308
  // of reaching the caller's recipient.
@@ -303,7 +334,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
303
334
  // Determine PRC-20 token and burn amount
304
335
  let prc20Token = selectors_1.ZERO_ADDRESS;
305
336
  let burnAmount = BigInt(0);
306
- if ((_j = params.funds) === null || _j === void 0 ? void 0 : _j.amount) {
337
+ if ((_l = params.funds) === null || _l === void 0 ? void 0 : _l.amount) {
307
338
  const token = params.funds.token;
308
339
  if (token) {
309
340
  prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token).address;
@@ -326,11 +357,13 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
326
357
  // Query gas fee
327
358
  let gasToken = selectors_1.ZERO_ADDRESS;
328
359
  let gasFee = BigInt(0);
360
+ let gasPrice = BigInt(0);
329
361
  let sizing;
330
362
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
331
363
  const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, targetChain);
332
364
  gasToken = result.gasToken;
333
365
  gasFee = result.gasFee;
366
+ gasPrice = result.gasPrice;
334
367
  sizing = result.sizing;
335
368
  }
336
369
  return Object.assign(Object.assign({}, baseDescriptor), { targetChain,
@@ -340,11 +373,16 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
340
373
  burnAmount,
341
374
  gasToken,
342
375
  gasFee,
376
+ gasPrice,
343
377
  sizing });
344
378
  }
345
379
  case route_detector_1.TransactionRoute.CEA_TO_PUSH: {
346
380
  // Route 3: Build CEA multicalls for sendUniversalTxFromCEA
347
381
  const sourceChain = params.from.chain;
382
+ const seedAmount = (_m = params.value) !== null && _m !== void 0 ? _m : BigInt(0);
383
+ if (isValueOnlyNativeSeedToOwnUea(params, ueaAddress)) {
384
+ return Object.assign(Object.assign({}, baseDescriptor), { sourceChain, burnAmount: BigInt(0), gasToken: selectors_1.ZERO_ADDRESS, gasFee: BigInt(0), nativeSeedOnly: true, nativeSeedAmount: seedAmount });
385
+ }
348
386
  // SVM chains use PDA-based CEA, not factory-deployed CEA
349
387
  if ((0, payload_builders_1.isSvmChain)(sourceChain)) {
350
388
  const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
@@ -361,7 +399,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
361
399
  Buffer.from(ceaPda.toBytes()).toString('hex'));
362
400
  let amount = BigInt(0);
363
401
  let prc20Token;
364
- if (((_k = params.funds) === null || _k === void 0 ? void 0 : _k.amount) && params.funds.amount > BigInt(0)) {
402
+ if (((_o = params.funds) === null || _o === void 0 ? void 0 : _o.amount) && params.funds.amount > BigInt(0)) {
365
403
  amount = params.funds.amount;
366
404
  const token = params.funds.token;
367
405
  if (token && token.address) {
@@ -381,25 +419,42 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
381
419
  }
382
420
  let gasToken = selectors_1.ZERO_ADDRESS;
383
421
  let gasFee = BigInt(0);
422
+ let gasPrice = BigInt(0);
384
423
  let sizing;
424
+ let effectiveGasLimit = gasLimit;
385
425
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
386
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, sourceChain);
426
+ let result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, effectiveGasLimit, sourceChain);
427
+ result = yield (0, svm_rent_1.ensureSvmFinalizeGasBudgetQuote)({
428
+ ctx,
429
+ ueaAddress,
430
+ targetChain: sourceChain,
431
+ prc20Token,
432
+ quote: result,
433
+ splMintBase58: undefined,
434
+ burnAmount: BigInt(0),
435
+ pathTag: 'buildHopDescriptor',
436
+ });
387
437
  gasToken = result.gasToken;
388
438
  gasFee = result.gasFee;
439
+ gasPrice = result.gasPrice;
389
440
  sizing = result.sizing;
441
+ effectiveGasLimit = result.gasLimitUsed;
390
442
  }
391
- return Object.assign(Object.assign({}, baseDescriptor), { sourceChain, ceaAddress: ceaPdaHex, isSvmTarget: true, prc20Token, burnAmount: amount, gasToken,
443
+ return Object.assign(Object.assign({}, baseDescriptor), { sourceChain, ceaAddress: ceaPdaHex, isSvmTarget: true, prc20Token,
444
+ // Route 3 is payload-only on Push: the drain amount lives inside the
445
+ // SVM payload. Burning here would require the UEA to pre-hold pSOL.
446
+ burnAmount: BigInt(0), gasToken,
392
447
  gasFee,
393
- sizing });
448
+ gasPrice, gasLimit: effectiveGasLimit, sizing });
394
449
  }
395
- const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_l = ctx.rpcUrls[sourceChain]) === null || _l === void 0 ? void 0 : _l[0]);
450
+ const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_p = ctx.rpcUrls[sourceChain]) === null || _p === void 0 ? void 0 : _p[0]);
396
451
  // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
397
452
  // if CEA doesn't exist yet. No SDK-side blocking needed.
398
453
  // Determine token/amount for the inbound
399
454
  let tokenAddress = selectors_1.ZERO_ADDRESS;
400
455
  let amount = BigInt(0);
401
456
  let nativeValue = BigInt(0);
402
- if ((_m = params.funds) === null || _m === void 0 ? void 0 : _m.amount) {
457
+ if ((_q = params.funds) === null || _q === void 0 ? void 0 : _q.amount) {
403
458
  const token = params.funds.token;
404
459
  if (token) {
405
460
  if (token.mechanism === 'native') {
@@ -420,17 +475,24 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
420
475
  const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
421
476
  let gasToken = selectors_1.ZERO_ADDRESS;
422
477
  let gasFee = BigInt(0);
478
+ let gasPrice = BigInt(0);
423
479
  let sizing;
424
480
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
425
481
  const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, sourceChain);
426
482
  gasToken = result.gasToken;
427
483
  gasFee = result.gasFee;
484
+ gasPrice = result.gasPrice;
428
485
  sizing = result.sizing;
429
486
  }
430
487
  return Object.assign(Object.assign({}, baseDescriptor), { sourceChain,
431
488
  ceaAddress,
432
- prc20Token, burnAmount: amount, gasToken,
489
+ prc20Token,
490
+ // Route 3 is payload-only on Push: the CEA-side sendUniversalTxToUEA
491
+ // carries `amount`. Burning here would require the UEA to pre-hold the
492
+ // token it is trying to receive from the external CEA.
493
+ burnAmount: BigInt(0), gasToken,
433
494
  gasFee,
495
+ gasPrice,
434
496
  sizing });
435
497
  }
436
498
  default:
@@ -463,7 +525,9 @@ function classifyIntoSegments(hops) {
463
525
  ? currentSegment.targetChain === hop.targetChain && !hop.isSvmTarget
464
526
  : segType === 'INBOUND_FROM_CEA'
465
527
  ? currentSegment.sourceChain === hop.sourceChain &&
466
- !(0, payload_builders_1.isSvmChain)(hop.sourceChain)
528
+ !(0, payload_builders_1.isSvmChain)(hop.sourceChain) &&
529
+ isNativeSeedOnlyHop(currentSegment.hops[0]) ===
530
+ isNativeSeedOnlyHop(hop)
467
531
  : segType === 'PUSH_EXECUTION');
468
532
  if (canMerge && currentSegment) {
469
533
  // Merge into current segment
@@ -483,6 +547,8 @@ function classifyIntoSegments(hops) {
483
547
  // Accumulate gas fees
484
548
  currentSegment.gasFee =
485
549
  (currentSegment.gasFee || BigInt(0)) + (hop.gasFee || BigInt(0));
550
+ currentSegment.gasPrice = mergeGasPrice(currentSegment.gasPrice, hop.gasPrice);
551
+ currentSegment.maxPCForGas = mergeMaxPCForGas(currentSegment.maxPCForGas, hop.maxPCForGas);
486
552
  }
487
553
  else if (segType === 'INBOUND_FROM_CEA') {
488
554
  // Sum burn amounts so the single sendUniversalTxToUEA carries the
@@ -495,6 +561,8 @@ function classifyIntoSegments(hops) {
495
561
  }
496
562
  currentSegment.gasFee =
497
563
  (currentSegment.gasFee || BigInt(0)) + (hop.gasFee || BigInt(0));
564
+ currentSegment.gasPrice = mergeGasPrice(currentSegment.gasPrice, hop.gasPrice);
565
+ currentSegment.maxPCForGas = mergeMaxPCForGas(currentSegment.maxPCForGas, hop.maxPCForGas);
498
566
  }
499
567
  else if (segType === 'PUSH_EXECUTION') {
500
568
  currentSegment.mergedPushMulticalls = [
@@ -532,7 +600,9 @@ function classifyIntoSegments(hops) {
532
600
  prc20Token: hop.prc20Token,
533
601
  gasToken: hop.gasToken,
534
602
  gasFee: hop.gasFee,
603
+ gasPrice: hop.gasPrice,
535
604
  gasLimit: hop.gasLimit,
605
+ maxPCForGas: hop.maxPCForGas,
536
606
  sizing: hop.sizing,
537
607
  };
538
608
  segments.push(currentSegment);
@@ -544,6 +614,18 @@ function classifyIntoSegments(hops) {
544
614
  function categoryRank(c) {
545
615
  return c === 'C' ? 2 : c === 'B' ? 1 : 0;
546
616
  }
617
+ function mergeMaxPCForGas(current, next) {
618
+ const a = current !== null && current !== void 0 ? current : BigInt(0);
619
+ const b = next !== null && next !== void 0 ? next : BigInt(0);
620
+ if (a === BigInt(0) || b === BigInt(0))
621
+ return BigInt(0);
622
+ return a + b;
623
+ }
624
+ function mergeGasPrice(current, next) {
625
+ const a = current !== null && current !== void 0 ? current : BigInt(0);
626
+ const b = next !== null && next !== void 0 ? next : BigInt(0);
627
+ return a > b ? a : b;
628
+ }
547
629
  // ============================================================================
548
630
  // getSegmentType
549
631
  // ============================================================================
@@ -559,46 +641,156 @@ function getSegmentType(route) {
559
641
  return 'PUSH_EXECUTION';
560
642
  }
561
643
  }
562
- // ============================================================================
563
- // composeCascade
564
- // ============================================================================
565
- function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
644
+ function sumMulticallNativeValue(multicalls) {
645
+ return (multicalls !== null && multicalls !== void 0 ? multicalls : []).reduce((sum, mc) => { var _a; return sum + ((_a = mc.value) !== null && _a !== void 0 ? _a : BigInt(0)); }, BigInt(0));
646
+ }
647
+ function isEmptyPayloadData(data) {
648
+ return (data === undefined ||
649
+ data === '0x' ||
650
+ (Array.isArray(data) && data.length === 0));
651
+ }
652
+ function isValueOnlyNativeSeedToOwnUea(params, ueaAddress) {
653
+ var _a, _b;
654
+ return (((_a = params.value) !== null && _a !== void 0 ? _a : BigInt(0)) > BigInt(0) &&
655
+ !((_b = params.funds) === null || _b === void 0 ? void 0 : _b.amount) &&
656
+ isEmptyPayloadData(params.data) &&
657
+ typeof params.to === 'string' &&
658
+ params.to.toLowerCase() === ueaAddress.toLowerCase());
659
+ }
660
+ function isNativeSeedOnlyHop(hop) {
661
+ return (hop === null || hop === void 0 ? void 0 : hop.nativeSeedOnly) === true;
662
+ }
663
+ function isNativeSeedOnlySegment(segment) {
664
+ return (segment.type === 'INBOUND_FROM_CEA' &&
665
+ segment.hops.length > 0 &&
666
+ segment.hops.every((hop) => isNativeSeedOnlyHop(hop)));
667
+ }
668
+ function segmentNeedsOutboundGas(segment) {
669
+ return segment.type !== 'PUSH_EXECUTION' && !isNativeSeedOnlySegment(segment);
670
+ }
671
+ function buildRootOutboundIndexByHop(segments, hops) {
672
+ var _a;
673
+ const hopIndexByDescriptor = new Map();
674
+ hops.forEach((hop, index) => hopIndexByDescriptor.set(hop, index));
675
+ const nextIndexByChain = new Map();
676
+ const result = new Map();
677
+ for (const segment of segments) {
678
+ let chain;
679
+ if (segment.type === 'OUTBOUND_TO_CEA') {
680
+ chain = segment.targetChain;
681
+ }
682
+ else if (segment.type === 'INBOUND_FROM_CEA' &&
683
+ !isNativeSeedOnlySegment(segment)) {
684
+ chain = segment.sourceChain;
685
+ }
686
+ if (!chain)
687
+ continue;
688
+ const outboundIndex = (_a = nextIndexByChain.get(chain)) !== null && _a !== void 0 ? _a : 0;
689
+ for (const hop of segment.hops) {
690
+ const hopIndex = hopIndexByDescriptor.get(hop);
691
+ if (hopIndex !== undefined) {
692
+ result.set(hopIndex, outboundIndex);
693
+ }
694
+ }
695
+ nextIndexByChain.set(chain, outboundIndex + 1);
696
+ }
697
+ return result;
698
+ }
699
+ function sumNativeSeedAmount(hops) {
700
+ return hops.reduce((sum, hop) => { var _a; return sum + ((_a = hop.nativeSeedAmount) !== null && _a !== void 0 ? _a : BigInt(0)); }, BigInt(0));
701
+ }
702
+ function getCascadeNativeSeed(segments) {
703
+ var _a, _b, _c;
704
+ let total = BigInt(0);
705
+ for (const segment of segments) {
706
+ for (const hop of segment.hops) {
707
+ const value = (_a = hop.params.value) !== null && _a !== void 0 ? _a : BigInt(0);
708
+ if (isNativeSeedOnlyHop(hop)) {
709
+ total += (_b = hop.nativeSeedAmount) !== null && _b !== void 0 ? _b : value;
710
+ continue;
711
+ }
712
+ // In a CEA->Push hop that also supplies `funds`, `value` is native PC
713
+ // forwarded inside the inbound UEA payload. That value is available to
714
+ // later Push-side cascade calls and must be counted for sizing fresh UEAs.
715
+ if (segment.type === 'INBOUND_FROM_CEA' &&
716
+ value > BigInt(0) &&
717
+ ((_c = hop.params.funds) === null || _c === void 0 ? void 0 : _c.amount)) {
718
+ total += value;
719
+ }
720
+ }
721
+ }
722
+ return total;
723
+ }
724
+ function getInboundFundingForBurnToken(segments, beforeSegmentIndex, burnToken, pushNetwork) {
725
+ var _a;
726
+ let total = BigInt(0);
727
+ const normalizedBurnToken = burnToken.toLowerCase();
728
+ for (const segment of segments.slice(0, beforeSegmentIndex)) {
729
+ if (segment.type !== 'INBOUND_FROM_CEA')
730
+ continue;
731
+ for (const hop of segment.hops) {
732
+ const params = hop.params;
733
+ let fundedToken;
734
+ let fundedAmount = BigInt(0);
735
+ if ((_a = params.funds) === null || _a === void 0 ? void 0 : _a.amount) {
736
+ const token = params.funds.token;
737
+ if (token) {
738
+ fundedToken = push_chain_1.PushChain.utils.tokens.getPRC20Address(token).address;
739
+ fundedAmount = params.funds.amount;
740
+ }
741
+ }
742
+ else if (params.value &&
743
+ params.value > BigInt(0) &&
744
+ hop.sourceChain &&
745
+ !isNativeSeedOnlyHop(hop)) {
746
+ fundedToken = (0, helpers_1.getNativePRC20ForChain)(hop.sourceChain, pushNetwork);
747
+ fundedAmount = params.value;
748
+ }
749
+ if ((fundedToken === null || fundedToken === void 0 ? void 0 : fundedToken.toLowerCase()) === normalizedBurnToken) {
750
+ total += fundedAmount;
751
+ }
752
+ }
753
+ }
754
+ return total;
755
+ }
756
+ function composeCascadeDetailed(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
566
757
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
567
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
758
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
568
759
  let accumulatedPushMulticalls = [];
760
+ let requiredNativeValue = BigInt(0);
569
761
  const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
762
+ const currentUeaBalance = ueaBalance !== null && ueaBalance !== void 0 ? ueaBalance : BigInt(0);
763
+ const cascadeNativeSeed = getCascadeNativeSeed(segments);
764
+ const effectiveUeaBalance = currentUeaBalance + cascadeNativeSeed;
765
+ if (cascadeNativeSeed > BigInt(0)) {
766
+ (0, context_1.printLog)(ctx, `composeCascade — counting cascade native seed ${cascadeNativeSeed.toString()} for cascade sizing (current UEA balance ${currentUeaBalance.toString()})`);
767
+ }
570
768
  // Compute per-outbound nativeValueForGas from UEA balance
571
769
  // Each outbound segment needs native value for the gas swap on the destination chain.
572
770
  // The contract refunds excess, so over-allocating is safe.
573
- const numOutbounds = segments.filter((s) => s.type !== 'PUSH_EXECUTION').length;
771
+ const numOutbounds = segments.filter(segmentNeedsOutboundGas).length;
574
772
  const CASCADE_GAS_RESERVE = BigInt(3e18); // 3 PC reserve for gas costs
575
773
  // Cascade pre-flight: catches the zero-balance silent-segment-zero failure
576
774
  // mode. When `ueaBalance <= CASCADE_GAS_RESERVE`, `perOutboundNativeValue`
577
775
  // is left undefined and downstream segments silently allocate value=0,
578
776
  // producing under-funded swaps that revert inside Uniswap. Throw early
579
777
  // with a structured error so the caller knows which path / hop failed.
580
- // Honor the per-call opt-out from the first non-PUSH segment (segments share
581
- // the same UniversalExecuteParams.options.allowUnderfundedSwap shape).
582
- const firstOutboundSegment = segments.find((s) => s.type !== 'PUSH_EXECUTION');
583
- const cascadeAllowUnderfunded = ((_d = (_c = (_b = (_a = firstOutboundSegment === null || firstOutboundSegment === void 0 ? void 0 : firstOutboundSegment.hops) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.params) === null || _c === void 0 ? void 0 : _c.options) === null || _d === void 0 ? void 0 : _d.allowUnderfundedSwap) ===
584
- true;
585
778
  if (numOutbounds > 0) {
586
779
  (0, preflight_1.runPreflight)({
587
780
  ctx,
588
781
  ueaAddress,
589
- ueaBalance: ueaBalance !== null && ueaBalance !== void 0 ? ueaBalance : BigInt(0),
782
+ ueaBalance: effectiveUeaBalance,
590
783
  // Required = at least one wei of allocatable PC after reserve. The
591
784
  // per-segment min-swap check below catches finer-grained shortfalls.
592
785
  requiredValue: BigInt(1),
593
786
  gasReserve: CASCADE_GAS_RESERVE,
594
787
  pathTag: 'CASCADE',
595
- allowUnderfundedSwap: cascadeAllowUnderfunded,
596
788
  });
597
789
  }
598
790
  let perOutboundNativeValue;
599
- if (ueaBalance && numOutbounds > 0 && ueaBalance > CASCADE_GAS_RESERVE) {
791
+ if (numOutbounds > 0 && effectiveUeaBalance > CASCADE_GAS_RESERVE) {
600
792
  perOutboundNativeValue =
601
- (ueaBalance - CASCADE_GAS_RESERVE) / BigInt(numOutbounds);
793
+ (effectiveUeaBalance - CASCADE_GAS_RESERVE) / BigInt(numOutbounds);
602
794
  }
603
795
  // Pool-price-based native-value override for SVM outbounds.
604
796
  // The flat split above works for EVM gas tokens (pBNB/pETH — cheap per unit),
@@ -614,11 +806,11 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
614
806
  s.gasFee > BigInt(0);
615
807
  });
616
808
  const svmNativeValueBySegment = new Map();
617
- if (svmSegments.length > 0 && ueaBalance && ueaBalance > CASCADE_GAS_RESERVE) {
809
+ if (svmSegments.length > 0 && effectiveUeaBalance > CASCADE_GAS_RESERVE) {
618
810
  const universalCoreAddress = yield ctx.pushClient.readContract({
619
811
  address: gatewayPcAddress,
620
812
  abi: abi_1.UNIVERSAL_GATEWAY_PC,
621
- functionName: 'UNIVERSAL_CORE',
813
+ functionName: 'universalCore',
622
814
  args: [],
623
815
  });
624
816
  // Compute per-SVM budget = ueaBalance − (PC needed for EVM outbounds) − 1 PC safety
@@ -629,11 +821,11 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
629
821
  ? perOutboundNativeValue * BigInt(numEvmOutbounds)
630
822
  : BigInt(0);
631
823
  const outerSafety = BigInt(1e18); // 1 PC for outer-tx gas
632
- const svmTotal = ueaBalance > evmReservation + outerSafety
633
- ? ueaBalance - evmReservation - outerSafety
824
+ const svmTotal = effectiveUeaBalance > evmReservation + outerSafety
825
+ ? effectiveUeaBalance - evmReservation - outerSafety
634
826
  : BigInt(0);
635
827
  const perSvmCap = svmTotal / BigInt(svmSegments.length);
636
- const UNCAPPED_BALANCE = BigInt(1e30); // effectively disable estimator's internal cap
828
+ const UNCAPPED_BALANCE = BigInt('1000000000000000000000000000000'); // effectively disable estimator's internal cap
637
829
  for (const seg of svmSegments) {
638
830
  // Cascade-level segment index (position in `segments`, not in `svmSegments`)
639
831
  // so the error's `segmentIndex` lines up with what the caller sees.
@@ -643,8 +835,13 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
643
835
  (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, estimated, seg.gasToken, 'CASCADE');
644
836
  const flat = perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : BigInt(0);
645
837
  let value = estimated > flat ? estimated : flat;
646
- if (perSvmCap > BigInt(0) && value > perSvmCap)
647
- value = perSvmCap;
838
+ if (perSvmCap > BigInt(0) && value > perSvmCap) {
839
+ const cappedValue = perSvmCap >= estimated ? perSvmCap : estimated;
840
+ if (cappedValue !== perSvmCap) {
841
+ (0, context_1.printLog)(ctx, `composeCascade — SVM outbound to ${(_a = seg.hops[0]) === null || _a === void 0 ? void 0 : _a.targetChain}: perSvmCap=${perSvmCap} is below live pool quote=${estimated}; using quote to avoid underfunded gas swap`);
842
+ }
843
+ value = cappedValue;
844
+ }
648
845
  // Upward-allocation ceiling (mirrors R3 SVM Fix #4 in route-handlers.ts:1690).
649
846
  // When UEA balance is huge, the flat split or perSvmCap can produce a value
650
847
  // far above what the swap actually needs. Submitting that into a Uniswap
@@ -659,16 +856,16 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
659
856
  ? estimated * BigInt(5)
660
857
  : UPWARD_BASE_CEILING_CASCADE_SVM;
661
858
  if (value > upwardCeilingCascadeSvm) {
662
- (0, context_1.printLog)(ctx, `composeCascade — SVM outbound to ${(_e = seg.hops[0]) === null || _e === void 0 ? void 0 : _e.targetChain}: capping value from ${value} to upwardCeiling=${upwardCeilingCascadeSvm} (pool quote=${estimated})`);
859
+ (0, context_1.printLog)(ctx, `composeCascade — SVM outbound to ${(_b = seg.hops[0]) === null || _b === void 0 ? void 0 : _b.targetChain}: capping value from ${value} to upwardCeiling=${upwardCeilingCascadeSvm} (pool quote=${estimated})`);
663
860
  value = upwardCeilingCascadeSvm;
664
861
  }
665
- (0, context_1.printLog)(ctx, `composeCascade — SVM outbound to ${(_f = seg.hops[0]) === null || _f === void 0 ? void 0 : _f.targetChain}: gasFee=${seg.gasFee}, estimatedWpc=${estimated}, flat=${flat}, perSvmCap=${perSvmCap}, upwardCeiling=${upwardCeilingCascadeSvm}, chosen=${value}`);
862
+ (0, context_1.printLog)(ctx, `composeCascade — SVM outbound to ${(_c = seg.hops[0]) === null || _c === void 0 ? void 0 : _c.targetChain}: gasFee=${seg.gasFee}, estimatedWpc=${estimated}, flat=${flat}, perSvmCap=${perSvmCap}, upwardCeiling=${upwardCeilingCascadeSvm}, chosen=${value}`);
666
863
  // Per-segment minimum-viable-swap check: if the chosen `value` is below
667
864
  // the live pool quote (`estimated`), the cap squeezed the budget below
668
865
  // what the swap actually needs and the segment will revert inside
669
866
  // Uniswap with STF. Throw cleanly with `segmentIndex` so the caller
670
867
  // knows which hop's pool is misbehaving.
671
- if (estimated > BigInt(0) && value < estimated && !cascadeAllowUnderfunded) {
868
+ if (estimated > BigInt(0) && value < estimated) {
672
869
  const shortfall = estimated - value;
673
870
  (0, context_2.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_003_04, estimated, value, shortfall, ueaAddress, 'CASCADE', { kind: 'NATIVE', segmentIndex: cascadeIdx });
674
871
  throw new errors_1.InsufficientUEABalanceError({
@@ -689,6 +886,11 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
689
886
  switch (segment.type) {
690
887
  case 'PUSH_EXECUTION': {
691
888
  // Prepend Push Chain multicalls to accumulated
889
+ const seedAmount = sumNativeSeedAmount(segment.hops);
890
+ if (seedAmount > requiredNativeValue) {
891
+ requiredNativeValue = seedAmount;
892
+ }
893
+ requiredNativeValue += sumMulticallNativeValue(segment.mergedPushMulticalls);
692
894
  accumulatedPushMulticalls = [
693
895
  ...(segment.mergedPushMulticalls || []),
694
896
  ...accumulatedPushMulticalls,
@@ -703,7 +905,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
703
905
  const isMigration = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.isMigration) === true;
704
906
  if (isSvmSegment) {
705
907
  // SVM: use the pre-built SVM payload from the hop descriptor
706
- outboundPayload = (_g = firstHop.svmPayload) !== null && _g !== void 0 ? _g : '0x';
908
+ outboundPayload = (_d = firstHop.svmPayload) !== null && _d !== void 0 ? _d : '0x';
707
909
  // For SVM, to.address IS the target (program for execute, recipient for withdraw)
708
910
  const svmTarget = firstHop.params.to;
709
911
  targetForOutbound = svmTarget.address;
@@ -717,7 +919,10 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
717
919
  // EVM: build CEA payload from merged multicalls
718
920
  outboundPayload = (0, payload_builders_1.buildCeaMulticallPayload)(segment.mergedCeaMulticalls || []);
719
921
  // Get CEA address from the first hop
720
- targetForOutbound = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.ceaAddress) || ueaAddress;
922
+ targetForOutbound = outboundPayload === '0x'
923
+ ? selectors_1.ZERO_ADDRESS
924
+ : (firstHop === null || firstHop === void 0 ? void 0 : firstHop.ceaAddress) || ueaAddress;
925
+ (0, payload_builders_1.assertCeaFundsParkingInvariant)(targetForOutbound, outboundPayload);
721
926
  }
722
927
  // Per-segment PRC-20 burn-balance pre-check (cascade R2-style segments
723
928
  // only — INBOUND_FROM_CEA segments are payload-only with totalBurnAmount=0
@@ -736,12 +941,24 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
736
941
  });
737
942
  const sufficient = onHand >= segBurnAmount;
738
943
  (0, context_2.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_003_03, segBurnAmount, onHand, sufficient, ueaAddress, 'CASCADE', { kind: 'PRC20', burnToken: segBurnToken, segmentIndex: segIdxLoop });
739
- if (!sufficient && !cascadeAllowUnderfunded) {
740
- const shortfall = segBurnAmount - onHand;
741
- (0, context_2.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_003_04, segBurnAmount, onHand, shortfall, ueaAddress, 'CASCADE', { kind: 'PRC20', burnToken: segBurnToken, segmentIndex: segIdxLoop });
944
+ const priorPushExecutionMayFundBurn = segments
945
+ .slice(0, segIdxLoop)
946
+ .some((s) => s.type === 'PUSH_EXECUTION');
947
+ const priorInboundFundingForBurn = getInboundFundingForBurnToken(segments, segIdxLoop, segBurnToken, ctx.pushNetwork);
948
+ const priorInboundMayFundBurn = onHand + priorInboundFundingForBurn >= segBurnAmount;
949
+ if (!sufficient &&
950
+ !priorPushExecutionMayFundBurn &&
951
+ !priorInboundMayFundBurn) {
952
+ const projectedOnHand = onHand + priorInboundFundingForBurn;
953
+ const shortfall = segBurnAmount - projectedOnHand;
954
+ (0, context_2.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_003_04, segBurnAmount, projectedOnHand, shortfall, ueaAddress, 'CASCADE', {
955
+ kind: 'PRC20',
956
+ burnToken: segBurnToken,
957
+ segmentIndex: segIdxLoop,
958
+ });
742
959
  throw new errors_1.InsufficientUEABalanceError({
743
960
  required: segBurnAmount,
744
- available: onHand,
961
+ available: projectedOnHand,
745
962
  shortfall,
746
963
  ueaAddress,
747
964
  pathTag: 'CASCADE',
@@ -750,9 +967,15 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
750
967
  segmentIndex: segIdxLoop,
751
968
  });
752
969
  }
970
+ else if (!sufficient && priorPushExecutionMayFundBurn) {
971
+ (0, context_1.printLog)(ctx, `composeCascade — skipping PRC-20 preflight failure for segment ${segIdxLoop}; earlier Push execution may fund ${segBurnToken} before this outbound runs`);
972
+ }
973
+ else if (!sufficient && priorInboundMayFundBurn) {
974
+ (0, context_1.printLog)(ctx, `composeCascade — skipping PRC-20 preflight failure for segment ${segIdxLoop}; earlier inbound funds ${priorInboundFundingForBurn.toString()} units of ${segBurnToken} before this outbound runs`);
975
+ }
753
976
  }
754
977
  // Build outbound request
755
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetForOutbound, segment.prc20Token || selectors_1.ZERO_ADDRESS, segment.totalBurnAmount || BigInt(0), (_h = segment.gasLimit) !== null && _h !== void 0 ? _h : BigInt(0), outboundPayload, ueaAddress);
978
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetForOutbound, segment.prc20Token || selectors_1.ZERO_ADDRESS, segment.totalBurnAmount || BigInt(0), (_e = segment.gasLimit) !== null && _e !== void 0 ? _e : BigInt(0), outboundPayload, ueaAddress, (_f = segment.maxPCForGas) !== null && _f !== void 0 ? _f : BigInt(0), (_g = segment.gasPrice) !== null && _g !== void 0 ? _g : BigInt(0));
756
979
  // Build approval + outbound multicalls
757
980
  const segGasFee = segment.gasFee || BigInt(0);
758
981
  const svmOverride = svmNativeValueBySegment.get(segment);
@@ -762,13 +985,14 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
762
985
  // across outbound segments. The contract refunds excess via
763
986
  // swapAndBurnGas, so overshooting is safe; undershooting causes STF.
764
987
  let evmFallbackValue;
765
- if (ueaBalance && ueaBalance > BigInt(0)) {
766
- evmFallbackValue = ueaBalance / BigInt(Math.max(numOutbounds, 1));
988
+ if (effectiveUeaBalance > BigInt(0)) {
989
+ evmFallbackValue =
990
+ effectiveUeaBalance / BigInt(Math.max(numOutbounds, 1));
767
991
  }
768
992
  else {
769
993
  evmFallbackValue = segGasFee * BigInt(1000000);
770
994
  }
771
- let nativeValueForGas = (_j = svmOverride !== null && svmOverride !== void 0 ? svmOverride : perOutboundNativeValue) !== null && _j !== void 0 ? _j : evmFallbackValue;
995
+ let nativeValueForGas = (_h = svmOverride !== null && svmOverride !== void 0 ? svmOverride : perOutboundNativeValue) !== null && _h !== void 0 ? _h : evmFallbackValue;
772
996
  // SDK 5.2 Case C: when the segment's sizing is category C, compose
773
997
  // R2/OUTBOUND_TO_CEA segments no longer apply Case A/B/C sizing
774
998
  // (scoped to R1 + R3 only). segment.sizing may still be populated
@@ -784,6 +1008,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
784
1008
  gatewayPcAddress,
785
1009
  outboundRequest: outboundReq,
786
1010
  });
1011
+ requiredNativeValue += sumMulticallNativeValue(outboundMulticalls);
787
1012
  // Prepend to accumulated
788
1013
  accumulatedPushMulticalls = [
789
1014
  ...outboundMulticalls,
@@ -794,13 +1019,21 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
794
1019
  case 'INBOUND_FROM_CEA': {
795
1020
  // The accumulated multicalls = what runs on Push Chain AFTER inbound arrives.
796
1021
  // Wrap in UniversalPayload struct with correct UEA nonce for the relay.
1022
+ if (isNativeSeedOnlySegment(segment)) {
1023
+ const seedAmount = sumNativeSeedAmount(segment.hops);
1024
+ if (seedAmount > requiredNativeValue) {
1025
+ requiredNativeValue = seedAmount;
1026
+ }
1027
+ (0, context_1.printLog)(ctx, `composeCascade — value-only CEA_TO_PUSH seed ${seedAmount.toString()} folded into root Push execution; no source-chain outbound needed`);
1028
+ break;
1029
+ }
797
1030
  // Build push multicalls from EVERY merged hop's own data in original
798
1031
  // order (e.g. counter.increment() + value transfer). Multi-hop R3
799
1032
  // segments (same sourceChain) bundle all target ops into the single
800
1033
  // UEA multicall so no nested outbound is needed.
801
1034
  const mergedHopMulticalls = [];
802
1035
  for (const h of segment.hops) {
803
- if (((_k = h === null || h === void 0 ? void 0 : h.params) === null || _k === void 0 ? void 0 : _k.data) || ((_l = h === null || h === void 0 ? void 0 : h.params) === null || _l === void 0 ? void 0 : _l.value)) {
1036
+ if (((_j = h === null || h === void 0 ? void 0 : h.params) === null || _j === void 0 ? void 0 : _j.data) || ((_k = h === null || h === void 0 ? void 0 : h.params) === null || _k === void 0 ? void 0 : _k.value)) {
804
1037
  const hopCalls = (0, payload_builders_1.buildExecuteMulticall)({
805
1038
  execute: {
806
1039
  to: h.params.to,
@@ -813,6 +1046,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
813
1046
  }
814
1047
  }
815
1048
  if (mergedHopMulticalls.length > 0) {
1049
+ requiredNativeValue += sumMulticallNativeValue(mergedHopMulticalls);
816
1050
  accumulatedPushMulticalls = [
817
1051
  ...mergedHopMulticalls,
818
1052
  ...accumulatedPushMulticalls,
@@ -824,7 +1058,9 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
824
1058
  if (accumulatedPushMulticalls.length > 0) {
825
1059
  const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, ueaAddress, accumulatedPushMulticalls);
826
1060
  // +1: the outbound tx consumes one nonce via execute()
827
- intermediatePayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: (ueaNonce !== null && ueaNonce !== void 0 ? ueaNonce : BigInt(0)) + BigInt(1) });
1061
+ intermediatePayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, {
1062
+ nonce: (ueaNonce !== null && ueaNonce !== void 0 ? ueaNonce : BigInt(0)) + BigInt(1),
1063
+ });
828
1064
  }
829
1065
  const hop = segment.hops[0];
830
1066
  const sourceChain = hop.sourceChain;
@@ -841,7 +1077,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
841
1077
  let drainAmount = BigInt(0);
842
1078
  let tokenMintHex;
843
1079
  const params = hop.params;
844
- if (((_m = params.funds) === null || _m === void 0 ? void 0 : _m.amount) && params.funds.amount > BigInt(0)) {
1080
+ if (((_l = params.funds) === null || _l === void 0 ? void 0 : _l.amount) && params.funds.amount > BigInt(0)) {
845
1081
  drainAmount = params.funds.amount;
846
1082
  const token = params.funds.token;
847
1083
  if (token && token.address) {
@@ -870,11 +1106,12 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
870
1106
  });
871
1107
  const svmBurnAmount = segment.totalBurnAmount || BigInt(0);
872
1108
  const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, segment.prc20Token ||
873
- (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), svmBurnAmount, (_o = segment.gasLimit) !== null && _o !== void 0 ? _o : BigInt(0), svmPayload, ueaAddress);
1109
+ (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), svmBurnAmount, (_m = segment.gasLimit) !== null && _m !== void 0 ? _m : BigInt(0), svmPayload, ueaAddress, (_o = segment.maxPCForGas) !== null && _o !== void 0 ? _o : BigInt(0), (_p = segment.gasPrice) !== null && _p !== void 0 ? _p : BigInt(0));
874
1110
  const inboundGasFee = segment.gasFee || BigInt(0);
875
1111
  let svmInboundFallback;
876
- if (ueaBalance && ueaBalance > BigInt(0)) {
877
- svmInboundFallback = ueaBalance / BigInt(Math.max(numOutbounds, 1));
1112
+ if (effectiveUeaBalance > BigInt(0)) {
1113
+ svmInboundFallback =
1114
+ effectiveUeaBalance / BigInt(Math.max(numOutbounds, 1));
878
1115
  }
879
1116
  else {
880
1117
  svmInboundFallback = inboundGasFee * BigInt(1000000);
@@ -883,7 +1120,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
883
1120
  // SDK 5.2 R3-style Case C bump: when the inbound segment's sizing
884
1121
  // is C, top up the swap budget by the overflow. No bridge-swap —
885
1122
  // R3 has no destination funds delivery.
886
- if (((_p = segment.sizing) === null || _p === void 0 ? void 0 : _p.category) === 'C' &&
1123
+ if (((_q = segment.sizing) === null || _q === void 0 ? void 0 : _q.category) === 'C' &&
887
1124
  segment.sizing.overflowNativePc > BigInt(0)) {
888
1125
  const bumped = svmInboundNativeValue + segment.sizing.overflowNativePc;
889
1126
  (0, context_1.printLog)(ctx, `composeCascade — INBOUND_FROM_CEA (SVM) Case C: bumping nativeValueForGas from ${svmInboundNativeValue} to ${bumped} (overflow=${segment.sizing.overflowNativePc})`);
@@ -899,10 +1136,12 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
899
1136
  gatewayPcAddress,
900
1137
  outboundRequest: outboundReq,
901
1138
  });
1139
+ requiredNativeValue += sumMulticallNativeValue(outboundMulticalls);
902
1140
  accumulatedPushMulticalls = [...outboundMulticalls];
903
1141
  break;
904
1142
  }
905
- // EVM path: Build CEA multicall: [approve?, sendUniversalTxFromCEA(payload)]
1143
+ // EVM path: Build CEA multicall: sendUniversalTxToUEA(payload).
1144
+ // CEA handles ERC20 gateway approval internally.
906
1145
  const ceaMulticalls = [];
907
1146
  // Add primary hop's own CEA operations if any
908
1147
  // (e.g., approve + swap before bridging back)
@@ -918,7 +1157,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
918
1157
  let amount = BigInt(0);
919
1158
  for (const mergedHop of segment.hops) {
920
1159
  const hParams = mergedHop.params;
921
- if ((_q = hParams.funds) === null || _q === void 0 ? void 0 : _q.amount) {
1160
+ if ((_r = hParams.funds) === null || _r === void 0 ? void 0 : _r.amount) {
922
1161
  const token = hParams.funds.token;
923
1162
  if (token) {
924
1163
  if (token.mechanism === 'native') {
@@ -932,32 +1171,6 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
932
1171
  }
933
1172
  tokenAddress = addr;
934
1173
  amount += hParams.funds.amount;
935
- // CEA approves gateway once with the cumulative allowance.
936
- const gatewayAddr = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
937
- if (!gatewayAddr) {
938
- throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
939
- }
940
- // Reset allowance to 0 first for USDT-style tokens
941
- const approveZeroData = (0, viem_1.encodeFunctionData)({
942
- abi: erc20_evm_1.ERC20_EVM,
943
- functionName: 'approve',
944
- args: [gatewayAddr, BigInt(0)],
945
- });
946
- ceaMulticalls.push({
947
- to: tokenAddress,
948
- value: BigInt(0),
949
- data: approveZeroData,
950
- });
951
- const approveData = (0, viem_1.encodeFunctionData)({
952
- abi: erc20_evm_1.ERC20_EVM,
953
- functionName: 'approve',
954
- args: [gatewayAddr, hParams.funds.amount],
955
- });
956
- ceaMulticalls.push({
957
- to: tokenAddress,
958
- value: BigInt(0),
959
- data: approveData,
960
- });
961
1174
  }
962
1175
  }
963
1176
  }
@@ -965,7 +1178,8 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
965
1178
  amount += hParams.value;
966
1179
  }
967
1180
  }
968
- // Build sendUniversalTxToUEA self-call on CEA (to=CEA, value=0)
1181
+ // Build sendUniversalTxToUEA self-call on CEA. CEA self-calls must
1182
+ // always use value=0; native funds are spent from the CEA balance.
969
1183
  const sendCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, tokenAddress, amount, intermediatePayload, ceaAddress);
970
1184
  ceaMulticalls.push(sendCall);
971
1185
  // Wrap CEA multicall in outbound from Push Chain
@@ -974,17 +1188,17 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
974
1188
  // gasLimit — UGPC.sendUniversalTxOutbound reverts with
975
1189
  // "Missing or invalid parameters" on 0. When the hop didn't supply
976
1190
  // one (typical for a pure-R3 leg composed inside a cascade), fall
977
- // back to DEFAULT_OUTBOUND_GAS_LIMIT just like the initial outbound
978
- // path does.
1191
+ // back to the EVM Route 3 default.
979
1192
  const effectiveGasLimit = segment.gasLimit && segment.gasLimit > BigInt(0)
980
1193
  ? segment.gasLimit
981
- : selectors_1.DEFAULT_OUTBOUND_GAS_LIMIT;
1194
+ : selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
982
1195
  const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, segment.prc20Token ||
983
- (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), segment.totalBurnAmount || BigInt(0), effectiveGasLimit, ceaPayload, ueaAddress);
1196
+ (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), segment.totalBurnAmount || BigInt(0), effectiveGasLimit, ceaPayload, ueaAddress, (_s = segment.maxPCForGas) !== null && _s !== void 0 ? _s : BigInt(0), (_t = segment.gasPrice) !== null && _t !== void 0 ? _t : BigInt(0));
984
1197
  const inboundGasFee = segment.gasFee || BigInt(0);
985
1198
  let evmInboundFallback;
986
- if (ueaBalance && ueaBalance > BigInt(0)) {
987
- evmInboundFallback = ueaBalance / BigInt(Math.max(numOutbounds, 1));
1199
+ if (effectiveUeaBalance > BigInt(0)) {
1200
+ evmInboundFallback =
1201
+ effectiveUeaBalance / BigInt(Math.max(numOutbounds, 1));
988
1202
  }
989
1203
  else {
990
1204
  evmInboundFallback = inboundGasFee * BigInt(1000000);
@@ -993,7 +1207,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
993
1207
  // SDK 5.2 R3-style Case C bump: when the inbound segment's sizing
994
1208
  // is C, top up the swap budget by the overflow. No bridge-swap —
995
1209
  // R3 has no destination funds delivery.
996
- if (((_r = segment.sizing) === null || _r === void 0 ? void 0 : _r.category) === 'C' &&
1210
+ if (((_u = segment.sizing) === null || _u === void 0 ? void 0 : _u.category) === 'C' &&
997
1211
  segment.sizing.overflowNativePc > BigInt(0)) {
998
1212
  const bumped = evmInboundNativeValue + segment.sizing.overflowNativePc;
999
1213
  (0, context_1.printLog)(ctx, `composeCascade — INBOUND_FROM_CEA (EVM) Case C: bumping nativeValueForGas from ${evmInboundNativeValue} to ${bumped} (overflow=${segment.sizing.overflowNativePc})`);
@@ -1009,19 +1223,29 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
1009
1223
  gatewayPcAddress,
1010
1224
  outboundRequest: outboundReq,
1011
1225
  });
1226
+ requiredNativeValue += sumMulticallNativeValue(outboundMulticalls);
1012
1227
  // Reset accumulated -- everything is now inside this outbound
1013
1228
  accumulatedPushMulticalls = [...outboundMulticalls];
1014
1229
  break;
1015
1230
  }
1016
1231
  }
1017
1232
  }
1018
- return accumulatedPushMulticalls;
1233
+ return {
1234
+ multicalls: accumulatedPushMulticalls,
1235
+ requiredNativeValue,
1236
+ };
1237
+ });
1238
+ }
1239
+ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
1240
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
1241
+ const result = yield composeCascadeDetailed(ctx, segments, ueaAddress, ueaBalance, ueaNonce);
1242
+ return result.multicalls;
1019
1243
  });
1020
1244
  }
1021
1245
  // ============================================================================
1022
1246
  // createCascadedBuilder
1023
1247
  // ============================================================================
1024
- function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1248
+ function createCascadedBuilder(ctx, preparedTxs, callbacks, defaultEventHook) {
1025
1249
  return {
1026
1250
  send: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
1027
1251
  var _a;
@@ -1031,8 +1255,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1031
1255
  // Classify into segments
1032
1256
  const segments = classifyIntoSegments(hops);
1033
1257
  // Check if this is a single-hop Route 1 (no composition needed)
1034
- if (preparedTxs.length === 1 &&
1035
- preparedTxs[0].route === 'UOA_TO_PUSH') {
1258
+ if (preparedTxs.length === 1 && preparedTxs[0].route === 'UOA_TO_PUSH') {
1036
1259
  const response = yield callbacks.executeFn(hops[0].params);
1037
1260
  const singleRoute1Result = {
1038
1261
  initialTxHash: response.hash,
@@ -1066,8 +1289,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1066
1289
  return singleRoute1Result;
1067
1290
  }
1068
1291
  // Check if single-hop Route 2 (just execute directly)
1069
- if (preparedTxs.length === 1 &&
1070
- preparedTxs[0].route === 'UOA_TO_CEA') {
1292
+ if (preparedTxs.length === 1 && preparedTxs[0].route === 'UOA_TO_CEA') {
1071
1293
  const response = yield callbacks.executeFn(hops[0].params);
1072
1294
  const targetChain = hops[0].targetChain || (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork);
1073
1295
  const singleRoute2Result = {
@@ -1111,8 +1333,11 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1111
1333
  ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress)
1112
1334
  : BigInt(0);
1113
1335
  let composedMulticalls;
1336
+ let cascadeRequiredNativeValue = BigInt(0);
1114
1337
  try {
1115
- composedMulticalls = yield composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonceCascade);
1338
+ const composed = yield composeCascadeDetailed(ctx, segments, ueaAddress, ueaBalance, ueaNonceCascade);
1339
+ composedMulticalls = composed.multicalls;
1340
+ cascadeRequiredNativeValue = composed.requiredNativeValue;
1116
1341
  }
1117
1342
  catch (err) {
1118
1343
  // Pre-flight or per-segment min-swap failure inside composeCascade
@@ -1129,6 +1354,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1129
1354
  // Execute the composed multicall as a single Push Chain tx
1130
1355
  const executeParams = {
1131
1356
  to: ueaAddress,
1357
+ value: cascadeRequiredNativeValue,
1132
1358
  data: composedMulticalls,
1133
1359
  };
1134
1360
  const response = yield callbacks.executeFn(executeParams);
@@ -1136,7 +1362,9 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1136
1362
  const hopInfos = hops.map((hop, index) => ({
1137
1363
  hopIndex: index,
1138
1364
  route: hop.route,
1139
- executionChain: hop.targetChain || hop.sourceChain || (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork),
1365
+ executionChain: hop.targetChain ||
1366
+ hop.sourceChain ||
1367
+ (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork),
1140
1368
  status: 'pending',
1141
1369
  }));
1142
1370
  // Mark first hop as submitted
@@ -1150,8 +1378,8 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1150
1378
  hops: hopInfos,
1151
1379
  hopCount: hops.length,
1152
1380
  waitForAll: (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
1153
- var _a, _b, _c, _d, _e;
1154
- const { pollingIntervalMs = 10000, timeout = 300000, progressHook: cascadeProgressHook, eventHook, } = opts || {};
1381
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1382
+ const { pollingIntervalMs = 10000, timeout = 300000, progressHook: cascadeProgressHook, eventHook = defaultEventHook, } = opts || {};
1155
1383
  const startTime = Date.now();
1156
1384
  // Per-hop progress emission. Fans out cascade markers to BOTH the
1157
1385
  // explicit `eventHook` (passed into waitForAll) AND the init-time
@@ -1180,9 +1408,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1180
1408
  if (!isMulti)
1181
1409
  return;
1182
1410
  const n = hopIndex0 + 1;
1183
- const fromChain = hopIndex0 === 0
1184
- ? signerOriginChain
1185
- : chainLabels[hopIndex0 - 1];
1411
+ const fromChain = hopIndex0 === 0 ? signerOriginChain : chainLabels[hopIndex0 - 1];
1186
1412
  const toChain = chainLabels[hopIndex0];
1187
1413
  emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_002_01](n, totalHops, fromChain, toChain));
1188
1414
  };
@@ -1229,49 +1455,14 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1229
1455
  elapsed: Date.now() - startTime,
1230
1456
  });
1231
1457
  yield response.wait();
1232
- // Mark all Push Chain (Route 1) hops as confirmed
1233
- for (const hop of hopInfos) {
1234
- if (hop.route === 'UOA_TO_PUSH') {
1235
- hop.status = 'confirmed';
1236
- hop.txHash = response.hash;
1237
- cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1238
- hopIndex: hop.hopIndex,
1239
- route: hop.route,
1240
- chain: (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork),
1241
- status: 'confirmed',
1242
- txHash: response.hash,
1243
- elapsed: Date.now() - startTime,
1244
- });
1245
- emitHopComplete(hop.hopIndex);
1246
- }
1247
- }
1248
- // 2. Track outbound hops (Route 2: UOA_TO_CEA)
1249
- // Hops after a CEA_TO_PUSH are "child outbounds" — they execute inside
1250
- // the inbound payload on Push Chain and live under a DIFFERENT utx_id
1251
- // (the inbound UTX, not the parent). We can't track them via the parent
1252
- // utx_id polling, so we auto-confirm them.
1253
- const ceaToPushIndex = hopInfos.findIndex((h) => h.route === 'CEA_TO_PUSH');
1254
- const outboundHops = hopInfos.filter((h, i) => {
1255
- if (h.route !== 'UOA_TO_CEA')
1256
- return false;
1257
- // Child outbounds (after CEA_TO_PUSH) live under the inbound UTX,
1258
- // not the parent — auto-confirm since we can't poll them here.
1259
- if (ceaToPushIndex >= 0 && i > ceaToPushIndex) {
1260
- h.status = 'confirmed';
1261
- cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1262
- hopIndex: h.hopIndex,
1263
- route: h.route,
1264
- chain: h.executionChain,
1265
- status: 'confirmed',
1266
- elapsed: Date.now() - startTime,
1267
- });
1268
- return false;
1269
- }
1270
- return true;
1271
- });
1272
- if (outboundHops.length > 0) {
1458
+ const ceaToPushIndex = hopInfos.findIndex((h, index) => h.route === 'CEA_TO_PUSH' && !isNativeSeedOnlyHop(hops[index]));
1459
+ const isAfterCeaToPush = (index) => ceaToPushIndex >= 0 && index > ceaToPushIndex;
1460
+ const rootOutboundIndexByHop = buildRootOutboundIndexByHop(segments, hops);
1461
+ const trackOutboundHops = (outboundHops, pushTxHash, resolvedSubTxId, outboundIndexByHop) => tslib_1.__awaiter(this, void 0, void 0, function* () {
1462
+ var _a, _b, _c, _d;
1463
+ if (outboundHops.length === 0)
1464
+ return null;
1273
1465
  if (outboundHops.length === 1) {
1274
- // Single direct outbound hop: use the existing V1-based tracking
1275
1466
  const hop = outboundHops[0];
1276
1467
  const hopHooks = (0, progress_route_hooks_1.pickWaitHooks)(hop.route);
1277
1468
  const remainingTimeout = timeout - (Date.now() - startTime);
@@ -1303,11 +1494,11 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1303
1494
  });
1304
1495
  let lastEmittedStatus;
1305
1496
  try {
1306
- const outboundDetails = yield callbacks.waitForOutboundTxFn(response.hash, {
1307
- initialWaitMs: Math.min(60000, remainingTimeout),
1308
- pollingIntervalMs,
1309
- timeout: remainingTimeout,
1310
- progressHook: (event) => {
1497
+ const outboundDetails = yield callbacks.waitForOutboundTxFn(pushTxHash, Object.assign(Object.assign({ initialWaitMs: Math.min(60000, remainingTimeout), pollingIntervalMs, timeout: remainingTimeout, _resolvedSubTxId: resolvedSubTxId, _expectedDestinationChain: hop.executionChain }, (outboundIndexByHop
1498
+ ? {
1499
+ _outboundIndex: (_a = outboundIndexByHop.get(hop.hopIndex)) !== null && _a !== void 0 ? _a : 0,
1500
+ }
1501
+ : {})), { progressHook: (event) => {
1311
1502
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1312
1503
  hopIndex: hop.hopIndex,
1313
1504
  route: hop.route,
@@ -1320,19 +1511,25 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1320
1511
  lastEmittedStatus = 'polling';
1321
1512
  emitHopEvent(hopHooks.polling(hop.executionChain, event.elapsed));
1322
1513
  }
1323
- },
1324
- });
1514
+ } }));
1515
+ // `outboundDetails.externalTxHash` from waitForOutboundTx
1516
+ // is the raw `0x`-hex form (internal canonical). For the
1517
+ // user-facing `hop.txHash`, `hop.outboundDetails`, and
1518
+ // cascade progress event, normalize to base58 for SVM so
1519
+ // consumers can paste it straight into a Solana explorer.
1520
+ const displayTxHash = (_b = (0, helpers_1.toExternalTxHashDisplay)(outboundDetails.destinationChain, outboundDetails.externalTxHash)) !== null && _b !== void 0 ? _b : outboundDetails.externalTxHash;
1521
+ const userFacingOutboundDetails = Object.assign(Object.assign({}, outboundDetails), { externalTxHash: displayTxHash });
1325
1522
  hop.status = 'confirmed';
1326
- hop.txHash = outboundDetails.externalTxHash;
1327
- hop.outboundDetails = outboundDetails;
1328
- emitHopEvent(hopHooks.success(outboundDetails));
1523
+ hop.txHash = displayTxHash;
1524
+ hop.outboundDetails = userFacingOutboundDetails;
1525
+ emitHopEvent(hopHooks.success(userFacingOutboundDetails));
1329
1526
  emitHopComplete(hop.hopIndex);
1330
1527
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1331
1528
  hopIndex: hop.hopIndex,
1332
1529
  route: hop.route,
1333
1530
  chain: hop.executionChain,
1334
1531
  status: 'confirmed',
1335
- txHash: outboundDetails.externalTxHash,
1532
+ txHash: displayTxHash,
1336
1533
  elapsed: Date.now() - startTime,
1337
1534
  });
1338
1535
  }
@@ -1364,19 +1561,17 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1364
1561
  }
1365
1562
  }
1366
1563
  else {
1367
- // Multiple outbound hops: use V2 API which returns outboundTx[]
1368
- // Emit "Awaiting External Chain" once per outbound hop before
1369
- // polling begins, using the route-specific awaiting hook.
1370
1564
  for (const outHop of outboundHops) {
1371
1565
  emitHopStart(outHop.hopIndex);
1372
1566
  emitHopEvent((0, progress_route_hooks_1.pickWaitHooks)(outHop.route).awaiting(outHop.executionChain));
1373
1567
  }
1374
1568
  const perHopLastStatus = new Map();
1375
1569
  const confirmedHops = new Set();
1376
- const allOutboundDetails = yield callbacks.waitForAllOutboundTxsFn(response.hash, outboundHops, {
1570
+ const allOutboundDetails = yield callbacks.waitForAllOutboundTxsFn(pushTxHash, outboundHops, {
1377
1571
  initialWaitMs: Math.min(60000, timeout - (Date.now() - startTime)),
1378
1572
  pollingIntervalMs,
1379
1573
  timeout: timeout - (Date.now() - startTime),
1574
+ _resolvedSubTxId: resolvedSubTxId,
1380
1575
  progressHook: (event) => {
1381
1576
  var _a, _b;
1382
1577
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
@@ -1390,8 +1585,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1390
1585
  const matchedHop = outboundHops.find((h) => h.hopIndex === event.hopIndex);
1391
1586
  const eventHooks = (0, progress_route_hooks_1.pickWaitHooks)(matchedHop === null || matchedHop === void 0 ? void 0 : matchedHop.route);
1392
1587
  const prev = perHopLastStatus.get(event.hopIndex);
1393
- if (event.status === 'polling' &&
1394
- prev !== 'polling') {
1588
+ if (event.status === 'polling' && prev !== 'polling') {
1395
1589
  perHopLastStatus.set(event.hopIndex, 'polling');
1396
1590
  emitHopEvent(eventHooks.polling((_b = (_a = matchedHop === null || matchedHop === void 0 ? void 0 : matchedHop.executionChain) !== null && _a !== void 0 ? _a : event.chain) !== null && _b !== void 0 ? _b : '', Date.now() - startTime));
1397
1591
  }
@@ -1412,12 +1606,12 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1412
1606
  },
1413
1607
  });
1414
1608
  if (!allOutboundDetails.success) {
1415
- const failedIdx = (_d = allOutboundDetails.failedAt) !== null && _d !== void 0 ? _d : 0;
1609
+ const failedIdx = (_c = allOutboundDetails.failedAt) !== null && _c !== void 0 ? _c : 0;
1416
1610
  const failedHop = outboundHops.find((h) => h.hopIndex === failedIdx);
1417
1611
  const failMsg = failedHop
1418
1612
  ? `Outbound failed for hop ${failedIdx} on ${failedHop.executionChain}`
1419
1613
  : `Outbound cascade failed at hop ${failedIdx}`;
1420
- emitHopEvent((0, progress_route_hooks_1.pickWaitHooks)(failedHop === null || failedHop === void 0 ? void 0 : failedHop.route).failed((_e = failedHop === null || failedHop === void 0 ? void 0 : failedHop.executionChain) !== null && _e !== void 0 ? _e : '', failMsg));
1614
+ emitHopEvent((0, progress_route_hooks_1.pickWaitHooks)(failedHop === null || failedHop === void 0 ? void 0 : failedHop.route).failed((_d = failedHop === null || failedHop === void 0 ? void 0 : failedHop.executionChain) !== null && _d !== void 0 ? _d : '', failMsg));
1421
1615
  emitCascadeFailed(failedIdx, failMsg);
1422
1616
  return {
1423
1617
  success: false,
@@ -1426,23 +1620,230 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1426
1620
  };
1427
1621
  }
1428
1622
  }
1623
+ return null;
1624
+ });
1625
+ // Mark only root Push Chain (Route 1) hops as confirmed. Route 1
1626
+ // hops after a Route 3 leg execute in the child inbound Push tx and
1627
+ // must wait for that child UTX to reach terminal success.
1628
+ for (const hop of hopInfos) {
1629
+ if (hop.route === 'CEA_TO_PUSH' &&
1630
+ isNativeSeedOnlyHop(hops[hop.hopIndex])) {
1631
+ emitHopStart(hop.hopIndex);
1632
+ hop.status = 'confirmed';
1633
+ hop.txHash = response.hash;
1634
+ cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1635
+ hopIndex: hop.hopIndex,
1636
+ route: hop.route,
1637
+ chain: (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork),
1638
+ status: 'confirmed',
1639
+ txHash: response.hash,
1640
+ elapsed: Date.now() - startTime,
1641
+ });
1642
+ emitHopComplete(hop.hopIndex);
1643
+ }
1429
1644
  }
1430
- // 3. Route 3 (CEA_TO_PUSH) tracking - mark as submitted
1431
- // Cascade does not poll inbound completion; we surface the hop-start
1432
- // marker so consumers see a "Starting tx N" event for the inbound,
1433
- // but skip emitHopComplete since we never observe terminal state.
1434
- const inboundHops = hopInfos.filter((h) => h.route === 'CEA_TO_PUSH');
1435
- for (const inboundHop of inboundHops) {
1436
- inboundHop.status = 'submitted';
1645
+ for (const hop of hopInfos) {
1646
+ if (hop.route === 'UOA_TO_PUSH' && !isAfterCeaToPush(hop.hopIndex)) {
1647
+ hop.status = 'confirmed';
1648
+ hop.txHash = response.hash;
1649
+ cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1650
+ hopIndex: hop.hopIndex,
1651
+ route: hop.route,
1652
+ chain: (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork),
1653
+ status: 'confirmed',
1654
+ txHash: response.hash,
1655
+ elapsed: Date.now() - startTime,
1656
+ });
1657
+ emitHopComplete(hop.hopIndex);
1658
+ }
1659
+ }
1660
+ // 2. Track direct outbound hops (Route 2: UOA_TO_CEA) that belong
1661
+ // to the root Push UTX.
1662
+ const rootOutboundHops = hopInfos.filter((h) => h.route === 'UOA_TO_CEA' && !isAfterCeaToPush(h.hopIndex));
1663
+ const rootOutboundFailure = yield trackOutboundHops(rootOutboundHops, response.hash, undefined, rootOutboundIndexByHop);
1664
+ if (rootOutboundFailure)
1665
+ return rootOutboundFailure;
1666
+ let childInbound;
1667
+ if (ceaToPushIndex >= 0) {
1668
+ const inboundHop = hopInfos[ceaToPushIndex];
1669
+ const hopHooks = (0, progress_route_hooks_1.pickWaitHooks)(inboundHop.route);
1670
+ const remainingTimeout = timeout - (Date.now() - startTime);
1671
+ if (remainingTimeout <= 0) {
1672
+ inboundHop.status = 'failed';
1673
+ emitHopEvent(hopHooks.timeout(inboundHop.executionChain, Date.now() - startTime));
1674
+ emitCascadeTimeout(inboundHop.hopIndex);
1675
+ return {
1676
+ success: false,
1677
+ hops: hopInfos,
1678
+ failedAt: inboundHop.hopIndex,
1679
+ };
1680
+ }
1437
1681
  emitHopStart(inboundHop.hopIndex);
1682
+ emitHopEvent(hopHooks.awaiting(inboundHop.executionChain));
1683
+ cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1684
+ hopIndex: inboundHop.hopIndex,
1685
+ route: inboundHop.route,
1686
+ chain: inboundHop.executionChain,
1687
+ status: 'polling',
1688
+ elapsed: Date.now() - startTime,
1689
+ });
1690
+ let outboundLastStatus;
1691
+ let sourceOutboundDetails;
1692
+ try {
1693
+ sourceOutboundDetails = yield callbacks.waitForOutboundTxFn(response.hash, {
1694
+ initialWaitMs: Math.min(60000, remainingTimeout),
1695
+ pollingIntervalMs,
1696
+ timeout: remainingTimeout,
1697
+ _expectedDestinationChain: inboundHop.executionChain,
1698
+ _outboundIndex: (_d = rootOutboundIndexByHop.get(inboundHop.hopIndex)) !== null && _d !== void 0 ? _d : 0,
1699
+ progressHook: (event) => {
1700
+ cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1701
+ hopIndex: inboundHop.hopIndex,
1702
+ route: inboundHop.route,
1703
+ chain: inboundHop.executionChain,
1704
+ status: event.status,
1705
+ elapsed: Date.now() - startTime,
1706
+ });
1707
+ if (event.status === 'polling' &&
1708
+ outboundLastStatus !== 'polling') {
1709
+ outboundLastStatus = 'polling';
1710
+ emitHopEvent(hopHooks.polling(inboundHop.executionChain, event.elapsed));
1711
+ }
1712
+ },
1713
+ });
1714
+ }
1715
+ catch (err) {
1716
+ const errMsg = err instanceof Error ? err.message : String(err);
1717
+ const isTimeout = errMsg.startsWith('Timeout waiting for outbound transaction');
1718
+ emitHopEvent(isTimeout
1719
+ ? hopHooks.timeout(inboundHop.executionChain, remainingTimeout)
1720
+ : hopHooks.failed(inboundHop.executionChain, errMsg));
1721
+ if (isTimeout)
1722
+ emitCascadeTimeout(inboundHop.hopIndex);
1723
+ else
1724
+ emitCascadeFailed(inboundHop.hopIndex, errMsg);
1725
+ inboundHop.status = 'failed';
1726
+ const failedExternalTxHash = err === null || err === void 0 ? void 0 : err.externalTxHash;
1727
+ const failedDisplayTxHash = (_e = (0, helpers_1.toExternalTxHashDisplay)(inboundHop.executionChain, failedExternalTxHash)) !== null && _e !== void 0 ? _e : failedExternalTxHash;
1728
+ if (failedDisplayTxHash)
1729
+ inboundHop.txHash = failedDisplayTxHash;
1730
+ cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1731
+ hopIndex: inboundHop.hopIndex,
1732
+ route: inboundHop.route,
1733
+ chain: inboundHop.executionChain,
1734
+ status: 'failed',
1735
+ txHash: failedDisplayTxHash,
1736
+ elapsed: Date.now() - startTime,
1737
+ });
1738
+ return {
1739
+ success: false,
1740
+ hops: hopInfos,
1741
+ failedAt: inboundHop.hopIndex,
1742
+ };
1743
+ }
1744
+ const displayExternalTxHash = (_f = (0, helpers_1.toExternalTxHashDisplay)(sourceOutboundDetails.destinationChain, sourceOutboundDetails.externalTxHash)) !== null && _f !== void 0 ? _f : sourceOutboundDetails.externalTxHash;
1745
+ inboundHop.outboundDetails = Object.assign(Object.assign({}, sourceOutboundDetails), { externalTxHash: displayExternalTxHash });
1746
+ inboundHop.txHash = displayExternalTxHash;
1747
+ emitHopEvent(hopHooks.success(inboundHop.outboundDetails));
1748
+ emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_310_01](inboundHop.executionChain));
1749
+ let inboundLastStatus;
1750
+ const inboundRemainingTimeout = timeout - (Date.now() - startTime);
1751
+ try {
1752
+ childInbound = yield ((_g = callbacks.waitForInboundPushTxFn) !== null && _g !== void 0 ? _g : ((externalTxHash, sourceChain, opts) => (0, inbound_tracker_1.waitForInboundPushTx)(ctx, externalTxHash, sourceChain, opts)))(sourceOutboundDetails.externalTxHash, inboundHop.executionChain, {
1753
+ initialWaitMs: Math.min(60000, inboundRemainingTimeout),
1754
+ pollingIntervalMs,
1755
+ timeout: inboundRemainingTimeout,
1756
+ progressHook: (event) => {
1757
+ if (event.status === 'polling' &&
1758
+ inboundLastStatus !== 'polling') {
1759
+ inboundLastStatus = 'polling';
1760
+ emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_310_02](inboundHop.executionChain, event.elapsedMs));
1761
+ }
1762
+ },
1763
+ });
1764
+ }
1765
+ catch (err) {
1766
+ const errMsg = err instanceof Error ? err.message : String(err);
1767
+ const isTimeout = err instanceof inbound_tracker_1.InboundTimeoutError;
1768
+ emitHopEvent(isTimeout
1769
+ ? progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_399_03](inboundHop.executionChain, inboundRemainingTimeout, 'inbound')
1770
+ : progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_399_02](errMsg, 'inbound', inboundHop.executionChain));
1771
+ if (isTimeout)
1772
+ emitCascadeTimeout(inboundHop.hopIndex);
1773
+ else
1774
+ emitCascadeFailed(inboundHop.hopIndex, errMsg);
1775
+ inboundHop.status = 'failed';
1776
+ cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1777
+ hopIndex: inboundHop.hopIndex,
1778
+ route: inboundHop.route,
1779
+ chain: inboundHop.executionChain,
1780
+ status: 'failed',
1781
+ elapsed: Date.now() - startTime,
1782
+ });
1783
+ return {
1784
+ success: false,
1785
+ hops: hopInfos,
1786
+ failedAt: inboundHop.hopIndex,
1787
+ };
1788
+ }
1789
+ if (childInbound.status !== 'confirmed') {
1790
+ const failMsg = (_h = childInbound.errorMessage) !== null && _h !== void 0 ? _h : 'inbound execution failed on Push Chain';
1791
+ inboundHop.status = 'failed';
1792
+ inboundHop.txHash =
1793
+ childInbound.pushTxHash || displayExternalTxHash;
1794
+ emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_399_02](failMsg, 'inbound', inboundHop.executionChain));
1795
+ emitCascadeFailed(inboundHop.hopIndex, failMsg);
1796
+ cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1797
+ hopIndex: inboundHop.hopIndex,
1798
+ route: inboundHop.route,
1799
+ chain: inboundHop.executionChain,
1800
+ status: 'failed',
1801
+ txHash: inboundHop.txHash,
1802
+ elapsed: Date.now() - startTime,
1803
+ });
1804
+ return {
1805
+ success: false,
1806
+ hops: hopInfos,
1807
+ failedAt: inboundHop.hopIndex,
1808
+ };
1809
+ }
1810
+ inboundHop.status = 'confirmed';
1811
+ inboundHop.txHash =
1812
+ childInbound.pushTxHash || displayExternalTxHash;
1813
+ emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_399_01](inboundHop.executionChain, childInbound.pushTxHash));
1814
+ emitHopComplete(inboundHop.hopIndex);
1438
1815
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1439
1816
  hopIndex: inboundHop.hopIndex,
1440
1817
  route: inboundHop.route,
1441
1818
  chain: inboundHop.executionChain,
1442
- status: 'waiting',
1819
+ status: 'confirmed',
1820
+ txHash: inboundHop.txHash,
1443
1821
  elapsed: Date.now() - startTime,
1444
1822
  });
1445
1823
  }
1824
+ if (childInbound) {
1825
+ for (const hop of hopInfos) {
1826
+ if (hop.route === 'UOA_TO_PUSH' &&
1827
+ isAfterCeaToPush(hop.hopIndex)) {
1828
+ emitHopStart(hop.hopIndex);
1829
+ hop.status = 'confirmed';
1830
+ hop.txHash = childInbound.pushTxHash;
1831
+ cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1832
+ hopIndex: hop.hopIndex,
1833
+ route: hop.route,
1834
+ chain: (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork),
1835
+ status: 'confirmed',
1836
+ txHash: childInbound.pushTxHash,
1837
+ elapsed: Date.now() - startTime,
1838
+ });
1839
+ emitHopComplete(hop.hopIndex);
1840
+ }
1841
+ }
1842
+ const childOutboundHops = hopInfos.filter((h) => h.route === 'UOA_TO_CEA' && isAfterCeaToPush(h.hopIndex));
1843
+ const childOutboundFailure = yield trackOutboundHops(childOutboundHops, childInbound.pushTxHash || response.hash, childInbound.childUtxId);
1844
+ if (childOutboundFailure)
1845
+ return childOutboundFailure;
1846
+ }
1446
1847
  emitCascadeSuccess();
1447
1848
  return { success: true, hops: hopInfos };
1448
1849
  }