@pushchain/core 5.1.5-alpha.1 → 5.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +1 -1
  3. package/src/lib/constants/abi/index.d.ts +2 -0
  4. package/src/lib/constants/abi/index.js +6 -1
  5. package/src/lib/constants/abi/uniswap-v3-router.evm.d.ts +58 -0
  6. package/src/lib/constants/abi/uniswap-v3-router.evm.js +41 -0
  7. package/src/lib/constants/abi/wpc.evm.d.ts +24 -0
  8. package/src/lib/constants/abi/wpc.evm.js +27 -0
  9. package/src/lib/constants/chain.d.ts +1 -0
  10. package/src/lib/constants/chain.js +8 -6
  11. package/src/lib/constants/tokens.js +3 -3
  12. package/src/lib/generated/uexecutor/v2/types.d.ts +14 -0
  13. package/src/lib/generated/uexecutor/v2/types.js +96 -3
  14. package/src/lib/orchestrator/internals/cascade.d.ts +1 -1
  15. package/src/lib/orchestrator/internals/cascade.js +521 -239
  16. package/src/lib/orchestrator/internals/confirmation.js +60 -15
  17. package/src/lib/orchestrator/internals/context.d.ts +23 -0
  18. package/src/lib/orchestrator/internals/context.js +58 -0
  19. package/src/lib/orchestrator/internals/execute-funds-only.js +47 -21
  20. package/src/lib/orchestrator/internals/execute-funds-payload.js +70 -37
  21. package/src/lib/orchestrator/internals/execute-standard.js +137 -26
  22. package/src/lib/orchestrator/internals/gas-calculator.d.ts +5 -2
  23. package/src/lib/orchestrator/internals/gas-calculator.js +59 -11
  24. package/src/lib/orchestrator/internals/gas-usd-sizer.d.ts +53 -0
  25. package/src/lib/orchestrator/internals/gas-usd-sizer.js +165 -0
  26. package/src/lib/orchestrator/internals/helpers.js +2 -0
  27. package/src/lib/orchestrator/internals/inbound-tracker.d.ts +83 -0
  28. package/src/lib/orchestrator/internals/inbound-tracker.js +322 -0
  29. package/src/lib/orchestrator/internals/index.d.ts +3 -1
  30. package/src/lib/orchestrator/internals/index.js +8 -2
  31. package/src/lib/orchestrator/internals/outbound-sync.d.ts +32 -1
  32. package/src/lib/orchestrator/internals/outbound-sync.js +154 -17
  33. package/src/lib/orchestrator/internals/pc-usd-oracle.d.ts +28 -0
  34. package/src/lib/orchestrator/internals/pc-usd-oracle.js +285 -0
  35. package/src/lib/orchestrator/internals/progress-route-hooks.d.ts +23 -0
  36. package/src/lib/orchestrator/internals/progress-route-hooks.js +72 -0
  37. package/src/lib/orchestrator/internals/push-chain-tx.d.ts +27 -1
  38. package/src/lib/orchestrator/internals/push-chain-tx.js +111 -15
  39. package/src/lib/orchestrator/internals/rescue.js +1 -1
  40. package/src/lib/orchestrator/internals/response-builder.d.ts +5 -0
  41. package/src/lib/orchestrator/internals/response-builder.js +242 -66
  42. package/src/lib/orchestrator/internals/route-handlers.js +480 -206
  43. package/src/lib/orchestrator/internals/tx-transformer.js +293 -35
  44. package/src/lib/orchestrator/orchestrator.d.ts +7 -1
  45. package/src/lib/orchestrator/orchestrator.js +108 -56
  46. package/src/lib/orchestrator/orchestrator.types.d.ts +111 -5
  47. package/src/lib/orchestrator/payload-builders.d.ts +16 -0
  48. package/src/lib/orchestrator/payload-builders.js +6 -5
  49. package/src/lib/orchestrator/route-detector.js +20 -8
  50. package/src/lib/orchestrator/svm-idl/normalize-address.d.ts +19 -0
  51. package/src/lib/orchestrator/svm-idl/normalize-address.js +56 -0
  52. package/src/lib/orchestrator/svm-idl/registry.d.ts +13 -4
  53. package/src/lib/orchestrator/svm-idl/registry.js +15 -14
  54. package/src/lib/orchestrator/svm-idl/resolve.js +5 -2
  55. package/src/lib/progress-hook/progress-hook.d.ts +28 -0
  56. package/src/lib/progress-hook/progress-hook.js +552 -133
  57. package/src/lib/progress-hook/progress-hook.types.d.ts +77 -29
  58. package/src/lib/progress-hook/progress-hook.types.js +75 -35
  59. package/src/lib/push-client/push-client.d.ts +7 -0
  60. package/src/lib/push-client/push-client.js +18 -1
  61. package/src/lib/universal-tx-detector/__tests__/fixtures/builders.d.ts +32 -0
  62. package/src/lib/universal-tx-detector/__tests__/fixtures/builders.js +65 -0
  63. package/src/lib/universal-tx-detector/__tests__/fixtures/scenarios.d.ts +72 -0
  64. package/src/lib/universal-tx-detector/__tests__/fixtures/scenarios.js +669 -0
  65. package/src/lib/universal-tx-detector/auto-detect.d.ts +37 -0
  66. package/src/lib/universal-tx-detector/auto-detect.js +112 -0
  67. package/src/lib/universal-tx-detector/cascade.d.ts +64 -0
  68. package/src/lib/universal-tx-detector/cascade.js +254 -0
  69. package/src/lib/universal-tx-detector/child-inbounds.d.ts +58 -0
  70. package/src/lib/universal-tx-detector/child-inbounds.js +145 -0
  71. package/src/lib/universal-tx-detector/classify.d.ts +33 -0
  72. package/src/lib/universal-tx-detector/classify.js +228 -0
  73. package/src/lib/universal-tx-detector/detector-svm.d.ts +46 -0
  74. package/src/lib/universal-tx-detector/detector-svm.js +324 -0
  75. package/src/lib/universal-tx-detector/detector.d.ts +13 -0
  76. package/src/lib/universal-tx-detector/detector.js +233 -0
  77. package/src/lib/universal-tx-detector/events.d.ts +765 -0
  78. package/src/lib/universal-tx-detector/events.js +241 -0
  79. package/src/lib/universal-tx-detector/index.d.ts +18 -0
  80. package/src/lib/universal-tx-detector/index.js +36 -0
  81. package/src/lib/universal-tx-detector/svm-events.d.ts +41 -0
  82. package/src/lib/universal-tx-detector/svm-events.js +68 -0
  83. package/src/lib/universal-tx-detector/types.d.ts +100 -0
  84. package/src/lib/universal-tx-detector/types.js +3 -0
  85. package/src/lib/utils.d.ts +30 -13
  86. package/src/lib/utils.js +47 -21
