@pushchain/core 5.1.5-alpha.0 → 5.1.5

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 +19 -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 +556 -225
  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 +54 -0
  19. package/src/lib/orchestrator/internals/execute-funds-only.js +47 -21
  20. package/src/lib/orchestrator/internals/execute-funds-payload.js +53 -31
  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 +252 -53
  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 +120 -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 +555 -101
  57. package/src/lib/progress-hook/progress-hook.types.d.ts +76 -24
  58. package/src/lib/progress-hook/progress-hook.types.js +74 -29
  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
@@ -22,11 +22,16 @@ const route_detector_1 = require("../route-detector");
22
22
  const cea_utils_1 = require("../cea-utils");
23
23
  const payload_builders_1 = require("../payload-builders");
24
24
  const push_chain_1 = require("../../push-chain/push-chain");
25
+ const context_1 = require("./context");
25
26
  const helpers_1 = require("./helpers");
26
27
  const payload_builder_1 = require("./payload-builder");
27
28
  const uea_manager_1 = require("./uea-manager");
28
29
  const gas_calculator_1 = require("./gas-calculator");
30
+ const abi_1 = require("../../constants/abi");
29
31
  const route_handlers_1 = require("./route-handlers");
32
+ const progress_route_hooks_1 = require("./progress-route-hooks");
33
+ const progress_hook_1 = tslib_1.__importDefault(require("../../progress-hook/progress-hook"));
34
+ const progress_hook_types_1 = require("../../progress-hook/progress-hook.types");
30
35
  const build_payload_1 = require("../svm-idl/build-payload");
31
36
  // ============================================================================
32
37
  // prepareTransaction
@@ -131,21 +136,29 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
131
136
  burnAmount = params.value;
132
137
  }
133
138
  else if (hasSvmExecute) {
139
+ // PAYLOAD-only SVM execute: still need native PRC-20 for chain namespace
140
+ // + gas fees, but burn amount must be zero. Matches executeUoaToCeaSvm
141
+ // (route-handlers.ts) and the EVM cascade branch below.
142
+ // Using burnAmount=1 would trigger transferFrom on the PRC-20 token,
143
+ // which reverts with InsufficientBalance when the UEA has no pSOL balance.
134
144
  prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, ctx.pushNetwork);
135
- burnAmount = BigInt(1);
145
+ burnAmount = BigInt(0);
136
146
  }
137
147
  let gasToken = selectors_1.ZERO_ADDRESS;
138
148
  let gasFee = BigInt(0);
149
+ let sizing;
139
150
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
140
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
151
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, targetChain);
141
152
  gasToken = result.gasToken;
142
153
  gasFee = result.gasFee;
154
+ sizing = result.sizing;
143
155
  }
144
156
  return Object.assign(Object.assign({}, baseDescriptor), { targetChain, isSvmTarget: true, svmPayload,
145
157
  prc20Token,
146
158
  burnAmount,
147
159
  gasToken,
148
- gasFee });
160
+ gasFee,
161
+ sizing });
149
162
  }
150
163
  // EVM path: Resolve CEA address + build CEA multicalls
151
164
  const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_c = ctx.rpcUrls[targetChain]) === null || _c === void 0 ? void 0 : _c[0]);
@@ -155,16 +168,18 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
155
168
  const burnAmount = BigInt(0); // Migration is logic-only — no funds. CEA rejects msg.value != 0.
156
169
  let gasToken = selectors_1.ZERO_ADDRESS;
157
170
  let gasFee = BigInt(0);
171
+ let sizing;
158
172
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
159
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
173
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, targetChain);
160
174
  gasToken = result.gasToken;
161
175
  gasFee = result.gasFee;
176
+ sizing = result.sizing;
162
177
  }
163
178
  return Object.assign(Object.assign({}, baseDescriptor), { targetChain,
164
179
  ceaAddress, ceaMulticalls: [], prc20Token,
165
180
  burnAmount,
166
181
  gasToken,
167
- gasFee, isMigration: true });
182
+ gasFee, isMigration: true, sizing });
168
183
  }
169
184
  // Build CEA multicalls
170
185
  const ceaMulticalls = [];
@@ -238,10 +253,12 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
238
253
  // Query gas fee
239
254
  let gasToken = selectors_1.ZERO_ADDRESS;
240
255
  let gasFee = BigInt(0);
256
+ let sizing;
241
257
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
242
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
258
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, targetChain);
243
259
  gasToken = result.gasToken;
244
260
  gasFee = result.gasFee;
261
+ sizing = result.sizing;
245
262
  }
246
263
  return Object.assign(Object.assign({}, baseDescriptor), { targetChain,
247
264
  ceaAddress,
@@ -249,7 +266,8 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
249
266
  prc20Token,
250
267
  burnAmount,
251
268
  gasToken,
252
- gasFee });
269
+ gasFee,
270
+ sizing });
253
271
  }
254
272
  case route_detector_1.TransactionRoute.CEA_TO_PUSH: {
255
273
  // Route 3: Build CEA multicalls for sendUniversalTxFromCEA
@@ -290,13 +308,16 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
290
308
  }
291
309
  let gasToken = selectors_1.ZERO_ADDRESS;
292
310
  let gasFee = BigInt(0);
311
+ let sizing;
293
312
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
294
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
313
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, sourceChain);
295
314
  gasToken = result.gasToken;
296
315
  gasFee = result.gasFee;
316
+ sizing = result.sizing;
297
317
  }
298
318
  return Object.assign(Object.assign({}, baseDescriptor), { sourceChain, ceaAddress: ceaPdaHex, isSvmTarget: true, prc20Token, burnAmount: amount, gasToken,
299
- gasFee });
319
+ gasFee,
320
+ sizing });
300
321
  }
301
322
  const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_h = ctx.rpcUrls[sourceChain]) === null || _h === void 0 ? void 0 : _h[0]);
302
323
  // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
@@ -326,15 +347,18 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
326
347
  const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
327
348
  let gasToken = selectors_1.ZERO_ADDRESS;
328
349
  let gasFee = BigInt(0);
350
+ let sizing;
329
351
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
330
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
352
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, sourceChain);
331
353
  gasToken = result.gasToken;
332
354
  gasFee = result.gasFee;
355
+ sizing = result.sizing;
333
356
  }
334
357
  return Object.assign(Object.assign({}, baseDescriptor), { sourceChain,
335
358
  ceaAddress,
336
359
  prc20Token, burnAmount: amount, gasToken,
337
- gasFee });
360
+ gasFee,
361
+ sizing });
338
362
  }
339
363
  default:
340
364
  return baseDescriptor;
