@pushchain/core 5.1.14 → 5.1.16

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 (26) hide show
  1. package/package.json +1 -1
  2. package/src/lib/orchestrator/internals/cascade.d.ts +16 -0
  3. package/src/lib/orchestrator/internals/cascade.js +205 -47
  4. package/src/lib/orchestrator/internals/error-decoder.d.ts +59 -0
  5. package/src/lib/orchestrator/internals/error-decoder.js +110 -0
  6. package/src/lib/orchestrator/internals/errors.d.ts +78 -0
  7. package/src/lib/orchestrator/internals/errors.js +83 -0
  8. package/src/lib/orchestrator/internals/outbound-sync.d.ts +2 -2
  9. package/src/lib/orchestrator/internals/outbound-sync.js +2 -2
  10. package/src/lib/orchestrator/internals/preflight.d.ts +80 -0
  11. package/src/lib/orchestrator/internals/preflight.js +145 -0
  12. package/src/lib/orchestrator/internals/push-chain-tx.d.ts +2 -18
  13. package/src/lib/orchestrator/internals/push-chain-tx.js +67 -40
  14. package/src/lib/orchestrator/internals/route-handlers.js +199 -114
  15. package/src/lib/orchestrator/internals/svm-rent.d.ts +46 -0
  16. package/src/lib/orchestrator/internals/svm-rent.js +100 -0
  17. package/src/lib/orchestrator/internals/tx-transformer.js +16 -1
  18. package/src/lib/orchestrator/orchestrator.js +34 -1
  19. package/src/lib/orchestrator/orchestrator.types.d.ts +27 -3
  20. package/src/lib/price-fetch/price-fetch.js +23 -5
  21. package/src/lib/progress-hook/progress-hook.js +130 -6
  22. package/src/lib/progress-hook/progress-hook.types.d.ts +8 -2
  23. package/src/lib/progress-hook/progress-hook.types.js +15 -0
  24. package/src/lib/universal/account/account.d.ts +1 -1
  25. package/src/lib/universal/account/account.js +1 -1
  26. package/src/lib/universal-tx-detector/child-inbounds.js +15 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushchain/core",
3
- "version": "5.1.14",
3
+ "version": "5.1.16",
4
4
  "homepage": "https://push.org",
5
5
  "keywords": [
6
6
  "pushchain",
@@ -15,6 +15,22 @@ export interface CascadeCallbacks {
15
15
  failedAt?: number;
16
16
  }>;
17
17
  }
18
+ /**
19
+ * Fan out a cascade progress event to up to two listeners with dedup.
20
+ *
21
+ * The two listener slots are:
22
+ * - `primary`: explicit `eventHook` from CascadeTrackOptions (passed at call
23
+ * time to `waitForAll` / `wait`).
24
+ * - `secondary`: init-time `ctx.progressHook` from PushChain.initialize.
25
+ *
26
+ * Both may be undefined; if both reference the same function we invoke once.
27
+ * Events with an empty `id` (the no-op sentinel returned by pickWaitHooks for
28
+ * R4/unknown routes) are dropped so consumers don't see placeholder frames.
29
+ *
30
+ * Exported for unit testing. Production code calls this from the
31
+ * `emitHopEvent` closure inside `createCascadedBuilder.waitForAll`.
32
+ */
33
+ export declare function dispatchCascadeProgressEvent(event: import('../../progress-hook/progress-hook.types').ProgressEvent, primary?: (e: import('../../progress-hook/progress-hook.types').ProgressEvent) => void, secondary?: (e: import('../../progress-hook/progress-hook.types').ProgressEvent) => void): void;
18
34
  export declare function prepareTransaction(ctx: OrchestratorContext, params: UniversalExecuteParams, callbacks: CascadeCallbacks): Promise<PreparedUniversalTx>;
19
35
  export declare function buildHopDescriptor(ctx: OrchestratorContext, params: UniversalExecuteParams, route: TransactionRoute, ueaAddress: `0x${string}`): Promise<HopDescriptor>;
20
36
  export declare function classifyIntoSegments(hops: HopDescriptor[]): CascadeSegment[];
@@ -6,6 +6,7 @@
6
6
  * getSegmentType, composeCascade, createCascadedBuilder, createChainedBuilder.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.dispatchCascadeProgressEvent = dispatchCascadeProgressEvent;
9
10
  exports.prepareTransaction = prepareTransaction;
10
11
  exports.buildHopDescriptor = buildHopDescriptor;
11
12
  exports.classifyIntoSegments = classifyIntoSegments;
@@ -33,6 +34,36 @@ const progress_route_hooks_1 = require("./progress-route-hooks");
33
34
  const progress_hook_1 = tslib_1.__importDefault(require("../../progress-hook/progress-hook"));
34
35
  const progress_hook_types_1 = require("../../progress-hook/progress-hook.types");
35
36
  const build_payload_1 = require("../svm-idl/build-payload");