@@ -22,14 +22,17 @@ 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");
30
- const build_payload_1 = require("../svm-idl/build-payload");
31
- const progress_hook_types_1 = require("../../progress-hook/progress-hook.types");
32
+ const progress_route_hooks_1 = require("./progress-route-hooks");
32
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");
35
+ const build_payload_1 = require("../svm-idl/build-payload");
33
36
  // ============================================================================
34
37
  // prepareTransaction
35
38
  // ============================================================================
@@ -133,21 +136,29 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
133
136
  burnAmount = params.value;
134
137
  }
135
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.
136
144
  prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, ctx.pushNetwork);
137
- burnAmount = BigInt(1);
145
+ burnAmount = BigInt(0);
138
146
  }
139
147
  let gasToken = selectors_1.ZERO_ADDRESS;
140
148
  let gasFee = BigInt(0);
149
+ let sizing;
141
150
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
142
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
151
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, targetChain);
143
152
  gasToken = result.gasToken;
144
153
  gasFee = result.gasFee;
154
+ sizing = result.sizing;
145
155
  }
146
156
  return Object.assign(Object.assign({}, baseDescriptor), { targetChain, isSvmTarget: true, svmPayload,
147
157
  prc20Token,
148
158
  burnAmount,
149
159
  gasToken,
150
- gasFee });
160
+ gasFee,
161
+ sizing });
151
162
  }
152
163
  // EVM path: Resolve CEA address + build CEA multicalls
153
164
  const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_c = ctx.rpcUrls[targetChain]) === null || _c === void 0 ? void 0 : _c[0]);
@@ -157,16 +168,18 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
157
168
  const burnAmount = BigInt(0); // Migration is logic-only — no funds. CEA rejects msg.value != 0.
158
169
  let gasToken = selectors_1.ZERO_ADDRESS;