@@ -354,10 +378,20 @@ function classifyIntoSegments(hops) {
354
378
  const chain = hop.targetChain || hop.sourceChain;
355
379
  const canMerge = currentSegment &&
356
380
  currentSegment.type === segType &&
357
- // Same-chain merging for OUTBOUND_TO_CEA (EVM only — SVM hops are atomic)
381
+ // Same-chain merging for OUTBOUND_TO_CEA (EVM only — SVM hops are atomic).
382
+ // Same-source merging for INBOUND_FROM_CEA (EVM only) — multiple R3 hops
383
+ // that share a source chain collapse into ONE CEA→UEA round-trip whose
384
+ // inbound multicall runs every hop's target op. Without this merge the
385
+ // cascade composer would nest the second R3 as an outbound from inside
386
+ // the first inbound's UEA multicall, which fails inside UGPC.
387
+ // sendUniversalTxOutbound because the UEA holds no PRC20 at that point
388
+ // (see cascade.ts composeCascade INBOUND_FROM_CEA branch).
358
389
  (segType === 'OUTBOUND_TO_CEA'
359
390
  ? currentSegment.targetChain === hop.targetChain && !hop.isSvmTarget
360
- : segType === 'PUSH_EXECUTION');
391
+ : segType === 'INBOUND_FROM_CEA'
392
+ ? currentSegment.sourceChain === hop.sourceChain &&
393
+ !(0, payload_builders_1.isSvmChain)(hop.sourceChain)
394
+ : segType === 'PUSH_EXECUTION');
361
395
  if (canMerge && currentSegment) {
362
396
  // Merge into current segment
363
397
  currentSegment.hops.push(hop);
@@ -377,12 +411,36 @@ function classifyIntoSegments(hops) {
377
411
  currentSegment.gasFee =
378
412
  (currentSegment.gasFee || BigInt(0)) + (hop.gasFee || BigInt(0));
379
413
  }
414
+ else if (segType === 'INBOUND_FROM_CEA') {
415
+ // Sum burn amounts so the single sendUniversalTxToUEA carries the
416
+ // combined native value forwarded from CEA back to the UEA.
417
+ currentSegment.totalBurnAmount =
418
+ (currentSegment.totalBurnAmount || BigInt(0)) +
419
+ (hop.burnAmount || BigInt(0));
420
+ if (hop.gasLimit > (currentSegment.gasLimit || BigInt(0))) {
421
+ currentSegment.gasLimit = hop.gasLimit;
422
+ }
423
+ currentSegment.gasFee =
424
+ (currentSegment.gasFee || BigInt(0)) + (hop.gasFee || BigInt(0));
425
+ }
380
426
  else if (segType === 'PUSH_EXECUTION') {
381
427
  currentSegment.mergedPushMulticalls = [
382
428
  ...(currentSegment.mergedPushMulticalls || []),
383
429
  ...(hop.pushMulticalls || []),
384
430
  ];
385
431
  }
432
+ // Merge sizing: take the strictest category (C > B > A). Overflow
433
+ // from a Case C hop wins so the segment's msg.value covers the
434
+ // highest-cost hop in the merge.
435
+ if (hop.sizing) {
436
+ if (!currentSegment.sizing) {
437
+ currentSegment.sizing = hop.sizing;
438
+ }
439
+ else if (categoryRank(hop.sizing.category) >
440
+ categoryRank(currentSegment.sizing.category)) {
441
+ currentSegment.sizing = hop.sizing;
442
+ }
443
+ }
386
444
  }
387
445
  else {
388
446
  // Start a new segment
@@ -402,12 +460,17 @@ function classifyIntoSegments(hops) {
402
460
  gasToken: hop.gasToken,
403
461
  gasFee: hop.gasFee,
404
462
  gasLimit: hop.gasLimit,
463
+ sizing: hop.sizing,
405
464
  };
406
465
  segments.push(currentSegment);
407
466
  }
408
467
  }
409
468
  return segments;
410
469
  }
470
+ /** Ordering for sizing-category severity (higher = stricter). */
471
+ function categoryRank(c) {
472
+ return c === 'C' ? 2 : c === 'B' ? 1 : 0;
473
+ }
411
474
  // ============================================================================
412
475
  // getSegmentType
413
476
  // ============================================================================