37
+ const preflight_1 = require("./preflight");
38
+ const svm_rent_1 = require("./svm-rent");
39
+ const errors_1 = require("./errors");
40
+ const context_2 = require("./context");
41
+ // ============================================================================
42
+ // dispatchCascadeProgressEvent
43
+ // ============================================================================
44
+ /**
45
+ * Fan out a cascade progress event to up to two listeners with dedup.
46
+ *
47
+ * The two listener slots are:
48
+ * - `primary`: explicit `eventHook` from CascadeTrackOptions (passed at call
49
+ * time to `waitForAll` / `wait`).
50
+ * - `secondary`: init-time `ctx.progressHook` from PushChain.initialize.
51
+ *
52
+ * Both may be undefined; if both reference the same function we invoke once.
53
+ * Events with an empty `id` (the no-op sentinel returned by pickWaitHooks for
54
+ * R4/unknown routes) are dropped so consumers don't see placeholder frames.
55
+ *
56
+ * Exported for unit testing. Production code calls this from the
57
+ * `emitHopEvent` closure inside `createCascadedBuilder.waitForAll`.
58
+ */
59
+ function dispatchCascadeProgressEvent(event, primary, secondary) {
60
+ if (!event.id)
61
+ return;
62
+ if (primary)
63
+ primary(event);
64
+ if (secondary && secondary !== primary)
65
+ secondary(event);
66
+ }
36
67
  // ============================================================================
37
68
  // prepareTransaction
38
69
  // ============================================================================
@@ -89,7 +120,7 @@ function prepareTransaction(ctx, params, callbacks) {
89
120
  // ============================================================================
90
121
  function buildHopDescriptor(ctx, params, route, ueaAddress) {
91
122
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
92
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
123
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
93
124
  // Pass 0 when user omits gasLimit → contract uses per-chain baseGasLimitByChainNamespace
94
125
  const gasLimit = (_a = params.gasLimit) !== null && _a !== void 0 ? _a : BigInt(0);
95
126
  const routeStr = route;
@@ -147,8 +178,21 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
147
178
  let gasToken = selectors_1.ZERO_ADDRESS;
148
179
  let gasFee = BigInt(0);
149
180
  let sizing;
181
+ // Conditional CEA-ATA rent bump (SPL-only). Mirrors single-route
182
+ // executeUoaToCeaSvm path. See svm-rent.ts for rationale.
183
+ const splMintBase58 = burnAmount > BigInt(0)
184
+ ? (_d = (_c = params.funds) === null || _c === void 0 ? void 0 : _c.token) === null || _d === void 0 ? void 0 : _d.address
185
+ : undefined;
186
+ const effectiveGasLimit = yield (0, svm_rent_1.maybeBumpForCeaAtaRent)({
187
+ ctx,
188
+ ueaAddress,
189
+ targetChain,
190
+ splMintBase58,
191
+ burnAmount,
192
+ effectiveGasLimit: gasLimit,
193
+ });
150
194
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
151
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, targetChain);
195
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, effectiveGasLimit, targetChain);
152
196
  gasToken = result.gasToken;
153
197
  gasFee = result.gasFee;
154
198
  sizing = result.sizing;
@@ -161,7 +205,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
161
205
  sizing });
162
206
  }
163
207
  // EVM path: Resolve CEA address + build CEA multicalls
164
- const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_c = ctx.rpcUrls[targetChain]) === null || _c === void 0 ? void 0 : _c[0]);
208
+ const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, targetChain, (_e = ctx.rpcUrls[targetChain]) === null || _e === void 0 ? void 0 : _e[0]);
165
209
  // Migration path: raw MIGRATION_SELECTOR payload, no multicall wrapping