159
170
  let gasFee = BigInt(0);
171
+ let sizing;
160
172
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
161
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
173
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, targetChain);
162
174
  gasToken = result.gasToken;
163
175
  gasFee = result.gasFee;
176
+ sizing = result.sizing;
164
177
  }
165
178
  return Object.assign(Object.assign({}, baseDescriptor), { targetChain,
166
179
  ceaAddress, ceaMulticalls: [], prc20Token,
167
180
  burnAmount,
168
181
  gasToken,
169
- gasFee, isMigration: true });
182
+ gasFee, isMigration: true, sizing });
170
183
  }
171
184
  // Build CEA multicalls
172
185
  const ceaMulticalls = [];
@@ -240,10 +253,12 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
240
253
  // Query gas fee
241
254
  let gasToken = selectors_1.ZERO_ADDRESS;
242
255
  let gasFee = BigInt(0);
256
+ let sizing;
243
257
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
244
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
258
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, targetChain);
245
259
  gasToken = result.gasToken;
246
260
  gasFee = result.gasFee;
261
+ sizing = result.sizing;
247
262
  }
248
263
  return Object.assign(Object.assign({}, baseDescriptor), { targetChain,
249
264
  ceaAddress,
@@ -251,7 +266,8 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
251
266
  prc20Token,
252
267
  burnAmount,
253
268
  gasToken,
254
- gasFee });
269
+ gasFee,
270
+ sizing });
255
271
  }
256
272
  case route_detector_1.TransactionRoute.CEA_TO_PUSH: {
257
273
  // Route 3: Build CEA multicalls for sendUniversalTxFromCEA
@@ -292,13 +308,16 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
292
308
  }
293
309
  let gasToken = selectors_1.ZERO_ADDRESS;
294
310
  let gasFee = BigInt(0);
311
+ let sizing;
295
312
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
296
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
313
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, sourceChain);
297
314
  gasToken = result.gasToken;
298
315
  gasFee = result.gasFee;
316
+ sizing = result.sizing;
299
317
  }
300
318
  return Object.assign(Object.assign({}, baseDescriptor), { sourceChain, ceaAddress: ceaPdaHex, isSvmTarget: true, prc20Token, burnAmount: amount, gasToken,
301
- gasFee });
319
+ gasFee,
320
+ sizing });
302
321
  }
303
322
  const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_h = ctx.rpcUrls[sourceChain]) === null || _h === void 0 ? void 0 : _h[0]);
304
323
  // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
@@ -328,15 +347,18 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
328
347
  const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
329
348
  let gasToken = selectors_1.ZERO_ADDRESS;
330
349
  let gasFee = BigInt(0);
350
+ let sizing;
331
351
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
332
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit);
352
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, sourceChain);
333
353
  gasToken = result.gasToken;
334
354
  gasFee = result.gasFee;
355
+ sizing = result.sizing;
335
356
  }
336
357
  return Object.assign(Object.assign({}, baseDescriptor), { sourceChain,
337
358
  ceaAddress,
338
359
  prc20Token, burnAmount: amount, gasToken,
339
- gasFee });
360
+ gasFee,
361
+ sizing });
340
362
  }
341
363
  default:
342
364
  return baseDescriptor;
@@ -356,10 +378,20 @@ function classifyIntoSegments(hops) {
356
378
  const chain = hop.targetChain || hop.sourceChain;
357
379
  const canMerge = currentSegment &&
358
380
  currentSegment.type === segType &&
359
- // 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).
360
389
  (segType === 'OUTBOUND_TO_CEA'
361
390
  ? currentSegment.targetChain === hop.targetChain && !hop.isSvmTarget
362
- : 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');
363
395
  if (canMerge && currentSegment) {
364
396
  // Merge into current segment
365
397
  currentSegment.hops.push(hop);
@@ -379,12 +411,36 @@ function classifyIntoSegments(hops) {
379
411
  currentSegment.gasFee =
380
412
  (currentSegment.gasFee || BigInt(0)) + (hop.gasFee || BigInt(0));
381
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
+ }
382
426
  else if (segType === 'PUSH_EXECUTION') {
383
427
  currentSegment.mergedPushMulticalls = [
384
428
  ...(currentSegment.mergedPushMulticalls || []),
385
429
  ...(hop.pushMulticalls || []),
386
430
  ];
387
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
+ }
388
444
  }
389
445
  else {
390
446
  // Start a new segment
@@ -404,12 +460,17 @@ function classifyIntoSegments(hops) {
404
460
  gasToken: hop.gasToken,
405
461
  gasFee: hop.gasFee,
406
462
  gasLimit: hop.gasLimit,
463
+ sizing: hop.sizing,
407
464
  };
408
465
  segments.push(currentSegment);
409
466
  }
410
467
  }
411
468
  return segments;
412
469
  }