@@ -427,237 +490,365 @@ function getSegmentType(route) {
427
490
  // composeCascade
428
491
  // ============================================================================
429
492
  function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
430
- var _a, _b, _c, _d, _e, _f, _g;
431
- let accumulatedPushMulticalls = [];
432
- const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
433
- // Compute per-outbound nativeValueForGas from UEA balance
434
- // Each outbound segment needs native value for the gas swap on the destination chain.
435
- // The contract refunds excess, so over-allocating is safe.
436
- const numOutbounds = segments.filter((s) => s.type !== 'PUSH_EXECUTION').length;
437
- const CASCADE_GAS_RESERVE = BigInt(3e18); // 3 PC reserve for gas costs
438
- let perOutboundNativeValue;
439
- if (ueaBalance && numOutbounds > 0 && ueaBalance > CASCADE_GAS_RESERVE) {
440
- perOutboundNativeValue =
441
- (ueaBalance - CASCADE_GAS_RESERVE) / BigInt(numOutbounds);
442
- }
443
- for (let i = segments.length - 1; i >= 0; i--) {
444
- const segment = segments[i];
445
- switch (segment.type) {
446
- case 'PUSH_EXECUTION': {
447
- // Prepend Push Chain multicalls to accumulated
448
- accumulatedPushMulticalls = [
449
- ...(segment.mergedPushMulticalls || []),
450
- ...accumulatedPushMulticalls,
451
- ];
452
- break;
493
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
494
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
495
+ let accumulatedPushMulticalls = [];
496
+ const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
497
+ // Compute per-outbound nativeValueForGas from UEA balance
498
+ // Each outbound segment needs native value for the gas swap on the destination chain.
499
+ // The contract refunds excess, so over-allocating is safe.
500
+ const numOutbounds = segments.filter((s) => s.type !== 'PUSH_EXECUTION').length;
501
+ const CASCADE_GAS_RESERVE = BigInt(3e18); // 3 PC reserve for gas costs
502
+ let perOutboundNativeValue;
503
+ if (ueaBalance && numOutbounds > 0 && ueaBalance > CASCADE_GAS_RESERVE) {
504
+ perOutboundNativeValue =
505
+ (ueaBalance - CASCADE_GAS_RESERVE) / BigInt(numOutbounds);
506
+ }
507
+ // Pool-price-based native-value override for SVM outbounds.
508
+ // The flat split above works for EVM gas tokens (pBNB/pETH — cheap per unit),
509
+ // but pSOL has a much higher WPC/pSOL pool price, so the flat 1 PC split is
510
+ // insufficient to swap for gasFee wei of pSOL and Uniswap reverts "STF".
511
+ // Mirror executeUoaToCeaSvm's pool-price read (route-handlers.ts) per SVM segment.
512
+ const svmSegments = segments.filter((s) => {
513
+ var _a;
514
+ return s.type === 'OUTBOUND_TO_CEA' &&
515
+ ((_a = s.hops[0]) === null || _a === void 0 ? void 0 : _a.isSvmTarget) === true &&
516
+ s.gasToken &&
517
+ s.gasFee &&
518
+ s.gasFee > BigInt(0);
519
+ });
520
+ const svmNativeValueBySegment = new Map();
521
+ if (svmSegments.length > 0 && ueaBalance && ueaBalance > CASCADE_GAS_RESERVE) {
522
+ const universalCoreAddress = yield ctx.pushClient.readContract({
523
+ address: gatewayPcAddress,
524
+ abi: abi_1.UNIVERSAL_GATEWAY_PC,
525
+ functionName: 'UNIVERSAL_CORE',
526
+ args: [],
527
+ });
528
+ // Compute per-SVM budget = ueaBalance − (PC needed for EVM outbounds) − 1 PC safety
529
+ // (outer-tx gas + refund headroom). Pass a huge accountBalance to the estimator so
530
+ // its internal cap is effectively disabled; we cap externally to the per-SVM budget.
531
+ const numEvmOutbounds = numOutbounds - svmSegments.length;
532
+ const evmReservation = perOutboundNativeValue && numEvmOutbounds > 0
533
+ ? perOutboundNativeValue * BigInt(numEvmOutbounds)
534
+ : BigInt(0);
535
+ const outerSafety = BigInt(1e18); // 1 PC for outer-tx gas
536
+ const svmTotal = ueaBalance > evmReservation + outerSafety
537
+ ? ueaBalance - evmReservation - outerSafety
538
+ : BigInt(0);
539
+ const perSvmCap = svmTotal / BigInt(svmSegments.length);
540
+ const UNCAPPED_BALANCE = BigInt(1e30); // effectively disable estimator's internal cap
541
+ for (const seg of svmSegments) {
542
+ const estimated = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, seg.gasToken, seg.gasFee, UNCAPPED_BALANCE);
543
+ const flat = perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : BigInt(0);
544
+ let value = estimated > flat ? estimated : flat;
545
+ if (perSvmCap > BigInt(0) && value > perSvmCap)
546
+ value = perSvmCap;
547
+ (0, context_1.printLog)(ctx, `composeCascade — SVM outbound to ${(_a = seg.hops[0]) === null || _a === void 0 ? void 0 : _a.targetChain}: gasFee=${seg.gasFee}, estimatedWpc=${estimated}, flat=${flat}, perSvmCap=${perSvmCap}, chosen=${value}`);
548
+ svmNativeValueBySegment.set(seg, value);
453
549
  }
454
- case 'OUTBOUND_TO_CEA': {
455
- const firstHop = segment.hops[0];
456
- const isSvmSegment = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.isSvmTarget) === true;
457
- let outboundPayload;
458
- let targetForOutbound;
459
- const isMigration = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.isMigration) === true;
460
- if (isSvmSegment) {
461
- // SVM: use the pre-built SVM payload from the hop descriptor
462
- outboundPayload = (_a = firstHop.svmPayload) !== null && _a !== void 0 ? _a : '0x';
463
- // For SVM, to.address IS the target (program for execute, recipient for withdraw)
464
- const svmTarget = firstHop.params.to;
465
- targetForOutbound = svmTarget.address;
466
- }
467
- else if (isMigration) {
468
- // Migration: use raw 4-byte MIGRATION_SELECTOR, no multicall wrapping
469
- outboundPayload = (0, payload_builders_1.buildMigrationPayload)();
470
- targetForOutbound = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.ceaAddress) || ueaAddress;
471
- }
472
- else {
473
- // EVM: build CEA payload from merged multicalls
474
- outboundPayload = (0, payload_builders_1.buildCeaMulticallPayload)(segment.mergedCeaMulticalls || []);
475
- // Get CEA address from the first hop
476
- targetForOutbound = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.ceaAddress) || ueaAddress;
550
+ }
551
+ for (let i = segments.length - 1; i >= 0; i--) {
552
+ const segment = segments[i];
553
+ switch (segment.type) {
554
+ case 'PUSH_EXECUTION': {
555
+ // Prepend Push Chain multicalls to accumulated
556
+ accumulatedPushMulticalls = [
557
+ ...(segment.mergedPushMulticalls || []),
558
+ ...accumulatedPushMulticalls,
559
+ ];
560
+ break;
477
561
  }
478
- // Build outbound request
479
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetForOutbound, segment.prc20Token || selectors_1.ZERO_ADDRESS, segment.totalBurnAmount || BigInt(0), (_b = segment.gasLimit) !== null && _b !== void 0 ? _b : BigInt(0), outboundPayload, ueaAddress);
480
- // Build approval + outbound multicalls
481
- const segGasFee = segment.gasFee || BigInt(0);
482
- const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
483
- prc20Token: segment.prc20Token || selectors_1.ZERO_ADDRESS,
484
- gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
485
- burnAmount: segment.totalBurnAmount || BigInt(0),
486
- gasFee: segGasFee,
487
- nativeValueForGas: perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : segGasFee * BigInt(1000),
488
- gatewayPcAddress,
489
- outboundRequest: outboundReq,
490
- });
491
- // Prepend to accumulated
492
- accumulatedPushMulticalls = [
493
- ...outboundMulticalls,
494
- ...accumulatedPushMulticalls,
495
- ];
496
- break;
497
- }
498
- case 'INBOUND_FROM_CEA': {
499
- // The accumulated multicalls = what runs on Push Chain AFTER inbound arrives.
500
- // Wrap in UniversalPayload struct with correct UEA nonce for the relay.
501
- // Build push multicalls from this hop's own data (e.g., counter.increment())
502
- // This is the Route 3 hop's payload that executes on Push Chain after inbound.
503
- const hop0 = segment.hops[0];
504
- if ((_c = hop0 === null || hop0 === void 0 ? void 0 : hop0.params) === null || _c === void 0 ? void 0 : _c.data) {
505
- const hopPushMulticalls = (0, payload_builders_1.buildExecuteMulticall)({
506
- execute: {
507
- to: hop0.params.to,
508
- value: hop0.params.value,
509
- data: hop0.params.data,
510
- },
511
- ueaAddress,
562
+ case 'OUTBOUND_TO_CEA': {
563
+ const firstHop = segment.hops[0];
564
+ const isSvmSegment = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.isSvmTarget) === true;
565
+ let outboundPayload;
566
+ let targetForOutbound;
567
+ const isMigration = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.isMigration) === true;
568
+ if (isSvmSegment) {
569
+ // SVM: use the pre-built SVM payload from the hop descriptor
570
+ outboundPayload = (_b = firstHop.svmPayload) !== null && _b !== void 0 ? _b : '0x';
571
+ // For SVM, to.address IS the target (program for execute, recipient for withdraw)
572
+ const svmTarget = firstHop.params.to;
573
+ targetForOutbound = svmTarget.address;
574
+ }
575
+ else if (isMigration) {
576
+ // Migration: use raw 4-byte MIGRATION_SELECTOR, no multicall wrapping
577
+ outboundPayload = (0, payload_builders_1.buildMigrationPayload)();
578
+ targetForOutbound = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.ceaAddress) || ueaAddress;
579
+ }
580
+ else {
581
+ // EVM: build CEA payload from merged multicalls
582
+ outboundPayload = (0, payload_builders_1.buildCeaMulticallPayload)(segment.mergedCeaMulticalls || []);
583
+ // Get CEA address from the first hop
584
+ targetForOutbound = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.ceaAddress) || ueaAddress;
585
+ }
586
+ // Build outbound request
587
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetForOutbound, segment.prc20Token || selectors_1.ZERO_ADDRESS, segment.totalBurnAmount || BigInt(0), (_c = segment.gasLimit) !== null && _c !== void 0 ? _c : BigInt(0), outboundPayload, ueaAddress);
588
+ // Build approval + outbound multicalls
589
+ const segGasFee = segment.gasFee || BigInt(0);
590
+ const svmOverride = svmNativeValueBySegment.get(segment);
591
+ // When UEA balance < CASCADE_GAS_RESERVE, perOutboundNativeValue is
592
+ // undefined. Mirror the drained-UEA branch in executeUoaToCea
593
+ // (route-handlers.ts): send the entire available balance divided
594
+ // across outbound segments. The contract refunds excess via
595
+ // swapAndBurnGas, so overshooting is safe; undershooting causes STF.
596
+ let evmFallbackValue;
597
+ if (ueaBalance && ueaBalance > BigInt(0)) {
598
+ evmFallbackValue = ueaBalance / BigInt(Math.max(numOutbounds, 1));
599
+ }
600
+ else {
601
+ evmFallbackValue = segGasFee * BigInt(1000000);
602
+ }
603
+ let nativeValueForGas = (_d = svmOverride !== null && svmOverride !== void 0 ? svmOverride : perOutboundNativeValue) !== null && _d !== void 0 ? _d : evmFallbackValue;
604
+ // SDK 5.2 Case C: when the segment's sizing is category C, compose
605
+ // R2/OUTBOUND_TO_CEA segments no longer apply Case A/B/C sizing
606
+ // (scoped to R1 + R3 only). segment.sizing may still be populated
607
+ // from the hop descriptor merge, but it is not consulted here —
608
+ // nativeValueForGas is driven by pool-quote on the single-hop path
609
+ // and by the cascade-level per-outbound allocation above.
610
+ const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
611
+ prc20Token: segment.prc20Token || selectors_1.ZERO_ADDRESS,
612
+ gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
613
+ burnAmount: segment.totalBurnAmount || BigInt(0),
614
+ gasFee: segGasFee,
615
+ nativeValueForGas,
616
+ gatewayPcAddress,
617
+ outboundRequest: outboundReq,
512
618
  });