166
210
  if (params.migration) {
167
211
  const prc20Token = (0, helpers_1.getNativePRC20ForChain)(targetChain, ctx.pushNetwork);
@@ -191,7 +235,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
191
235
  // When ERC-20 funds are provided with a single payload, auto-prepend a
192
236
  // transfer() call so the tokens minted to the CEA are forwarded to the
193
237
  // target address. This mirrors the Route 1 behavior in buildExecuteMulticall.
194
- if ((_d = params.funds) === null || _d === void 0 ? void 0 : _d.amount) {
238
+ if ((_f = params.funds) === null || _f === void 0 ? void 0 : _f.amount) {
195
239
  const token = params.funds.token;
196
240
  if (token && token.mechanism !== 'native') {
197
241
  const erc20Transfer = (0, viem_1.encodeFunctionData)({
@@ -211,7 +255,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
211
255
  // native value to the CEA, and the multicall forwards it to the target.
212
256
  ceaMulticalls.push({
213
257
  to: target.address,
214
- value: (_e = params.value) !== null && _e !== void 0 ? _e : BigInt(0),
258
+ value: (_g = params.value) !== null && _g !== void 0 ? _g : BigInt(0),
215
259
  data: params.data,
216
260
  });
217
261
  }
@@ -227,7 +271,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
227
271
  });
228
272
  }
229
273
  }
230
- else if ((_f = params.funds) === null || _f === void 0 ? void 0 : _f.amount) {
274
+ else if ((_h = params.funds) === null || _h === void 0 ? void 0 : _h.amount) {
231
275
  // Funds-only transfer (no data, no value). Without this branch the
232
276
  // CEA multicall is empty and relayed funds stay in the CEA instead
233
277
  // of reaching the caller's recipient.
@@ -259,7 +303,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
259
303
  // Determine PRC-20 token and burn amount
260
304
  let prc20Token = selectors_1.ZERO_ADDRESS;
261
305
  let burnAmount = BigInt(0);
262
- if ((_g = params.funds) === null || _g === void 0 ? void 0 : _g.amount) {
306
+ if ((_j = params.funds) === null || _j === void 0 ? void 0 : _j.amount) {
263
307
  const token = params.funds.token;
264
308
  if (token) {
265
309
  prc20Token = push_chain_1.PushChain.utils.tokens.getPRC20Address(token).address;
@@ -317,7 +361,7 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
317
361
  Buffer.from(ceaPda.toBytes()).toString('hex'));
318
362
  let amount = BigInt(0);
319
363
  let prc20Token;
320
- if (((_h = params.funds) === null || _h === void 0 ? void 0 : _h.amount) && params.funds.amount > BigInt(0)) {
364
+ if (((_k = params.funds) === null || _k === void 0 ? void 0 : _k.amount) && params.funds.amount > BigInt(0)) {
321
365
  amount = params.funds.amount;
322
366
  const token = params.funds.token;
323
367
  if (token && token.address) {
@@ -348,14 +392,14 @@ function buildHopDescriptor(ctx, params, route, ueaAddress) {
348
392
  gasFee,
349
393
  sizing });
350
394
  }
351
- const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_j = ctx.rpcUrls[sourceChain]) === null || _j === void 0 ? void 0 : _j[0]);
395
+ const { cea: ceaAddress } = yield (0, cea_utils_1.getCEAAddress)(ueaAddress, sourceChain, (_l = ctx.rpcUrls[sourceChain]) === null || _l === void 0 ? void 0 : _l[0]);
352
396
  // CEA auto-deploys on-chain: Vault.finalizeUniversalTx calls CEAFactory.deployCEA()
353
397
  // if CEA doesn't exist yet. No SDK-side blocking needed.
354
398
  // Determine token/amount for the inbound
355
399
  let tokenAddress = selectors_1.ZERO_ADDRESS;
356
400
  let amount = BigInt(0);
357
401
  let nativeValue = BigInt(0);
358
- if ((_k = params.funds) === null || _k === void 0 ? void 0 : _k.amount) {
402
+ if ((_m = params.funds) === null || _m === void 0 ? void 0 : _m.amount) {
359
403
  const token = params.funds.token;
360
404
  if (token) {
361
405
  if (token.mechanism === 'native') {
@@ -520,7 +564,7 @@ function getSegmentType(route) {
520
564
  // ============================================================================
521
565
  function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
522
566
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
523
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
567
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
524
568
  let accumulatedPushMulticalls = [];
525
569
  const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
526
570
  // Compute per-outbound nativeValueForGas from UEA balance
@@ -528,6 +572,29 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
528
572
  // The contract refunds excess, so over-allocating is safe.
529
573
  const numOutbounds = segments.filter((s) => s.type !== 'PUSH_EXECUTION').length;
530
574
  const CASCADE_GAS_RESERVE = BigInt(3e18); // 3 PC reserve for gas costs
575
+ // Cascade pre-flight: catches the zero-balance silent-segment-zero failure
576
+ // mode. When `ueaBalance <= CASCADE_GAS_RESERVE`, `perOutboundNativeValue`
577
+ // is left undefined and downstream segments silently allocate value=0,
578
+ // producing under-funded swaps that revert inside Uniswap. Throw early
579
+ // with a structured error so the caller knows which path / hop failed.
580
+ // Honor the per-call opt-out from the first non-PUSH segment (segments share
581
+ // the same UniversalExecuteParams.options.allowUnderfundedSwap shape).
582
+ const firstOutboundSegment = segments.find((s) => s.type !== 'PUSH_EXECUTION');
583
+ const cascadeAllowUnderfunded = ((_d = (_c = (_b = (_a = firstOutboundSegment === null || firstOutboundSegment === void 0 ? void 0 : firstOutboundSegment.hops) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.params) === null || _c === void 0 ? void 0 : _c.options) === null || _d === void 0 ? void 0 : _d.allowUnderfundedSwap) ===
584
+ true;
585
+ if (numOutbounds > 0) {
586
+ (0, preflight_1.runPreflight)({
587
+ ctx,
588
+ ueaAddress,
589
+ ueaBalance: ueaBalance !== null && ueaBalance !== void 0 ? ueaBalance : BigInt(0),
590
+ // Required = at least one wei of allocatable PC after reserve. The
591
+ // per-segment min-swap check below catches finer-grained shortfalls.
592
+ requiredValue: BigInt(1),
593
+ gasReserve: CASCADE_GAS_RESERVE,
594
+ pathTag: 'CASCADE',
595
+ allowUnderfundedSwap: cascadeAllowUnderfunded,
596
+ });
597
+ }
531
598
  let perOutboundNativeValue;
532
599
  if (ueaBalance && numOutbounds > 0 && ueaBalance > CASCADE_GAS_RESERVE) {
533
600
  perOutboundNativeValue =
@@ -568,17 +635,57 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
568
635
  const perSvmCap = svmTotal / BigInt(svmSegments.length);
569
636
  const UNCAPPED_BALANCE = BigInt(1e30); // effectively disable estimator's internal cap
570
637
  for (const seg of svmSegments) {
638
+ // Cascade-level segment index (position in `segments`, not in `svmSegments`)
639
+ // so the error's `segmentIndex` lines up with what the caller sees.
640
+ const cascadeIdx = segments.indexOf(seg);
571
641
  const estimated = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, seg.gasToken, seg.gasFee, UNCAPPED_BALANCE);
642
+ // SVM warn-threshold telemetry (no truncation — pre-flight handles drain protection).
643
+ (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, estimated, seg.gasToken, 'CASCADE');
572
644
  const flat = perOutboundNativeValue !== null && perOutboundNativeValue !== void 0 ? perOutboundNativeValue : BigInt(0);
573
645
  let value = estimated > flat ? estimated : flat;
574
646
  if (perSvmCap > BigInt(0) && value > perSvmCap)
575
647
  value = perSvmCap;
576
- (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}`);
648
+ // Upward-allocation ceiling (mirrors R3 SVM Fix #4 in route-handlers.ts:1690).
649
+ // When UEA balance is huge, the flat split or perSvmCap can produce a value
650
+ // far above what the swap actually needs. Submitting that into a Uniswap
651
+ // pool moves the price for other users. Cap at max(200 PC, 5×estimated):
652
+ // - fair pool (estimated ≈ 100 PC): cap = 500 PC → caps voluntary excess
653
+ // - skewed pool (estimated > 200 PC): cap = 5×estimated → still allows
654
+ // pool-skew margin; min-swap check below still fires if value < estimated
655
+ // The contract still refunds excess via swapAndBurnGas, so this is purely
656
+ // a guard against submitting wasteful msg.value to fair-priced pools.
657
+ const UPWARD_BASE_CEILING_CASCADE_SVM = BigInt(200) * BigInt(1e18);
658
+ const upwardCeilingCascadeSvm = estimated * BigInt(5) > UPWARD_BASE_CEILING_CASCADE_SVM
659
+ ? estimated * BigInt(5)
660
+ : UPWARD_BASE_CEILING_CASCADE_SVM;
661
+ if (value > upwardCeilingCascadeSvm) {
662
+ (0, context_1.printLog)(ctx, `composeCascade — SVM outbound to ${(_e = seg.hops[0]) === null || _e === void 0 ? void 0 : _e.targetChain}: capping value from ${value} to upwardCeiling=${upwardCeilingCascadeSvm} (pool quote=${estimated})`);
663
+ value = upwardCeilingCascadeSvm;
664
+ }
665
+ (0, context_1.printLog)(ctx, `composeCascade — SVM outbound to ${(_f = seg.hops[0]) === null || _f === void 0 ? void 0 : _f.targetChain}: gasFee=${seg.gasFee}, estimatedWpc=${estimated}, flat=${flat}, perSvmCap=${perSvmCap}, upwardCeiling=${upwardCeilingCascadeSvm}, chosen=${value}`);
666
+ // Per-segment minimum-viable-swap check: if the chosen `value` is below
667
+ // the live pool quote (`estimated`), the cap squeezed the budget below
668
+ // what the swap actually needs and the segment will revert inside
669
+ // Uniswap with STF. Throw cleanly with `segmentIndex` so the caller
670
+ // knows which hop's pool is misbehaving.
671
+ if (estimated > BigInt(0) && value < estimated && !cascadeAllowUnderfunded) {
672
+ const shortfall = estimated - value;
673
+ (0, context_2.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_003_04, estimated, value, shortfall, ueaAddress, 'CASCADE', { kind: 'NATIVE', segmentIndex: cascadeIdx });
674
+ throw new errors_1.InsufficientUEABalanceError({
675
+ required: estimated,
676
+ available: value,
677
+ shortfall,
678
+ ueaAddress,
679
+ pathTag: 'CASCADE',
680
+ reason: 'NATIVE',
681
+ segmentIndex: cascadeIdx,
682
+ });
683
+ }
577
684
  svmNativeValueBySegment.set(seg, value);
578
685
  }
579
686
  }
580
- for (let i = segments.length - 1; i >= 0; i--) {
581
- const segment = segments[i];
687
+ for (let segIdxLoop = segments.length - 1; segIdxLoop >= 0; segIdxLoop--) {
688
+ const segment = segments[segIdxLoop];
582
689
  switch (segment.type) {
583
690
  case 'PUSH_EXECUTION': {
584
691
  // Prepend Push Chain multicalls to accumulated
@@ -596,7 +703,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
596
703
  const isMigration = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.isMigration) === true;
597
704
  if (isSvmSegment) {
598
705
  // SVM: use the pre-built SVM payload from the hop descriptor
599
- outboundPayload = (_b = firstHop.svmPayload) !== null && _b !== void 0 ? _b : '0x';
706
+ outboundPayload = (_g = firstHop.svmPayload) !== null && _g !== void 0 ? _g : '0x';
600
707
  // For SVM, to.address IS the target (program for execute, recipient for withdraw)
601
708
  const svmTarget = firstHop.params.to;
602
709
  targetForOutbound = svmTarget.address;
@@ -612,8 +719,40 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
612
719
  // Get CEA address from the first hop
613
720
  targetForOutbound = (firstHop === null || firstHop === void 0 ? void 0 : firstHop.ceaAddress) || ueaAddress;
614
721
  }
722
+ // Per-segment PRC-20 burn-balance pre-check (cascade R2-style segments
723
+ // only — INBOUND_FROM_CEA segments are payload-only with totalBurnAmount=0
724
+ // per the 2026-03-19 fix). Skip MIGRATION (totalBurnAmount=0). Fires
725
+ // whenever burnAmount > 0 even when burn token equals gas token — the
726
+ // gateway's transferFrom happens BEFORE the gas swap, so user must
727
+ // pre-hold the burn amount.
728
+ const segBurnAmount = segment.totalBurnAmount || BigInt(0);
729
+ const segBurnToken = segment.prc20Token;
730
+ if (segBurnAmount > BigInt(0) && segBurnToken && !isMigration) {
731
+ const onHand = yield ctx.pushClient.readContract({
732
+ address: segBurnToken,
733
+ abi: erc20_evm_1.ERC20_EVM,
734
+ functionName: 'balanceOf',
735
+ args: [ueaAddress],
736
+ });
737
+ const sufficient = onHand >= segBurnAmount;
738
+ (0, context_2.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_003_03, segBurnAmount, onHand, sufficient, ueaAddress, 'CASCADE', { kind: 'PRC20', burnToken: segBurnToken, segmentIndex: segIdxLoop });
739
+ if (!sufficient && !cascadeAllowUnderfunded) {
740
+ const shortfall = segBurnAmount - onHand;
741
+ (0, context_2.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_003_04, segBurnAmount, onHand, shortfall, ueaAddress, 'CASCADE', { kind: 'PRC20', burnToken: segBurnToken, segmentIndex: segIdxLoop });
742
+ throw new errors_1.InsufficientUEABalanceError({
743
+ required: segBurnAmount,
744
+ available: onHand,
745
+ shortfall,
746
+ ueaAddress,
747
+ pathTag: 'CASCADE',
748
+ reason: 'PRC20',
749
+ burnToken: segBurnToken,
750
+ segmentIndex: segIdxLoop,
751
+ });
752
+ }
753
+ }
615
754
  // Build outbound request
616
- 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);
755
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(targetForOutbound, segment.prc20Token || selectors_1.ZERO_ADDRESS, segment.totalBurnAmount || BigInt(0), (_h = segment.gasLimit) !== null && _h !== void 0 ? _h : BigInt(0), outboundPayload, ueaAddress);
617
756
  // Build approval + outbound multicalls
618
757
  const segGasFee = segment.gasFee || BigInt(0);
619
758
  const svmOverride = svmNativeValueBySegment.get(segment);
@@ -629,7 +768,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
629
768
  else {
630
769
  evmFallbackValue = segGasFee * BigInt(1000000);
631
770
  }
632
- let nativeValueForGas = (_d = svmOverride !== null && svmOverride !== void 0 ? svmOverride : perOutboundNativeValue) !== null && _d !== void 0 ? _d : evmFallbackValue;
771
+ let nativeValueForGas = (_j = svmOverride !== null && svmOverride !== void 0 ? svmOverride : perOutboundNativeValue) !== null && _j !== void 0 ? _j : evmFallbackValue;
633
772
  // SDK 5.2 Case C: when the segment's sizing is category C, compose
634
773
  // R2/OUTBOUND_TO_CEA segments no longer apply Case A/B/C sizing
635
774
  // (scoped to R1 + R3 only). segment.sizing may still be populated
@@ -661,7 +800,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
661
800
  // UEA multicall so no nested outbound is needed.
662
801
  const mergedHopMulticalls = [];
663
802
  for (const h of segment.hops) {
664
- 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)) {
803
+ if (((_k = h === null || h === void 0 ? void 0 : h.params) === null || _k === void 0 ? void 0 : _k.data) || ((_l = h === null || h === void 0 ? void 0 : h.params) === null || _l === void 0 ? void 0 : _l.value)) {
665
804
  const hopCalls = (0, payload_builders_1.buildExecuteMulticall)({
666
805
  execute: {
667
806
  to: h.params.to,
@@ -702,7 +841,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
702
841
  let drainAmount = BigInt(0);
703
842
  let tokenMintHex;
704
843
  const params = hop.params;
705
- if (((_g = params.funds) === null || _g === void 0 ? void 0 : _g.amount) && params.funds.amount > BigInt(0)) {
844
+ if (((_m = params.funds) === null || _m === void 0 ? void 0 : _m.amount) && params.funds.amount > BigInt(0)) {
706
845
  drainAmount = params.funds.amount;
707
846
  const token = params.funds.token;
708
847
  if (token && token.address) {
@@ -731,7 +870,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
731
870
  });
732
871
  const svmBurnAmount = segment.totalBurnAmount || BigInt(0);
733
872
  const outboundReq = (0, payload_builders_1.buildOutboundRequest)(gatewayProgramHex, segment.prc20Token ||
734
- (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), svmBurnAmount, (_h = segment.gasLimit) !== null && _h !== void 0 ? _h : BigInt(0), svmPayload, ueaAddress);
873
+ (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork), svmBurnAmount, (_o = segment.gasLimit) !== null && _o !== void 0 ? _o : BigInt(0), svmPayload, ueaAddress);
735
874
  const inboundGasFee = segment.gasFee || BigInt(0);
736
875
  let svmInboundFallback;
737
876
  if (ueaBalance && ueaBalance > BigInt(0)) {
@@ -744,7 +883,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
744
883
  // SDK 5.2 R3-style Case C bump: when the inbound segment's sizing
745
884
  // is C, top up the swap budget by the overflow. No bridge-swap —
746
885
  // R3 has no destination funds delivery.
747
- if (((_j = segment.sizing) === null || _j === void 0 ? void 0 : _j.category) === 'C' &&
886
+ if (((_p = segment.sizing) === null || _p === void 0 ? void 0 : _p.category) === 'C' &&
748
887
  segment.sizing.overflowNativePc > BigInt(0)) {
749
888
  const bumped = svmInboundNativeValue + segment.sizing.overflowNativePc;
750
889
  (0, context_1.printLog)(ctx, `composeCascade — INBOUND_FROM_CEA (SVM) Case C: bumping nativeValueForGas from ${svmInboundNativeValue} to ${bumped} (overflow=${segment.sizing.overflowNativePc})`);
@@ -779,7 +918,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
779
918
  let amount = BigInt(0);
780
919
  for (const mergedHop of segment.hops) {
781
920
  const hParams = mergedHop.params;
782
- if ((_k = hParams.funds) === null || _k === void 0 ? void 0 : _k.amount) {
921
+ if ((_q = hParams.funds) === null || _q === void 0 ? void 0 : _q.amount) {
783
922
  const token = hParams.funds.token;
784
923
  if (token) {
785
924
  if (token.mechanism === 'native') {
@@ -854,7 +993,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
854
993
  // SDK 5.2 R3-style Case C bump: when the inbound segment's sizing
855
994
  // is C, top up the swap budget by the overflow. No bridge-swap —
856
995
  // R3 has no destination funds delivery.
857
- if (((_l = segment.sizing) === null || _l === void 0 ? void 0 : _l.category) === 'C' &&
996
+ if (((_r = segment.sizing) === null || _r === void 0 ? void 0 : _r.category) === 'C' &&
858
997
  segment.sizing.overflowNativePc > BigInt(0)) {
859
998
  const bumped = evmInboundNativeValue + segment.sizing.overflowNativePc;
860
999
  (0, context_1.printLog)(ctx, `composeCascade — INBOUND_FROM_CEA (EVM) Case C: bumping nativeValueForGas from ${evmInboundNativeValue} to ${bumped} (overflow=${segment.sizing.overflowNativePc})`);
@@ -885,6 +1024,7 @@ function composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonce) {
885
1024
  function createCascadedBuilder(ctx, preparedTxs, callbacks) {
886
1025
  return {
887
1026
  send: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
1027
+ var _a;
888
1028
  const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
889
1029
  // Extract HopDescriptors
890
1030
  const hops = preparedTxs.map((tx) => tx._hop);
@@ -970,7 +1110,22 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
970
1110
  const ueaNonceCascade = ueaCodeCascade !== undefined
971
1111
  ? yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress)
972
1112
  : BigInt(0);
973
- const composedMulticalls = yield composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonceCascade);
1113
+ let composedMulticalls;
1114
+ try {
1115
+ composedMulticalls = yield composeCascade(ctx, segments, ueaAddress, ueaBalance, ueaNonceCascade);
1116
+ }
1117
+ catch (err) {
1118
+ // Pre-flight or per-segment min-swap failure inside composeCascade
1119
+ // throws before the cascade ever broadcasts. Fire 999-02 with the
1120
+ // segmentIndex (1-indexed for the failedAt slot) so consumers see a
1121
+ // terminal "cascade failed" frame instead of the stream stopping
1122
+ // silently after 203-04.
1123
+ if (err instanceof errors_1.InsufficientUEABalanceError) {
1124
+ const failedHopIndex = (_a = err.segmentIndex) !== null && _a !== void 0 ? _a : 0;
1125
+ (0, context_2.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_02, failedHopIndex + 1, preparedTxs.length, err.message);
1126
+ }
1127
+ throw err;
1128
+ }
974
1129
  // Execute the composed multicall as a single Push Chain tx
975
1130
  const executeParams = {
976
1131
  to: ueaAddress,
@@ -998,14 +1153,17 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
998
1153
  var _a, _b, _c, _d, _e;
999
1154
  const { pollingIntervalMs = 10000, timeout = 300000, progressHook: cascadeProgressHook, eventHook, } = opts || {};
1000
1155
  const startTime = Date.now();
1001
- // Per-hop progress emission. Drops events whose id is '' (the no-op
1002
- // sentinel returned by pickWaitHooks for R4/unknown routes), so
1003
- // consumers don't see placeholder events while the cascade still
1004
- // drives its own state machine.
1005
- const emitHopEvent = (hook, event) => {
1006
- if (!hook || !event.id)
1007
- return;
1008
- hook(event);
1156
+ // Per-hop progress emission. Fans out cascade markers to BOTH the
1157
+ // explicit `eventHook` (passed into waitForAll) AND the init-time
1158
+ // `ctx.progressHook` (set on PushChain.initialize), deduping if the
1159
+ // two are the same reference. This is the bridge that lets UI-kit
1160
+ // consumers — which only listen on `ctx.progressHook` — receive the
1161
+ // cascade-level marker stream (001 / 002-xx / 203-xx / 204-xx /
1162
+ // 209-xx / 299-01 / 999-xx) without having to manually wire
1163
+ // `eventHook` at every call site. Drops events whose id is '' (the
1164
+ // no-op sentinel returned by pickWaitHooks for R4/unknown routes).
1165
+ const emitHopEvent = (event) => {
1166
+ dispatchCascadeProgressEvent(event, eventHook, ctx.progressHook);
1009
1167
  };
1010
1168
  // ---- Multichain (multi-hop) cascade markers (001 / 002 / 999) ----
1011
1169
  // Only fire when this is genuinely a multi-hop cascade. Single-hop
@@ -1026,7 +1184,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1026
1184
  ? signerOriginChain
1027
1185
  : chainLabels[hopIndex0 - 1];
1028
1186
  const toChain = chainLabels[hopIndex0];
1029
- emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_002_01](n, totalHops, fromChain, toChain));
1187
+ emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_002_01](n, totalHops, fromChain, toChain));
1030
1188
  };
1031
1189
  const emitHopComplete = (hopIndex0) => {
1032
1190
  if (!isMulti)
@@ -1034,25 +1192,25 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1034
1192
  const n = hopIndex0 + 1;
1035
1193
  if (n >= totalHops)
1036
1194
  return; // skip after final hop — 999-01 takes over
1037
- emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_002_99_99](n, totalHops));
1195
+ emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_002_99_99](n, totalHops));
1038
1196
  };
1039
1197
  const emitCascadeSuccess = () => {
1040
1198
  if (!isMulti)
1041
1199
  return;
1042
- emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_01](totalHops));
1200
+ emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_01](totalHops));
1043
1201
  };
1044
1202
  const emitCascadeFailed = (failedIdx0, errMsg) => {
1045
1203
  if (!isMulti)
1046
1204
  return;
1047
- emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_02](failedIdx0 + 1, totalHops, errMsg));
1205
+ emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_02](failedIdx0 + 1, totalHops, errMsg));
1048
1206
  };
1049
1207
  const emitCascadeTimeout = (failedIdx0) => {
1050
1208
  if (!isMulti)
1051
1209
  return;
1052
- emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_03](failedIdx0 + 1, totalHops));
1210
+ emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_03](failedIdx0 + 1, totalHops));
1053
1211
  };
1054
1212
  if (isMulti) {
1055
- emitHopEvent(eventHook, progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_001](totalHops, chainLabels));
1213
+ emitHopEvent(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_001](totalHops, chainLabels));
1056
1214
  }
1057
1215
  try {
1058
1216
  // 1. Wait for initial Push Chain tx confirmation
@@ -1126,7 +1284,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1126
1284
  status: 'timeout',
1127
1285
  elapsed: Date.now() - startTime,
1128
1286
  });
1129
- emitHopEvent(eventHook, hopHooks.timeout(hop.executionChain, Date.now() - startTime));
1287
+ emitHopEvent(hopHooks.timeout(hop.executionChain, Date.now() - startTime));
1130
1288
  emitCascadeTimeout(hop.hopIndex);
1131
1289
  return {
1132
1290
  success: false,
@@ -1135,7 +1293,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1135
1293
  };
1136
1294
  }
1137
1295
  emitHopStart(hop.hopIndex);
1138
- emitHopEvent(eventHook, hopHooks.awaiting(hop.executionChain));
1296
+ emitHopEvent(hopHooks.awaiting(hop.executionChain));
1139
1297
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1140
1298
  hopIndex: hop.hopIndex,
1141
1299
  route: hop.route,
@@ -1160,14 +1318,14 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1160
1318
  if (event.status === 'polling' &&
1161
1319
  lastEmittedStatus !== 'polling') {
1162
1320
  lastEmittedStatus = 'polling';
1163
- emitHopEvent(eventHook, hopHooks.polling(hop.executionChain, event.elapsed));
1321
+ emitHopEvent(hopHooks.polling(hop.executionChain, event.elapsed));
1164
1322
  }
1165
1323
  },
1166
1324
  });
1167
1325
  hop.status = 'confirmed';
1168
1326
  hop.txHash = outboundDetails.externalTxHash;
1169
1327
  hop.outboundDetails = outboundDetails;
1170
- emitHopEvent(eventHook, hopHooks.success(outboundDetails));
1328
+ emitHopEvent(hopHooks.success(outboundDetails));
1171
1329
  emitHopComplete(hop.hopIndex);
1172
1330
  cascadeProgressHook === null || cascadeProgressHook === void 0 ? void 0 : cascadeProgressHook({
1173
1331
  hopIndex: hop.hopIndex,
@@ -1181,7 +1339,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1181
1339
  catch (err) {
1182
1340
  const errMsg = err instanceof Error ? err.message : String(err);
1183
1341
  const isTimeout = errMsg.startsWith('Timeout waiting for outbound transaction');
1184
- emitHopEvent(eventHook, isTimeout
1342
+ emitHopEvent(isTimeout
1185
1343
  ? hopHooks.timeout(hop.executionChain, remainingTimeout)
1186
1344
  : hopHooks.failed(hop.executionChain, errMsg));
1187
1345
  if (isTimeout) {
@@ -1211,7 +1369,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1211
1369
  // polling begins, using the route-specific awaiting hook.
1212
1370
  for (const outHop of outboundHops) {
1213
1371
  emitHopStart(outHop.hopIndex);
1214
- emitHopEvent(eventHook, (0, progress_route_hooks_1.pickWaitHooks)(outHop.route).awaiting(outHop.executionChain));
1372
+ emitHopEvent((0, progress_route_hooks_1.pickWaitHooks)(outHop.route).awaiting(outHop.executionChain));
1215
1373
  }
1216
1374
  const perHopLastStatus = new Map();
1217
1375
  const confirmedHops = new Set();
@@ -1235,7 +1393,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1235
1393
  if (event.status === 'polling' &&
1236
1394
  prev !== 'polling') {
1237
1395
  perHopLastStatus.set(event.hopIndex, 'polling');
1238
- 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));
1396
+ emitHopEvent(eventHooks.polling((_b = (_a = matchedHop === null || matchedHop === void 0 ? void 0 : matchedHop.executionChain) !== null && _a !== void 0 ? _a : event.chain) !== null && _b !== void 0 ? _b : '', Date.now() - startTime));
1239
1397
  }
1240
1398
  if (event.status === 'confirmed' &&
1241
1399
  event.txHash &&
@@ -1247,7 +1405,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1247
1405
  // guard above, so we only reach here with full details.
1248
1406
  if (matchedHop === null || matchedHop === void 0 ? void 0 : matchedHop.outboundDetails) {
1249
1407
  confirmedHops.add(event.hopIndex);
1250
- emitHopEvent(eventHook, eventHooks.success(matchedHop.outboundDetails));
1408
+ emitHopEvent(eventHooks.success(matchedHop.outboundDetails));
1251
1409
  emitHopComplete(event.hopIndex);
1252
1410
  }
1253
1411
  }
@@ -1259,7 +1417,7 @@ function createCascadedBuilder(ctx, preparedTxs, callbacks) {
1259
1417
  const failMsg = failedHop
1260
1418
  ? `Outbound failed for hop ${failedIdx} on ${failedHop.executionChain}`
1261
1419
  : `Outbound cascade failed at hop ${failedIdx}`;
1262
- 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));
1420
+ emitHopEvent((0, progress_route_hooks_1.pickWaitHooks)(failedHop === null || failedHop === void 0 ? void 0 : failedHop.route).failed((_e = failedHop === null || failedHop === void 0 ? void 0 : failedHop.executionChain) !== null && _e !== void 0 ? _e : '', failMsg));
1263
1421
  emitCascadeFailed(failedIdx, failMsg);
1264
1422
  return {
1265
1423
  success: false,
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Decoder for revert-error messages surfaced by Cosmos `tx.rawLog`.
3
+ *
4
+ * Two extraction paths:
5
+ * 1. 4-byte custom selector (e.g. `0xacfdb444:` or `0xf4d678b8000000…`) —
6
+ * looked up against `KNOWN_ERROR_SELECTORS` for a friendly name + hint.
7
+ * 2. `Error(string)` ABI-encoded revert (`0x08c379a0` selector + offset +
8
+ * length + data) — decoded into the original UTF-8 string.
9
+ *
10
+ * Unknown selectors fall through with `kind: 'unknown'` so callers can log
11
+ * them at INFO and grow the table from real reverts.
12
+ *
13
+ * The selector→name mappings below are EMPIRICAL: they were observed in
14
+ * production e2e logs and SDK test fixtures. `keccak256` of the obvious
15
+ * parameterless signatures (e.g. `keccak256("InsufficientBalance()")` =
16
+ * `0x01e1e567`) does NOT reproduce the listed selectors, so the deployed-
17
+ * source mapping is still unverified. Names and hints are worded as
18
+ * "likely cause" until the bytecode-source trace lands.
19
+ */
20
+ export interface KnownErrorSelector {
21
+ /** Best-effort error name. Worded as a likely cause until source-traced. */
22
+ name: string;
23
+ /** User-facing remediation hint. */
24
+ hint: string;
25
+ /** Where this mapping was first observed (log path or fixture). */
26
+ provenance: string;
27
+ }
28
+ export declare const KNOWN_ERROR_SELECTORS: Record<`0x${string}`, KnownErrorSelector>;
29
+ /** Selector for `Error(string)` (Solidity `require(false, "msg")`). */
30
+ export declare const ERROR_STRING_SELECTOR: `0x${string}`;
31
+ export type DecodedRevertCustom = {
32
+ kind: 'custom';
33
+ selector: `0x${string}`;
34
+ name: string;
35
+ hint: string;
36
+ provenance: string;
37
+ };
38
+ export type DecodedRevertString = {
39
+ kind: 'string';
40
+ selector: `0x${string}`;
41
+ decoded: string;
42
+ };
43
+ export type DecodedRevertUnknown = {
44
+ kind: 'unknown';
45
+ selector: `0x${string}`;
46
+ };
47
+ export type DecodedRevert = DecodedRevertCustom | DecodedRevertString | DecodedRevertUnknown;
48
+ /**
49
+ * Extracts the first hex blob after `ret ` from a Cosmos tx error message,
50
+ * pulls the 4-byte selector, and looks it up. Returns `null` when no blob
51
+ * is found.
52
+ *
53
+ * **Assumption**: today's Cosmos `tx.rawLog` format contains a single
54
+ * `ret 0x…:` substring per failed message. If a future log format includes
55
+ * nested call traces with multiple `ret` substrings, this regex will pick
56
+ * the first selector — which may not be the most relevant one. Revisit
57
+ * if multi-revert log formats appear in production.
58
+ */
59
+ export declare function decodeRevert(errorMsg: string): DecodedRevert | null;