470
+ /** Ordering for sizing-category severity (higher = stricter). */
471
+ function categoryRank(c) {
472
+ return c === 'C' ? 2 : c === 'B' ? 1 : 0;
473
+ }
413
474
  // ============================================================================
414
475
  // getSegmentType
415
476
  // ============================================================================
@@ -429,237 +490,365 @@ function getSegmentType(route) {
429
490
  // composeCascade
430
491
  // ============================================================================
431
492
  function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
432
- var _a, _b, _c, _d, _e, _f, _g;
433
- let accumulatedPushMulticalls = [];
434
- const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
435
- // Compute per-outbound nativeValueForGas from UEA balance
436
- // Each outbound segment needs native value for the gas swap on the destination chain.
437
- // The contract refunds excess, so over-allocating is safe.
438
- const numOutbounds = segments.filter((s) => s.type !== 'PUSH_EXECUTION').length;
439
- const CASCADE_GAS_RESERVE = BigInt(3e18); // 3 PC reserve for gas costs
440
- let perOutboundNativeValue;
441
- if (ueaBalance && numOutbounds > 0 && ueaBalance > CASCADE_GAS_RESERVE) {
442
- perOutboundNativeValue =
443
- (ueaBalance - CASCADE_GAS_RESERVE) / BigInt(numOutbounds);
444
- }
445
- for (let i = segments.length - 1; i >= 0; i--) {
446
- const segment = segments[i];
447
- switch (segment.type) {
448
- case 'PUSH_EXECUTION': {
449
- // Prepend Push Chain multicalls to accumulated
450
- accumulatedPushMulticalls = [
451
- ...(segment.mergedPushMulticalls || []),
452
- ...accumulatedPushMulticalls,
453
- ];
454
- 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);
455
549
  }
456
- case 'OUTBOUND_TO_CEA': {
457
- const firstHop = segment.hops[0];
458
- const isSvmSegment = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.isSvmTarget) === true;
459
- let outboundPayload;
460
- let targetForOutbound;
461
- const isMigration = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.isMigration) === true;
462
- if (isSvmSegment) {
463
- // SVM: use the pre-built SVM payload from the hop descriptor
464
- outboundPayload = (_a = firstHop.svmPayload) !== null && _a !== void 0 ? _a : '0x';
465
- // For SVM, to.address IS the target (program for execute, recipient for withdraw)
466
- const svmTarget = firstHop.params.to;
467
- targetForOutbound = svmTarget.address;
468
- }
469
- else if (isMigration) {
470
- // Migration: use raw 4-byte MIGRATION_SELECTOR, no multicall wrapping
471
- outboundPayload = (0, payload_builders_1.buildMigrationPayload)();
472
- targetForOutbound = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.ceaAddress) || ueaAddress;
473
- }
474
- else {
475
- // EVM: build CEA payload from merged multicalls
476
- outboundPayload = (0, payload_builders_1.buildCeaMulticallPayload)(segment.mergedCeaMulticalls || []);
477
- // Get CEA address from the first hop
478
- 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;
479
561
  }