513
- // Prepend the Route 3's own push calls before subsequent hops
619
+ // Prepend to accumulated
514
620
  accumulatedPushMulticalls = [
515
- ...hopPushMulticalls,
621
+ ...outboundMulticalls,
516
622
  ...accumulatedPushMulticalls,
517
623
  ];
624
+ break;
518
625
  }
519
- let intermediatePayload = '0x';
520
- if (accumulatedPushMulticalls.length > 0) {
521
- const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, ueaAddress, accumulatedPushMulticalls);
522
- // +1: the outbound tx consumes one nonce via execute()
523
- intermediatePayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: (ueaNonce !== null && ueaNonce !== void 0 ? ueaNonce : BigInt(0)) + BigInt(1) });
524
- }
525
- const hop = segment.hops[0];
526
- const sourceChain = hop.sourceChain;
527
- const ceaAddress = hop.ceaAddress || ueaAddress;
528
- // SVM chains: build SVM CPI payload instead of EVM CEA multicall
529
- if ((0, payload_builders_1.isSvmChain)(sourceChain)) {
530
- const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
531
- if (!lockerContract) {
532
- throw new Error(`No SVM gateway program configured for chain ${sourceChain}`);
626
+ case 'INBOUND_FROM_CEA': {
627
+ // The accumulated multicalls = what runs on Push Chain AFTER inbound arrives.
628
+ // Wrap in UniversalPayload struct with correct UEA nonce for the relay.
629
+ // Build push multicalls from EVERY merged hop's own data in original
630
+ // order (e.g. counter.increment() + value transfer). Multi-hop R3
631
+ // segments (same sourceChain) bundle all target ops into the single
632
+ // UEA multicall so no nested outbound is needed.
633
+ const mergedHopMulticalls = [];
634
+ for (const h of segment.hops) {
635
+ if (((_e = h === null || h === void 0 ? void 0 : h.params) === null || _e === void 0 ? void 0 : _e.data) || ((_f = h === null || h === void 0 ? void 0 : h.params) === null || _f === void 0 ? void 0 : _f.value)) {
636
+ const hopCalls = (0, payload_builders_1.buildExecuteMulticall)({
637
+ execute: {
638
+ to: h.params.to,
639
+ value: h.params.value,
640
+ data: h.params.data,
641
+ },
642
+ ueaAddress,
643
+ });
644
+ mergedHopMulticalls.push(...hopCalls);
645
+ }
533
646
  }
534
- const programPk = new web3_js_1.PublicKey(lockerContract);
535
- const gatewayProgramHex = ('0x' +
536
- Buffer.from(programPk.toBytes()).toString('hex'));
537
- let drainAmount = BigInt(0);
538
- let tokenMintHex;
539
- const params = hop.params;
540
- if (((_d = params.funds) === null || _d === void 0 ? void 0 : _d.amount) && params.funds.amount > BigInt(0)) {
541
- drainAmount = params.funds.amount;
542
- const token = params.funds.token;
543
- if (token && token.address) {
544
- const mintPk = new web3_js_1.PublicKey(token.address);
545
- tokenMintHex = ('0x' +
546
- Buffer.from(mintPk.toBytes()).toString('hex'));
647
+ if (mergedHopMulticalls.length > 0) {
648
+ accumulatedPushMulticalls = [
649
+ ...mergedHopMulticalls,
650
+ ...accumulatedPushMulticalls,
651
+ ];
652
+ }
653
+ // Primary hop carries the sourceChain / CEA address (shared across merged hops).
654
+ const hop0 = segment.hops[0];
655
+ let intermediatePayload = '0x';
656
+ if (accumulatedPushMulticalls.length > 0) {
657
+ const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, ueaAddress, accumulatedPushMulticalls);
658
+ // +1: the outbound tx consumes one nonce via execute()
659
+ intermediatePayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: (ueaNonce !== null && ueaNonce !== void 0 ? ueaNonce : BigInt(0)) + BigInt(1) });
660
+ }
661
+ const hop = segment.hops[0];
662
+ const sourceChain = hop.sourceChain;
663
+ const ceaAddress = hop.ceaAddress || ueaAddress;
664
+ // SVM chains: build SVM CPI payload instead of EVM CEA multicall
665
+ if ((0, payload_builders_1.isSvmChain)(sourceChain)) {
666
+ const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
667
+ if (!lockerContract) {
668
+ throw new Error(`No SVM gateway program configured for chain ${sourceChain}`);
669
+ }
670
+ const programPk = new web3_js_1.PublicKey(lockerContract);
671
+ const gatewayProgramHex = ('0x' +
672
+ Buffer.from(programPk.toBytes()).toString('hex'));
673
+ let drainAmount = BigInt(0);
674
+ let tokenMintHex;
675
+ const params = hop.params;
676
+ if (((_g = params.funds) === null || _g === void 0 ? void 0 : _g.amount) && params.funds.amount > BigInt(0)) {
677
+ drainAmount = params.funds.amount;
678
+ const token = params.funds.token;
679
+ if (token && token.address) {
680
+ const mintPk = new web3_js_1.PublicKey(token.address);
681
+ tokenMintHex = ('0x' +
682
+ Buffer.from(mintPk.toBytes()).toString('hex'));
683
+ }
547
684
  }
685
+ else if (params.value && params.value > BigInt(0)) {
686
+ drainAmount = params.value;
687
+ }
688
+ // Derive CEA PDA as revert recipient
689
+ const ueaBytes = Buffer.from(ueaAddress.slice(2), 'hex');
690
+ const [ceaPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes], programPk);
691
+ const ceaPdaHex = ('0x' +
692
+ Buffer.from(ceaPda.toBytes()).toString('hex'));
693
+ // Build SVM payload with intermediate Push Chain payload embedded
694
+ const svmPayload = (0, payload_builders_1.encodeSvmCeaToUeaPayload)({
695
+ gatewayProgramHex,
696
+ drainAmount,
697
+ tokenMintHex,
698
+ revertRecipientHex: ceaPdaHex,
699
+ extraPayload: intermediatePayload !== '0x'
700
+ ? new Uint8Array(Buffer.from(intermediatePayload.slice(2), 'hex'))
701
+ : undefined,
702
+ });
703
+ const svmBurnAmount = segment.totalBurnAmount || BigInt(0);
704
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, segment.prc20Token ||
705
+ (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), svmBurnAmount, (_h = segment.gasLimit) !== null && _h !== void 0 ? _h : BigInt(0), svmPayload, ueaAddress);
706
+ const inboundGasFee = segment.gasFee || BigInt(0);
707
+ let svmInboundFallback;
708
+ if (ueaBalance && ueaBalance > BigInt(0)) {
709
+ svmInboundFallback = ueaBalance / BigInt(Math.max(numOutbounds, 1));
710
+ }
711
+ else {
712
+ svmInboundFallback = inboundGasFee * BigInt(1000000);
713
+ }
714
+ let svmInboundNativeValue = perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : svmInboundFallback;
715
+ // SDK 5.2 R3-style Case C bump: when the inbound segment's sizing
716
+ // is C, top up the swap budget by the overflow. No bridge-swap —
717
+ // R3 has no destination funds delivery.
718
+ if (((_j = segment.sizing) === null || _j === void 0 ? void 0 : _j.category) === 'C' &&
719
+ segment.sizing.overflowNativePc > BigInt(0)) {
720
+ const bumped = svmInboundNativeValue + segment.sizing.overflowNativePc;
721
+ (0, context_1.printLog)(ctx, `composeCascade — INBOUND_FROM_CEA (SVM) Case C: bumping nativeValueForGas from ${svmInboundNativeValue} to ${bumped} (overflow=${segment.sizing.overflowNativePc})`);
722
+ svmInboundNativeValue = bumped;
723
+ }
724
+ const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
725
+ prc20Token: segment.prc20Token ||
726
+ (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork),
727
+ gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
728
+ burnAmount: svmBurnAmount,
729
+ gasFee: inboundGasFee,
730
+ nativeValueForGas: svmInboundNativeValue,
731
+ gatewayPcAddress,
732
+ outboundRequest: outboundReq,
733
+ });
734
+ accumulatedPushMulticalls = [...outboundMulticalls];
735
+ break;
548
736
  }
549
- else if (params.value && params.value > BigInt(0)) {
550
- drainAmount = params.value;
737
+ // EVM path: Build CEA multicall: [approve?, sendUniversalTxFromCEA(payload)]
738
+ const ceaMulticalls = [];
739
+ // Add primary hop's own CEA operations if any
740
+ // (e.g., approve + swap before bridging back)
741
+ if (hop.ceaMulticalls && hop.ceaMulticalls.length > 0) {
742
+ ceaMulticalls.push(...hop.ceaMulticalls);
551
743
  }
552
- // Derive CEA PDA as revert recipient
553
- const ueaBytes = Buffer.from(ueaAddress.slice(2), 'hex');
554
- const [ceaPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes], programPk);
555
- const ceaPdaHex = ('0x' +
556
- Buffer.from(ceaPda.toBytes()).toString('hex'));
557
- // Build SVM payload with intermediate Push Chain payload embedded
558
- const svmPayload = (0, payload_builders_1.encodeSvmCeaToUeaPayload)({
559
- gatewayProgramHex,
560
- drainAmount,
561
- tokenMintHex,
562
- revertRecipientHex: ceaPdaHex,
563
- extraPayload: intermediatePayload !== '0x'
564
- ? new Uint8Array(Buffer.from(intermediatePayload.slice(2), 'hex'))
565
- : undefined,
566
- });
567
- const svmBurnAmount = segment.totalBurnAmount || BigInt(0);
568
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, segment.prc20Token ||
569
- (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), svmBurnAmount, (_e = segment.gasLimit) !== null && _e !== void 0 ? _e : BigInt(0), svmPayload, ueaAddress);
744
+ // Aggregate token/amount across all merged hops so the single
745
+ // sendUniversalTxToUEA call carries the combined forward amount.
746
+ // For multi-hop R3 segments (same sourceChain), ERC20 funds across
747
+ // hops must resolve to the same token — otherwise this path can't
748
+ // express it in one call and the caller should split manually.
749
+ let tokenAddress = selectors_1.ZERO_ADDRESS;
750
+ let amount = BigInt(0);
751
+ for (const mergedHop of segment.hops) {
752
+ const hParams = mergedHop.params;
753
+ if ((_k = hParams.funds) === null || _k === void 0 ? void 0 : _k.amount) {
754
+ const token = hParams.funds.token;
755
+ if (token) {
756
+ if (token.mechanism === 'native') {
757
+ amount += hParams.funds.amount;
758
+ }
759
+ else {
760
+ const addr = token.address;
761
+ if (tokenAddress !== selectors_1.ZERO_ADDRESS &&
762
+ tokenAddress.toLowerCase() !== addr.toLowerCase()) {
763
+ throw new Error(`composeCascade: merged R3 hops target different ERC20 tokens (${tokenAddress} vs ${addr}); split into separate executeTransactions batches`);
764
+ }
765
+ tokenAddress = addr;
766
+ amount += hParams.funds.amount;
767
+ // CEA approves gateway once with the cumulative allowance.
768
+ const gatewayAddr = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
769
+ if (!gatewayAddr) {
770
+ throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
771
+ }
772
+ // Reset allowance to 0 first for USDT-style tokens
773
+ const approveZeroData = (0, viem_1.encodeFunctionData)({
774
+ abi: erc20_evm_1.ERC20_EVM,
775
+ functionName: 'approve',
776
+ args: [gatewayAddr, BigInt(0)],
777
+ });
778
+ ceaMulticalls.push({
779
+ to: tokenAddress,
780
+ value: BigInt(0),
781
+ data: approveZeroData,
782
+ });
783
+ const approveData = (0, viem_1.encodeFunctionData)({
784
+ abi: erc20_evm_1.ERC20_EVM,
785
+ functionName: 'approve',
786
+ args: [gatewayAddr, hParams.funds.amount],
787
+ });
788
+ ceaMulticalls.push({
789
+ to: tokenAddress,
790
+ value: BigInt(0),
791
+ data: approveData,
792
+ });
793
+ }
794
+ }
795
+ }
796
+ else if (hParams.value && hParams.value > BigInt(0)) {
797
+ amount += hParams.value;
798
+ }
799
+ }
800
+ // Build sendUniversalTxToUEA self-call on CEA (to=CEA, value=0)
801
+ const sendCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, tokenAddress, amount, intermediatePayload, ceaAddress);
802
+ ceaMulticalls.push(sendCall);
803
+ // Wrap CEA multicall in outbound from Push Chain
804
+ const ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
805
+ // The nested outbound back to the source chain needs a non-zero
806
+ // gasLimit — UGPC.sendUniversalTxOutbound reverts with
807
+ // "Missing or invalid parameters" on 0. When the hop didn't supply
808
+ // one (typical for a pure-R3 leg composed inside a cascade), fall
809
+ // back to DEFAULT_OUTBOUND_GAS_LIMIT just like the initial outbound
810
+ // path does.
811
+ const effectiveGasLimit = segment.gasLimit && segment.gasLimit > BigInt(0)
812
+ ? segment.gasLimit
813
+ : selectors_1.DEFAULT_OUTBOUND_GAS_LIMIT;
814
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, segment.prc20Token ||
815
+ (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), segment.totalBurnAmount || BigInt(0), effectiveGasLimit, ceaPayload, ueaAddress);
570
816
  const inboundGasFee = segment.gasFee || BigInt(0);