480
- // Build outbound request
481
- 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);
482
- // Build approval + outbound multicalls
483
- const segGasFee = segment.gasFee || BigInt(0);
484
- const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
485
- prc20Token: segment.prc20Token || selectors_1.ZERO_ADDRESS,
486
- gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
487
- burnAmount: segment.totalBurnAmount || BigInt(0),
488
- gasFee: segGasFee,
489
- nativeValueForGas: perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : segGasFee * BigInt(1000),
490
- gatewayPcAddress,
491
- outboundRequest: outboundReq,
492
- });
493
- // Prepend to accumulated
494
- accumulatedPushMulticalls = [
495
- ...outboundMulticalls,
496
- ...accumulatedPushMulticalls,
497
- ];
498
- break;
499
- }
500
- case 'INBOUND_FROM_CEA': {
501
- // The accumulated multicalls = what runs on Push Chain AFTER inbound arrives.
502
- // Wrap in UniversalPayload struct with correct UEA nonce for the relay.
503
- // Build push multicalls from this hop's own data (e.g., counter.increment())
504
- // This is the Route 3 hop's payload that executes on Push Chain after inbound.
505
- const hop0 = segment.hops[0];
506
- if ((_c = hop0 === null || hop0 === void 0 ? void 0 : hop0.params) === null || _c === void 0 ? void 0 : _c.data) {
507
- const hopPushMulticalls = (0, payload_builders_1.buildExecuteMulticall)({
508
- execute: {
509
- to: hop0.params.to,
510
- value: hop0.params.value,
511
- data: hop0.params.data,
512
- },
513
- 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,
514
618
  });
515
- // Prepend the Route 3's own push calls before subsequent hops
619
+ // Prepend to accumulated
516
620
  accumulatedPushMulticalls = [
517
- ...hopPushMulticalls,
621
+ ...outboundMulticalls,
518
622
  ...accumulatedPushMulticalls,
519
623
  ];
624
+ break;
520
625
  }
521
- let intermediatePayload = '0x';
522
- if (accumulatedPushMulticalls.length > 0) {
523
- const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, ueaAddress, accumulatedPushMulticalls);
524
- // +1: the outbound tx consumes one nonce via execute()
525
- intermediatePayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: (ueaNonce !== null && ueaNonce !== void 0 ? ueaNonce : BigInt(0)) + BigInt(1) });
526
- }
527
- const hop = segment.hops[0];
528
- const sourceChain = hop.sourceChain;
529
- const ceaAddress = hop.ceaAddress || ueaAddress;
530
- // SVM chains: build SVM CPI payload instead of EVM CEA multicall
531
- if ((0, payload_builders_1.isSvmChain)(sourceChain)) {
532
- const lockerContract = chain_1.CHAIN_INFO[sourceChain].lockerContract;
533
- if (!lockerContract) {
534
- 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
+ }
535
646
  }