817
+ let evmInboundFallback;
818
+ if (ueaBalance && ueaBalance > BigInt(0)) {
819
+ evmInboundFallback = ueaBalance / BigInt(Math.max(numOutbounds, 1));
820
+ }
821
+ else {
822
+ evmInboundFallback = inboundGasFee * BigInt(1000000);
823
+ }
824
+ let evmInboundNativeValue = perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : evmInboundFallback;
825
+ // SDK 5.2 R3-style Case C bump: when the inbound segment's sizing
826
+ // is C, top up the swap budget by the overflow. No bridge-swap —
827
+ // R3 has no destination funds delivery.
828
+ if (((_l = segment.sizing) === null || _l === void 0 ? void 0 : _l.category) === 'C' &&
829
+ segment.sizing.overflowNativePc > BigInt(0)) {
830
+ const bumped = evmInboundNativeValue + segment.sizing.overflowNativePc;
831
+ (0, context_1.printLog)(ctx, `composeCascade — INBOUND_FROM_CEA (EVM) Case C: bumping nativeValueForGas from ${evmInboundNativeValue} to ${bumped} (overflow=${segment.sizing.overflowNativePc})`);
832
+ evmInboundNativeValue = bumped;
833
+ }
571
834
  const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
572
835
  prc20Token: segment.prc20Token ||
573
836
  (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork),
574
837
  gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
575
- burnAmount: svmBurnAmount,
838
+ burnAmount: segment.totalBurnAmount || BigInt(0),
576
839
  gasFee: inboundGasFee,
577
- nativeValueForGas: perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : inboundGasFee * BigInt(1000),
840
+ nativeValueForGas: evmInboundNativeValue,
578
841
  gatewayPcAddress,
579
842
  outboundRequest: outboundReq,
580
843
  });
844
+ // Reset accumulated -- everything is now inside this outbound
581
845
  accumulatedPushMulticalls = [...outboundMulticalls];
582
846
  break;
583
847
  }
584
- // EVM path: Build CEA multicall: [approve?, sendUniversalTxFromCEA(payload)]
585
- const ceaMulticalls = [];
586
- // Add hop's own CEA operations if any
587
- // (e.g., approve + swap before bridging back)
588
- if (hop.ceaMulticalls && hop.ceaMulticalls.length > 0) {
589
- ceaMulticalls.push(...hop.ceaMulticalls);
590
- }
591
- // Determine token/amount for inbound
592
- let tokenAddress = selectors_1.ZERO_ADDRESS;
593
- let amount = BigInt(0);
594
- const params = hop.params;
595
- if ((_f = params.funds) === null || _f === void 0 ? void 0 : _f.amount) {
596
- const token = params.funds.token;
597
- if (token) {
598
- if (token.mechanism === 'native') {
599
- amount = params.funds.amount;
600
- }
601
- else {
602
- tokenAddress = token.address;
603
- amount = params.funds.amount;
604
- // Add approve for ERC20 (CEA approves gateway)
605
- const gatewayAddr = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
606
- if (!gatewayAddr) {
607
- throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
608
- }
609
- // Reset allowance to 0 first for USDT-style tokens
610
- const approveZeroData = (0, viem_1.encodeFunctionData)({
611
- abi: erc20_evm_1.ERC20_EVM,
612
- functionName: 'approve',
613
- args: [gatewayAddr, BigInt(0)],
614
- });
615
- ceaMulticalls.push({
616
- to: tokenAddress,
617
- value: BigInt(0),
618
- data: approveZeroData,
619
- });
620
- const approveData = (0, viem_1.encodeFunctionData)({
621
- abi: erc20_evm_1.ERC20_EVM,
622
- functionName: 'approve',
623
- args: [gatewayAddr, amount],
624
- });
625
- ceaMulticalls.push({
626
- to: tokenAddress,
627
- value: BigInt(0),
628
- data: approveData,
629
- });
630
- }
631
- }
632
- }
633
- else if (params.value && params.value > BigInt(0)) {
634
- amount = params.value;
635
- }
636
- // Build sendUniversalTxToUEA self-call on CEA (to=CEA, value=0)
637
- const sendCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, tokenAddress, amount, intermediatePayload, ceaAddress);
638
- ceaMulticalls.push(sendCall);
639
- // Wrap CEA multicall in outbound from Push Chain
640
- const ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
641
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, segment.prc20Token ||
642
- (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), segment.totalBurnAmount || BigInt(0), (_g = segment.gasLimit) !== null && _g !== void 0 ? _g : BigInt(0), ceaPayload, ueaAddress);
643
- const inboundGasFee = segment.gasFee || BigInt(0);
644
- const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
645
- prc20Token: segment.prc20Token ||
646
- (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork),
647
- gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
648
- burnAmount: segment.totalBurnAmount || BigInt(0),
649
- gasFee: inboundGasFee,
650
- nativeValueForGas: perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : inboundGasFee * BigInt(1000),
651
- gatewayPcAddress,
652
- outboundRequest: outboundReq,
653
- });
654
- // Reset accumulated -- everything is now inside this outbound
655
- accumulatedPushMulticalls = [...outboundMulticalls];
656
- break;
657
848
  }
658
849
  }
659
- }
660
- return accumulatedPushMulticalls;
850
+ return accumulatedPushMulticalls;
851
+ });
661
852
  }
662
853
  // ============================================================================
663
854
  // createCascadedBuilder
@@ -750,7 +941,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
750
941
  const ueaNonceCascade = ueaCodeCascade !== undefined
751
942
  ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress)
752
943
  : BigInt(0);
753
- const composedMulticalls = composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonceCascade);
944
+ const composedMulticalls = yield composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonceCascade);
754
945
  // Execute the composed multicall as a single Push Chain tx
755
946
  const executeParams = {
756
947
  to: ueaAddress,
@@ -775,14 +966,77 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
775
966
  hops: hopInfos,
776
967
  hopCount: hops.length,
777
968
  waitForAll: (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
778
- var _a;
779
- const { pollingIntervalMs = 10000, timeout = 300000, progressHook: cascadeProgressHook, } = opts || {};
969
+ var _a, _b, _c, _d, _e;
970
+ const { pollingIntervalMs = 10000, timeout = 300000, progressHook: cascadeProgressHook, eventHook, } = opts || {};
780
971
  const startTime = Date.now();
972
+ // Per-hop progress emission. Drops events whose id is '' (the no-op
973
+ // sentinel returned by pickWaitHooks for R4/unknown routes), so
974
+ // consumers don't see placeholder events while the cascade still
975
+ // drives its own state machine.
976
+ const emitHopEvent = (hook, event) => {
977
+ if (!hook || !event.id)
978
+ return;
979
+ hook(event);
980
+ };
981
+ // ---- Multichain (multi-hop) cascade markers (001 / 002 / 999) ----
982
+ // Only fire when this is genuinely a multi-hop cascade. Single-hop
983
+ // R1/R2 takes the early-return paths above and never reaches here,
984
+ // but guard explicitly in case someone wires a 1-element prepared list.
985
+ const totalHops = hopInfos.length;
986
+ const isMulti = totalHops > 1;
987
+ const chainLabels = hops.map((h) => {
988
+ var _a, _b;
989
+ return String((_b = (_a = h.targetChain) !== null && _a !== void 0 ? _a : h.sourceChain) !== null && _b !== void 0 ? _b : (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork));
990
+ });
991
+ const signerOriginChain = String((_a = ctx.universalSigner.account.chain) !== null && _a !== void 0 ? _a : 'origin');
992
+ const emitHopStart = (hopIndex0) => {
993
+ if (!isMulti)
994
+ return;
995
+ const n = hopIndex0 + 1;
996
+ const fromChain = hopIndex0 === 0
997
+ ? signerOriginChain
998
+ : chainLabels[hopIndex0 - 1];
999
+ const toChain = chainLabels[hopIndex0];
1000
+ emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_002_01](n, totalHops, fromChain, toChain));
1001
+ };
1002
+ const emitHopComplete = (hopIndex0) => {
1003
+ if (!isMulti)
1004
+ return;
1005
+ const n = hopIndex0 + 1;
1006
+ if (n >= totalHops)
1007
+ return; // skip after final hop — 999-01 takes over
1008
+ emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_002_99_99](n, totalHops));
1009
+ };
1010
+ const emitCascadeSuccess = () => {
1011
+ if (!isMulti)
1012
+ return;
1013
+ emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_01](totalHops));
1014
+ };
1015
+ const emitCascadeFailed = (failedIdx0, errMsg) => {
1016
+ if (!isMulti)
1017
+ return;
1018
+ emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_02](failedIdx0 + 1, totalHops, errMsg));
1019
+ };
1020
+ const emitCascadeTimeout = (failedIdx0) => {
1021
+ if (!isMulti)
1022
+ return;
1023
+ emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_03](failedIdx0 + 1, totalHops));
1024
+ };
1025
+ if (isMulti) {
1026
+ emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_001](totalHops, chainLabels));
1027
+ }
781
1028
  try {
782
1029
  // 1. Wait for initial Push Chain tx confirmation
1030
+ // Emit 002-01 for hop 0 ONLY when the first hop is a Push (R1) tx —
1031
+ // its "start" coincides with submitting the composed Push tx. For
1032
+ // cascades that begin with R2/R3, the per-outbound/inbound branches
1033
+ // below own the emitHopStart for hop 0 to avoid a duplicate marker.
1034
+ if (((_b = hopInfos[0]) === null || _b === void 0 ? void 0 : _b.route) === 'UOA_TO_PUSH') {
1035
+ emitHopStart(0);
1036
+ }
783
1037
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
784
1038
  hopIndex: 0,
785
- route: ((_a = hopInfos[0]) === null || _a === void 0 ? void 0 : _a.route) || 'UOA_TO_PUSH',
1039
+ route: ((_c = hopInfos[0]) === null || _c === void 0 ? void 0 : _c.route) || 'UOA_TO_PUSH',
786
1040
  chain: (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork),
787
1041
  status: 'waiting',
788
1042
  elapsed: Date.now() - startTime,
@@ -801,6 +1055,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
801
1055
  txHash: response.hash,
802
1056
  elapsed: Date.now() - startTime,
803
1057
  });
1058
+ emitHopComplete(hop.hopIndex);
804
1059
  }
805
1060
  }
806
1061
  // 2. Track outbound hops (Route 2: UOA_TO_CEA)
@@ -831,6 +1086,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
831
1086
  if (outboundHops.length === 1) {
832
1087
  // Single direct outbound hop: use the existing V1-based tracking
833
1088
  const hop = outboundHops[0];
1089
+ const hopHooks = (0, progress_route_hooks_1.pickWaitHooks)(hop.route);
834
1090
  const remainingTimeout = timeout - (Date.now() - startTime);
835
1091
  if (remainingTimeout <= 0) {
836
1092
  hop.status = 'failed';
@@ -841,12 +1097,16 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
841
1097
  status: 'timeout',
842
1098
  elapsed: Date.now() - startTime,
843
1099
  });
1100
+ emitHopEvent(eventHook, hopHooks.timeout(hop.executionChain, Date.now() - startTime));
1101
+ emitCascadeTimeout(hop.hopIndex);
844
1102
  return {
845
1103
  success: false,
846
1104
  hops: hopInfos,
847
1105
  failedAt: hop.hopIndex,
848
1106
  };
849
1107
  }
1108
+ emitHopStart(hop.hopIndex);
1109
+ emitHopEvent(eventHook, hopHooks.awaiting(hop.executionChain));
850
1110
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
851
1111
  hopIndex: hop.hopIndex,
852
1112
  route: hop.route,
@@ -854,6 +1114,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
854
1114
  status: 'polling',
855
1115
  elapsed: Date.now() - startTime,
856
1116
  });
1117
+ let lastEmittedStatus;
857
1118
  try {
858
1119
  const outboundDetails = yield callbacks.waitForOutboundTxFn(response.hash, {
859
1120
  initialWaitMs: Math.min(60000, remainingTimeout),
@@ -867,11 +1128,18 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
867
1128
  status: event.status,
868
1129
  elapsed: Date.now() - startTime,
869
1130
  });
1131
+ if (event.status === 'polling' &&
1132
+ lastEmittedStatus !== 'polling') {
1133
+ lastEmittedStatus = 'polling';
1134
+ emitHopEvent(eventHook, hopHooks.polling(hop.executionChain, event.elapsed));
1135
+ }
870
1136
  },