536
- const programPk = new web3_js_1.PublicKey(lockerContract);
537
- const gatewayProgramHex = ('0x' +
538
- Buffer.from(programPk.toBytes()).toString('hex'));
539
- let drainAmount = BigInt(0);
540
- let tokenMintHex;
541
- const params = hop.params;
542
- if (((_d = params.funds) === null || _d === void 0 ? void 0 : _d.amount) && params.funds.amount > BigInt(0)) {
543
- drainAmount = params.funds.amount;
544
- const token = params.funds.token;
545
- if (token && token.address) {
546
- const mintPk = new web3_js_1.PublicKey(token.address);
547
- tokenMintHex = ('0x' +
548
- 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}`);
549
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
+ }
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;
550
736
  }
551
- else if (params.value && params.value > BigInt(0)) {
552
- 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);
553
743
  }
554
- // Derive CEA PDA as revert recipient
555
- const ueaBytes = Buffer.from(ueaAddress.slice(2), 'hex');
556
- const [ceaPda] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from('push_identity'), ueaBytes], programPk);
557
- const ceaPdaHex = ('0x' +
558
- Buffer.from(ceaPda.toBytes()).toString('hex'));
559
- // Build SVM payload with intermediate Push Chain payload embedded
560
- const svmPayload = (0, payload_builders_1.encodeSvmCeaToUeaPayload)({
561
- gatewayProgramHex,
562
- drainAmount,
563
- tokenMintHex,
564
- revertRecipientHex: ceaPdaHex,
565
- extraPayload: intermediatePayload !== '0x'
566
- ? new Uint8Array(Buffer.from(intermediatePayload.slice(2), 'hex'))
567
- : undefined,
568
- });
569
- const svmBurnAmount = segment.totalBurnAmount || BigInt(0);
570
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, segment.prc20Token ||
571
- (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);
572
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
+ }
573
834
  const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
574
835
  prc20Token: segment.prc20Token ||
575
836
  (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork),
576
837
  gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
577
- burnAmount: svmBurnAmount,
838
+ burnAmount: segment.totalBurnAmount || BigInt(0),
578
839
  gasFee: inboundGasFee,
579
- nativeValueForGas: perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : inboundGasFee * BigInt(1000),
840
+ nativeValueForGas: evmInboundNativeValue,
580
841
  gatewayPcAddress,
581
842
  outboundRequest: outboundReq,
582
843
  });
844
+ // Reset accumulated -- everything is now inside this outbound
583
845
  accumulatedPushMulticalls = [...outboundMulticalls];
584
846
  break;
585
847
  }
586
- // EVM path: Build CEA multicall: [approve?, sendUniversalTxFromCEA(payload)]
587
- const ceaMulticalls = [];
588
- // Add hop's own CEA operations if any
589
- // (e.g., approve + swap before bridging back)
590
- if (hop.ceaMulticalls && hop.ceaMulticalls.length > 0) {
591
- ceaMulticalls.push(...hop.ceaMulticalls);
592
- }
593
- // Determine token/amount for inbound
594
- let tokenAddress = selectors_1.ZERO_ADDRESS;
595
- let amount = BigInt(0);
596
- const params = hop.params;
597
- if ((_f = params.funds) === null || _f === void 0 ? void 0 : _f.amount) {
598
- const token = params.funds.token;
599
- if (token) {
600
- if (token.mechanism === 'native') {
601
- amount = params.funds.amount;
602
- }
603
- else {
604
- tokenAddress = token.address;
605
- amount = params.funds.amount;
606
- // Add approve for ERC20 (CEA approves gateway)
607
- const gatewayAddr = chain_1.UNIVERSAL_GATEWAY_ADDRESSES[sourceChain];
608
- if (!gatewayAddr) {
609
- throw new Error(`No UniversalGateway address configured for chain ${sourceChain}`);
610
- }
611
- // Reset allowance to 0 first for USDT-style tokens
612
- const approveZeroData = (0, viem_1.encodeFunctionData)({
613
- abi: erc20_evm_1.ERC20_EVM,
614
- functionName: 'approve',
615
- args: [gatewayAddr, BigInt(0)],
616
- });
617
- ceaMulticalls.push({
618
- to: tokenAddress,
619
- value: BigInt(0),
620
- data: approveZeroData,
621
- });
622
- const approveData = (0, viem_1.encodeFunctionData)({
623
- abi: erc20_evm_1.ERC20_EVM,
624
- functionName: 'approve',
625
- args: [gatewayAddr, amount],
626
- });
627
- ceaMulticalls.push({
628
- to: tokenAddress,
629
- value: BigInt(0),
630
- data: approveData,
631
- });
632
- }
633
- }
634
- }
635
- else if (params.value && params.value > BigInt(0)) {
636
- amount = params.value;
637
- }
638
- // Build sendUniversalTxToUEA self-call on CEA (to=CEA, value=0)
639
- const sendCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, tokenAddress, amount, intermediatePayload, ceaAddress);
640
- ceaMulticalls.push(sendCall);
641
- // Wrap CEA multicall in outbound from Push Chain
642
- const ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
643
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, segment.prc20Token ||
644
- (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), segment.totalBurnAmount || BigInt(0), (_g = segment.gasLimit) !== null && _g !== void 0 ? _g : BigInt(0), ceaPayload, ueaAddress);
645
- const inboundGasFee = segment.gasFee || BigInt(0);
646
- const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
647
- prc20Token: segment.prc20Token ||
648
- (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork),
649
- gasToken: segment.gasToken || selectors_1.ZERO_ADDRESS,
650
- burnAmount: segment.totalBurnAmount || BigInt(0),
651
- gasFee: inboundGasFee,
652
- nativeValueForGas: perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : inboundGasFee * BigInt(1000),
653
- gatewayPcAddress,
654
- outboundRequest: outboundReq,
655
- });
656
- // Reset accumulated -- everything is now inside this outbound
657
- accumulatedPushMulticalls = [...outboundMulticalls];
658
- break;
659
848
  }
660
849
  }
661
- }
662
- return accumulatedPushMulticalls;
850
+ return accumulatedPushMulticalls;
851
+ });
663
852
  }
664
853
  // ============================================================================
665
854
  // createCascadedBuilder
@@ -752,7 +941,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
752
941
  const ueaNonceCascade = ueaCodeCascade !== undefined
753
942
  ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress)
754
943
  : BigInt(0);
755
- const composedMulticalls = composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonceCascade);
944
+ const composedMulticalls = yield composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonceCascade);
756
945
  // Execute the composed multicall as a single Push Chain tx
757
946
  const executeParams = {
758
947
  to: ueaAddress,
@@ -777,14 +966,77 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
777
966
  hops: hopInfos,
778
967
  hopCount: hops.length,
779
968
  waitForAll: (opts) => tslib_1.__awaiter(this, void 0, void 0, function* () {
780
- var _a, _b;
969
+ var _a, _b, _c, _d, _e;
781
970
  const { pollingIntervalMs = 10000, timeout = 300000, progressHook: cascadeProgressHook, eventHook, } = opts || {};
782
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
+ }
783
1028
  try {
784
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
+ }
785
1037
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
786
1038
  hopIndex: 0,
787
- 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',
788
1040
  chain: (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork),
789
1041
  status: 'waiting',
790
1042
  elapsed: Date.now() - startTime,
@@ -803,6 +1055,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
803
1055
  txHash: response.hash,
804
1056
  elapsed: Date.now() - startTime,
805
1057
  });
1058
+ emitHopComplete(hop.hopIndex);
806
1059
  }
807
1060
  }
808
1061
  // 2. Track outbound hops (Route 2: UOA_TO_CEA)
@@ -833,6 +1086,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
833
1086
  if (outboundHops.length === 1) {
834
1087
  // Single direct outbound hop: use the existing V1-based tracking
835
1088
  const hop = outboundHops[0];
1089
+ const hopHooks = (0, progress_route_hooks_1.pickWaitHooks)(hop.route);
836
1090
  const remainingTimeout = timeout - (Date.now() - startTime);
837
1091
  if (remainingTimeout <= 0) {
838
1092
  hop.status = 'failed';
@@ -843,14 +1097,16 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
843
1097
  status: 'timeout',
844
1098
  elapsed: Date.now() - startTime,
845
1099
  });
846
- eventHook === null || eventHook === void 0 ? void 0 : eventHook(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_99_04](Date.now() - startTime));
1100
+ emitHopEvent(eventHook, hopHooks.timeout(hop.executionChain, Date.now() - startTime));
1101
+ emitCascadeTimeout(hop.hopIndex);
847
1102
  return {
848
1103
  success: false,
849
1104
  hops: hopInfos,
850
1105
  failedAt: hop.hopIndex,
851
1106
  };
852
1107
  }
853
- eventHook === null || eventHook === void 0 ? void 0 : eventHook(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_08_01](hop.executionChain));
1108
+ emitHopStart(hop.hopIndex);
1109
+ emitHopEvent(eventHook, hopHooks.awaiting(hop.executionChain));
854
1110
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
855
1111
  hopIndex: hop.hopIndex,
856
1112
  route: hop.route,
@@ -875,14 +1131,15 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
875
1131
  if (event.status === 'polling' &&
876
1132
  lastEmittedStatus !== 'polling') {
877
1133
  lastEmittedStatus = 'polling';
878
- eventHook === null || eventHook === void 0 ? void 0 : eventHook(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_08_02](event.elapsed));
1134
+ emitHopEvent(eventHook, hopHooks.polling(hop.executionChain, event.elapsed));
879
1135
  }
880
1136
  },
881
1137
  });
882
1138
  hop.status = 'confirmed';
883
1139
  hop.txHash = outboundDetails.externalTxHash;
884
1140
  hop.outboundDetails = outboundDetails;
885
- eventHook === null || eventHook === void 0 ? void 0 : eventHook(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_99_03](outboundDetails));
1141
+ emitHopEvent(eventHook, hopHooks.success(outboundDetails));
1142
+ emitHopComplete(hop.hopIndex);
886
1143
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
887
1144
  hopIndex: hop.hopIndex,
888
1145
  route: hop.route,
@@ -895,9 +1152,15 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
895
1152
  catch (err) {
896
1153
  const errMsg = err instanceof Error ? err.message : String(err);
897
1154
  const isTimeout = errMsg.startsWith('Timeout waiting for outbound transaction');
898
- eventHook === null || eventHook === void 0 ? void 0 : eventHook(isTimeout
899
- ? progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_99_04](remainingTimeout)
900
- : progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_99_05](errMsg));
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
+ }
901
1164
  hop.status = 'failed';
902
1165
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
903
1166
  hopIndex: hop.hopIndex,
@@ -916,9 +1179,10 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
916
1179
  else {
917
1180
  // Multiple outbound hops: use V2 API which returns outboundTx[]
918
1181
  // Emit "Awaiting External Chain" once per outbound hop before
919
- // polling begins.
1182
+ // polling begins, using the route-specific awaiting hook.
920
1183
  for (const outHop of outboundHops) {
921
- eventHook === null || eventHook === void 0 ? void 0 : eventHook(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_08_01](outHop.executionChain));
1184
+ emitHopStart(outHop.hopIndex);
1185
+ emitHopEvent(eventHook, (0, progress_route_hooks_1.pickWaitHooks)(outHop.route).awaiting(outHop.executionChain));
922
1186
  }
923
1187
  const perHopLastStatus = new Map();
924
1188
  const confirmedHops = new Set();
@@ -927,6 +1191,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
927
1191
  pollingIntervalMs,
928
1192
  timeout: timeout - (Date.now() - startTime),
929
1193
  progressHook: (event) => {
1194
+ var _a, _b;
930
1195
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
931
1196
  hopIndex: event.hopIndex,
932
1197
  route: event.route,
@@ -935,11 +1200,13 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
935
1200
  txHash: event.txHash,
936
1201
  elapsed: Date.now() - startTime,
937
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);
938
1205
  const prev = perHopLastStatus.get(event.hopIndex);
939
1206
  if (event.status === 'polling' &&
940
1207
  prev !== 'polling') {
941
1208
  perHopLastStatus.set(event.hopIndex, 'polling');
942
- eventHook === null || eventHook === void 0 ? void 0 : eventHook(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_08_02](Date.now() - startTime));
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));
943
1210
  }
944
1211
  if (event.status === 'confirmed' &&
945
1212
  event.txHash &&
@@ -949,21 +1216,22 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
949
1216
  // outbound-sync.ts:348-355). Auto-promotion paths
950
1217
  // without a txHash are skipped by the `event.txHash`
951
1218
  // guard above, so we only reach here with full details.
952
- const matchedHop = outboundHops.find((h) => h.hopIndex === event.hopIndex);
953
1219
  if (matchedHop === null || matchedHop === void 0 ? void 0 : matchedHop.outboundDetails) {
954
1220
  confirmedHops.add(event.hopIndex);
955
- eventHook === null || eventHook === void 0 ? void 0 : eventHook(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_99_03](matchedHop.outboundDetails));
1221
+ emitHopEvent(eventHook, eventHooks.success(matchedHop.outboundDetails));
1222
+ emitHopComplete(event.hopIndex);
956
1223
  }
957
1224
  }
958
1225
  },
959
1226
  });
960
1227
  if (!allOutboundDetails.success) {
961
- const failedIdx = (_b = allOutboundDetails.failedAt) !== null && _b !== void 0 ? _b : 0;
1228
+ const failedIdx = (_d = allOutboundDetails.failedAt) !== null && _d !== void 0 ? _d : 0;
962
1229
  const failedHop = outboundHops.find((h) => h.hopIndex === failedIdx);
963
1230
  const failMsg = failedHop
964
1231
  ? `Outbound failed for hop ${failedIdx} on ${failedHop.executionChain}`
965
1232
  : `Outbound cascade failed at hop ${failedIdx}`;
966
- eventHook === null || eventHook === void 0 ? void 0 : eventHook(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_99_05](failMsg));
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);
967
1235
  return {
968
1236
  success: false,
969
1237
  hops: hopInfos,
@@ -973,9 +1241,13 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
973
1241
  }
974
1242
  }
975
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.
976
1247
  const inboundHops = hopInfos.filter((h) => h.route === 'CEA_TO_PUSH');
977
1248
  for (const inboundHop of inboundHops) {
978
1249
  inboundHop.status = 'submitted';
1250
+ emitHopStart(inboundHop.hopIndex);
979
1251
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
980
1252
  hopIndex: inboundHop.hopIndex,
981
1253
  route: inboundHop.route,
@@ -984,14 +1256,24 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
984
1256
  elapsed: Date.now() - startTime,
985
1257
  });
986
1258
  }
1259
+ emitCascadeSuccess();
987
1260
  return { success: true, hops: hopInfos };
988
1261
  }
989
1262
  catch (err) {
990
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
+ }
991
1273
  return {
992
1274
  success: false,
993
1275
  hops: hopInfos,
994
- failedAt: failedIdx >= 0 ? failedIdx : 0,
1276
+ failedAt,
995
1277
  };
996
1278
  }
997
1279
  }),