871
1137
  });
872
1138
  hop.status = 'confirmed';
873
1139
  hop.txHash = outboundDetails.externalTxHash;
874
1140
  hop.outboundDetails = outboundDetails;
1141
+ emitHopEvent(eventHook, hopHooks.success(outboundDetails));
1142
+ emitHopComplete(hop.hopIndex);
875
1143
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
876
1144
  hopIndex: hop.hopIndex,
877
1145
  route: hop.route,
@@ -882,6 +1150,17 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
882
1150
  });
883
1151
  }
884
1152
  catch (err) {
1153
+ const errMsg = err instanceof Error ? err.message : String(err);
1154
+ const isTimeout = errMsg.startsWith('Timeout waiting for outbound transaction');
1155
+ emitHopEvent(eventHook, isTimeout
1156
+ ? hopHooks.timeout(hop.executionChain, remainingTimeout)
1157
+ : hopHooks.failed(hop.executionChain, errMsg));
1158
+ if (isTimeout) {
1159
+ emitCascadeTimeout(hop.hopIndex);
1160
+ }
1161
+ else {
1162
+ emitCascadeFailed(hop.hopIndex, errMsg);
1163
+ }
885
1164
  hop.status = 'failed';
886
1165
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
887
1166
  hopIndex: hop.hopIndex,
@@ -899,11 +1178,20 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
899
1178
  }
900
1179
  else {
901
1180
  // Multiple outbound hops: use V2 API which returns outboundTx[]
1181
+ // Emit "Awaiting External Chain" once per outbound hop before
1182
+ // polling begins, using the route-specific awaiting hook.
1183
+ for (const outHop of outboundHops) {
1184
+ emitHopStart(outHop.hopIndex);
1185
+ emitHopEvent(eventHook, (0, progress_route_hooks_1.pickWaitHooks)(outHop.route).awaiting(outHop.executionChain));
1186
+ }
1187
+ const perHopLastStatus = new Map();
1188
+ const confirmedHops = new Set();
902
1189
  const allOutboundDetails = yield callbacks.waitForAllOutboundTxsFn(response.hash, outboundHops, {
903
1190
  initialWaitMs: Math.min(60000, timeout - (Date.now() - startTime)),
904
1191
  pollingIntervalMs,
905
1192
  timeout: timeout - (Date.now() - startTime),
906
1193
  progressHook: (event) => {
1194
+ var _a, _b;
907
1195
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
908
1196
  hopIndex: event.hopIndex,
909
1197
  route: event.route,
@@ -912,21 +1200,54 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
912
1200
  txHash: event.txHash,
913
1201
  elapsed: Date.now() - startTime,
914
1202
  });
1203
+ const matchedHop = outboundHops.find((h) => h.hopIndex === event.hopIndex);
1204
+ const eventHooks = (0, progress_route_hooks_1.pickWaitHooks)(matchedHop === null || matchedHop === void 0 ? void 0 : matchedHop.route);
1205
+ const prev = perHopLastStatus.get(event.hopIndex);
1206
+ if (event.status === 'polling' &&
1207
+ prev !== 'polling') {
1208
+ perHopLastStatus.set(event.hopIndex, 'polling');
1209
+ emitHopEvent(eventHook, 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));
1210
+ }
1211
+ if (event.status === 'confirmed' &&
1212
+ event.txHash &&
1213
+ !confirmedHops.has(event.hopIndex)) {
1214
+ // waitForAllOutboundTxsV2 populates hop.outboundDetails
1215
+ // on the matched hop before emitting 'confirmed' (see
1216
+ // outbound-sync.ts:348-355). Auto-promotion paths
1217
+ // without a txHash are skipped by the `event.txHash`
1218
+ // guard above, so we only reach here with full details.
1219
+ if (matchedHop === null || matchedHop === void 0 ? void 0 : matchedHop.outboundDetails) {
1220
+ confirmedHops.add(event.hopIndex);
1221
+ emitHopEvent(eventHook, eventHooks.success(matchedHop.outboundDetails));
1222
+ emitHopComplete(event.hopIndex);
1223
+ }
1224
+ }
915
1225
  },
916
1226
  });
917
1227
  if (!allOutboundDetails.success) {
1228
+ const failedIdx = (_d = allOutboundDetails.failedAt) !== null && _d !== void 0 ? _d : 0;
1229
+ const failedHop = outboundHops.find((h) => h.hopIndex === failedIdx);
1230
+ const failMsg = failedHop
1231
+ ? `Outbound failed for hop ${failedIdx} on ${failedHop.executionChain}`
1232
+ : `Outbound cascade failed at hop ${failedIdx}`;
1233
+ emitHopEvent(eventHook, (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));
1234
+ emitCascadeFailed(failedIdx, failMsg);
918
1235
  return {
919
1236
  success: false,
920
1237
  hops: hopInfos,
921
- failedAt: allOutboundDetails.failedAt,
1238
+ failedAt: failedIdx,
922
1239
  };
923
1240
  }
924
1241
  }
925
1242
  }
926
1243
  // 3. Route 3 (CEA_TO_PUSH) tracking - mark as submitted
1244
+ // Cascade does not poll inbound completion; we surface the hop-start
1245
+ // marker so consumers see a "Starting tx N" event for the inbound,
1246
+ // but skip emitHopComplete since we never observe terminal state.
927
1247
  const inboundHops = hopInfos.filter((h) => h.route === 'CEA_TO_PUSH');
928
1248
  for (const inboundHop of inboundHops) {
929
1249
  inboundHop.status = 'submitted';
1250
+ emitHopStart(inboundHop.hopIndex);
930
1251
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
931
1252
  hopIndex: inboundHop.hopIndex,
932
1253
  route: inboundHop.route,
@@ -935,14 +1256,24 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
935
1256
  elapsed: Date.now() - startTime,
936
1257
  });
937
1258
  }
1259
+ emitCascadeSuccess();
938
1260
  return { success: true, hops: hopInfos };
939
1261
  }
940
1262
  catch (err) {
941
1263
  const failedIdx = hopInfos.findIndex((h) => h.status !== 'confirmed');
1264
+ const errMsg = err instanceof Error ? err.message : String(err);
1265
+ const failedAt = failedIdx >= 0 ? failedIdx : 0;
1266
+ const isTimeout = errMsg.startsWith('Timeout');
1267
+ if (isTimeout) {
1268
+ emitCascadeTimeout(failedAt);
1269
+ }
1270
+ else {
1271
+ emitCascadeFailed(failedAt, errMsg);
1272
+ }
942
1273
  return {
943
1274
  success: false,
944
1275
  hops: hopInfos,
945
- failedAt: failedIdx >= 0 ? failedIdx : 0,
1276
+ failedAt,
946
1277
  };
947
1278
  }
948
1279
  }),