@pushchain/core 5.1.16 → 5.1.18

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/lib/constants/index.d.ts +1 -1
  3. package/src/lib/constants/index.js +2 -1
  4. package/src/lib/constants/selectors.d.ts +8 -0
  5. package/src/lib/constants/selectors.js +9 -1
  6. package/src/lib/formatters.d.ts +1 -0
  7. package/src/lib/formatters.js +32 -0
  8. package/src/lib/orchestrator/internals/cascade.d.ts +8 -1
  9. package/src/lib/orchestrator/internals/cascade.js +486 -143
  10. package/src/lib/orchestrator/internals/error-decoder.d.ts +2 -6
  11. package/src/lib/orchestrator/internals/error-decoder.js +10 -10
  12. package/src/lib/orchestrator/internals/errors.d.ts +1 -1
  13. package/src/lib/orchestrator/internals/errors.js +7 -4
  14. package/src/lib/orchestrator/internals/execute-standard.js +7 -6
  15. package/src/lib/orchestrator/internals/gas-calculator.js +78 -11
  16. package/src/lib/orchestrator/internals/helpers.d.ts +2 -1
  17. package/src/lib/orchestrator/internals/helpers.js +13 -11
  18. package/src/lib/orchestrator/internals/outbound-sync.d.ts +1 -0
  19. package/src/lib/orchestrator/internals/outbound-sync.js +93 -37
  20. package/src/lib/orchestrator/internals/preflight.d.ts +9 -23
  21. package/src/lib/orchestrator/internals/preflight.js +22 -48
  22. package/src/lib/orchestrator/internals/response-builder.js +23 -7
  23. package/src/lib/orchestrator/internals/route-handlers.js +34 -49
  24. package/src/lib/orchestrator/internals/tx-transformer.js +2 -1
  25. package/src/lib/orchestrator/orchestrator.d.ts +7 -1
  26. package/src/lib/orchestrator/orchestrator.js +40 -2
  27. package/src/lib/orchestrator/orchestrator.types.d.ts +4 -14
  28. package/src/lib/progress-hook/progress-hook.js +66 -16
  29. package/src/lib/progress-hook/progress-hook.types.d.ts +4 -1
  30. package/src/lib/progress-hook/progress-hook.types.js +8 -0
  31. package/src/lib/push-chain/push-chain.d.ts +11 -1
  32. package/src/lib/push-chain/push-chain.js +2 -2
  33. package/src/lib/universal-tx-detector/cascade.js +14 -4
  34. package/src/lib/universal-tx-detector/child-inbounds.js +9 -1
  35. package/src/lib/universal-tx-detector/detector.js +9 -3
  36. package/src/lib/universal-tx-detector/normalize.d.ts +42 -0
  37. package/src/lib/universal-tx-detector/normalize.js +127 -0
  38. package/src/lib/utils/external-tx-hash.d.ts +37 -0
  39. package/src/lib/utils/external-tx-hash.js +62 -0
@@ -23,7 +23,7 @@ import { type PreflightPathTag } from './errors';
23
23
  export interface RunPreflightOpts {
24
24
  ctx: OrchestratorContext;
25
25
  ueaAddress: `0x${string}`;
26
- /** Effective UEA balance in wei UPC (already includes fresh-wallet prediction). */
26
+ /** Effective UEA balance in native PC wei (already includes fresh-wallet prediction). */
27
27
  ueaBalance: bigint;
28
28
  /** Buffered pool quote (pre-clamp) — what the SDK would actually attempt. */
29
29
  requiredValue: bigint;
@@ -36,45 +36,31 @@ export interface RunPreflightOpts {
36
36
  prc20Balance?: bigint;
37
37
  /** Cascade-only: identifies which segment failed. */
38
38
  segmentIndex?: number;
39
- /**
40
- * Per-call opt-out (`params.options.allowUnderfundedSwap`). When `true`,
41
- * pre-flight logs the shortfall but does NOT fire 203-04 or throw —
42
- * caller can fall back to the legacy clamp-and-refund branch.
43
- */
44
- allowUnderfundedSwap?: boolean;
45
39
  }
46
40
  export type PreflightResult = {
47
41
  ok: true;
48
42
  /** What `nativeValueForGas` should be (== requiredValue when sufficient). */
49
43
  adjustedValue: bigint;
50
- } | {
51
- ok: false;
52
- /** Set when allowUnderfundedSwap=true: the legacy clamped value. */
53
- legacyClampedValue: bigint;
54
44
  };
55
45
  /**
56
46
  * Pre-flight check. PRC-20 balance is checked before the native check —
57
47
  * a missing burn token fails before the gas-balance check, so the user
58
- * sees the actionable PRC-20 shortfall instead of a stale UPC error.
59
- *
60
- * Returns a result object instead of throwing for the legacy opt-out
61
- * path. The throw happens here (not in the caller) for the default
62
- * `allowUnderfundedSwap=false` case so all 6 paths share one throw site.
48
+ * sees the actionable PRC-20 shortfall instead of a stale native PC error.
63
49
  */
64
50
  export declare function runPreflight(opts: RunPreflightOpts): PreflightResult;
65
51
  /**
66
52
  * SVM-only telemetry threshold. Buffered pool quotes above this value
67
- * fire `SEND-TX-203-05` (single-route) or `SEND-TX-003-05` (cascade).
68
- * The threshold is for visibility, not truncation — pre-flight handles
69
- * drain protection. v2 may add a hard cap once telemetry justifies a
70
- * number.
53
+ * fire `SEND-TX-203-05` (R2 single-route), `SEND-TX-303-06` (R3
54
+ * single-route), or `SEND-TX-003-05` (cascade). The threshold is for
55
+ * visibility, not truncation — pre-flight handles drain protection.
56
+ * v2 may add a hard cap once telemetry justifies a number.
71
57
  */
72
58
  export declare const SVM_NATIVE_VALUE_WARN_THRESHOLD: bigint;
73
59
  /**
74
60
  * Fire the SVM warn-threshold hook + log line if the buffered quote
75
61
  * exceeds the threshold. Non-blocking; `nativeValueForGas` is not
76
- * truncated by the caller. The hook ID switches to the cascade 0xx
77
- * bucket (`003-05`) when `pathTag === 'CASCADE'` so cascade streams
78
- * stay self-consistent.
62
+ * truncated by the caller. The hook ID switches between buckets so
63
+ * each stream stays self-consistent within its convention (R2 → 203,
64
+ * R3 → 303, cascade → 003).
79
65
  */
80
66
  export declare function maybeFireSvmWarnThreshold(ctx: OrchestratorContext, bufferedQuote: bigint, gasToken: `0x${string}`, pathTag: PreflightPathTag): void;
@@ -26,41 +26,29 @@ exports.maybeFireSvmWarnThreshold = maybeFireSvmWarnThreshold;
26
26
  const context_1 = require("./context");
27
27
  const progress_hook_types_1 = require("../../progress-hook/progress-hook.types");
28
28
  const errors_1 = require("./errors");
29
- /**
30
- * Internal: compute the legacy clamp value (matches the pre-existing
31
- * branches in route-handlers.ts:643-659 / :968-980 / :1396-1412).
32
- * Returned only when `allowUnderfundedSwap=true`.
33
- */
34
- function legacyClamp(ueaBalance, requiredValue, gasReserve) {
35
- if (ueaBalance >= requiredValue + gasReserve)
36
- return requiredValue;
37
- if (ueaBalance > gasReserve)
38
- return ueaBalance - gasReserve;
39
- if (ueaBalance > BigInt(0))
40
- return ueaBalance < requiredValue ? ueaBalance : requiredValue;
41
- return BigInt(0);
42
- }
43
29
  /**
44
30
  * Pre-flight check. PRC-20 balance is checked before the native check —
45
31
  * a missing burn token fails before the gas-balance check, so the user
46
- * sees the actionable PRC-20 shortfall instead of a stale UPC error.
47
- *
48
- * Returns a result object instead of throwing for the legacy opt-out
49
- * path. The throw happens here (not in the caller) for the default
50
- * `allowUnderfundedSwap=false` case so all 6 paths share one throw site.
32
+ * sees the actionable PRC-20 shortfall instead of a stale native PC error.
51
33
  */
52
34
  function runPreflight(opts) {
53
- const { ctx, ueaAddress, ueaBalance, requiredValue, gasReserve, pathTag, burnToken, burnAmount, prc20Balance, segmentIndex, allowUnderfundedSwap, } = opts;
54
- // Cascade context emits 003-xx (cascade 0xx bucket); single routes (R2/R3)
55
- // emit 203-xx (R2 bucket). The two ID sets share an identical formatter
35
+ const { ctx, ueaAddress, ueaBalance, requiredValue, gasReserve, pathTag, burnToken, burnAmount, prc20Balance, segmentIndex, } = opts;
36
+ // Each route bucket gets its own preflight IDs so hook streams stay
37
+ // self-consistent within their convention (R2 → 203-xx, R3 → 303-xx,
38
+ // cascade → 003-xx). The three hook formatters share an identical
56
39
  // signature, so the runtime branching is just an ID swap at each fire site.
57
40
  const isCascade = pathTag === 'CASCADE';
41
+ const isR3 = pathTag === 'R3_EVM' || pathTag === 'R3_SVM';
58
42
  const HOOK_PRE_INFO = isCascade
59
43
  ? progress_hook_types_1.PROGRESS_HOOK.SEND_TX_003_03
60
- : progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_03;
44
+ : isR3
45
+ ? progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_04
46
+ : progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_03;
61
47
  const HOOK_PRE_FAIL = isCascade
62
48
  ? progress_hook_types_1.PROGRESS_HOOK.SEND_TX_003_04
63
- : progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_04;
49
+ : isR3
50
+ ? progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_05
51
+ : progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_04;
64
52
  // 1. PRC-20 burn-balance check (skipped when burnAmount = 0 — R3 paths
65
53
  // structurally hold zero of the source-chain native PRC-20; see plan §9 #4).
66
54
  if (burnAmount && burnAmount > BigInt(0) && burnToken) {
@@ -69,15 +57,6 @@ function runPreflight(opts) {
69
57
  (0, context_1.fireProgressHook)(ctx, HOOK_PRE_INFO, burnAmount, onHand, sufficient, ueaAddress, pathTag, { kind: 'PRC20', burnToken, segmentIndex });
70
58
  if (!sufficient) {
71
59
  const shortfall = burnAmount - onHand;
72
- if (allowUnderfundedSwap) {
73
- (0, context_1.printLog)(ctx, `[preflight ${pathTag}] PRC-20 shortfall ${shortfall} of ${burnToken}; ` +
74
- `allowUnderfundedSwap=true — proceeding with legacy clamp behaviour, ` +
75
- `swap is expected to revert with InsufficientBalance(0xf4d678b8) on-chain.`);
76
- return {
77
- ok: false,
78
- legacyClampedValue: legacyClamp(ueaBalance, requiredValue, gasReserve),
79
- };
80
- }
81
60
  (0, context_1.fireProgressHook)(ctx, HOOK_PRE_FAIL, burnAmount, onHand, shortfall, ueaAddress, pathTag, { kind: 'PRC20', burnToken, segmentIndex });
82
61
  throw new errors_1.InsufficientUEABalanceError({
83
62
  required: burnAmount,
@@ -99,13 +78,6 @@ function runPreflight(opts) {
99
78
  return { ok: true, adjustedValue: requiredValue };
100
79
  }
101
80
  const shortfall = totalRequired - ueaBalance;
102
- if (allowUnderfundedSwap) {
103
- const clamped = legacyClamp(ueaBalance, requiredValue, gasReserve);
104
- (0, context_1.printLog)(ctx, `[preflight ${pathTag}] UPC shortfall ${shortfall} wei; ` +
105
- `allowUnderfundedSwap=true — proceeding with legacy clamped value ${clamped}, ` +
106
- `swap may revert inside Uniswap with Error("STF") if the clamp under-funds the pool quote.`);
107
- return { ok: false, legacyClampedValue: clamped };
108
- }
109
81
  (0, context_1.fireProgressHook)(ctx, HOOK_PRE_FAIL, totalRequired, ueaBalance, shortfall, ueaAddress, pathTag, { kind: 'NATIVE', segmentIndex });
110
82
  throw new errors_1.InsufficientUEABalanceError({
111
83
  required: totalRequired,
@@ -119,25 +91,27 @@ function runPreflight(opts) {
119
91
  }
120
92
  /**
121
93
  * SVM-only telemetry threshold. Buffered pool quotes above this value
122
- * fire `SEND-TX-203-05` (single-route) or `SEND-TX-003-05` (cascade).
123
- * The threshold is for visibility, not truncation — pre-flight handles
124
- * drain protection. v2 may add a hard cap once telemetry justifies a
125
- * number.
94
+ * fire `SEND-TX-203-05` (R2 single-route), `SEND-TX-303-06` (R3
95
+ * single-route), or `SEND-TX-003-05` (cascade). The threshold is for
96
+ * visibility, not truncation — pre-flight handles drain protection.
97
+ * v2 may add a hard cap once telemetry justifies a number.
126
98
  */
127
99
  exports.SVM_NATIVE_VALUE_WARN_THRESHOLD = BigInt(5000) * BigInt(1e18);
128
100
  /**
129
101
  * Fire the SVM warn-threshold hook + log line if the buffered quote
130
102
  * exceeds the threshold. Non-blocking; `nativeValueForGas` is not
131
- * truncated by the caller. The hook ID switches to the cascade 0xx
132
- * bucket (`003-05`) when `pathTag === 'CASCADE'` so cascade streams
133
- * stay self-consistent.
103
+ * truncated by the caller. The hook ID switches between buckets so
104
+ * each stream stays self-consistent within its convention (R2 → 203,
105
+ * R3 → 303, cascade → 003).
134
106
  */
135
107
  function maybeFireSvmWarnThreshold(ctx, bufferedQuote, gasToken, pathTag) {
136
108
  if (bufferedQuote <= exports.SVM_NATIVE_VALUE_WARN_THRESHOLD)
137
109
  return;
138
110
  const HOOK_WARN = pathTag === 'CASCADE'
139
111
  ? progress_hook_types_1.PROGRESS_HOOK.SEND_TX_003_05
140
- : progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_05;
112
+ : pathTag === 'R3_EVM' || pathTag === 'R3_SVM'
113
+ ? progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_06
114
+ : progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_05;
141
115
  (0, context_1.fireProgressHook)(ctx, HOOK_WARN, bufferedQuote, exports.SVM_NATIVE_VALUE_WARN_THRESHOLD, gasToken, pathTag);
142
116
  (0, context_1.printLog)(ctx, `[preflight ${pathTag}] SVM warn-threshold tripped: bufferedQuote=${bufferedQuote} > ` +
143
117
  `${exports.SVM_NATIVE_VALUE_WARN_THRESHOLD} for gasToken=${gasToken} — pool may be skewed`);
@@ -536,7 +536,7 @@ function transformToUniversalTxResponse(ctx_1, tx_1) {
536
536
  accessList: Array.isArray(tx.accessList) ? [...tx.accessList] : [],
537
537
  // 6. Utilities
538
538
  wait: (waitOptions) => tslib_1.__awaiter(this, void 0, void 0, function* () {
539
- var _a, _b, _c, _d, _e, _f;
539
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
540
540
  // For trackTransaction replays the tx is historical — cosmos already
541
541
  // has the full graph indexed, so the 20s/30s initial-wait buffers
542
542
  // designed for live sends are pure deadweight. Default them to 0 when
@@ -605,9 +605,17 @@ function transformToUniversalTxResponse(ctx_1, tx_1) {
605
605
  timeout: outboundTimeoutMs,
606
606
  progressHook: outboundTranslator,
607
607
  });
608
- emit(hooks.success(outboundDetails));
609
- // Merge external chain details into receipt
610
- baseReceipt = Object.assign(Object.assign({}, baseReceipt), { externalTxHash: outboundDetails.externalTxHash, externalChain: outboundDetails.destinationChain, externalExplorerUrl: outboundDetails.explorerUrl, externalRecipient: outboundDetails.recipient, externalAmount: outboundDetails.amount, externalAssetAddr: outboundDetails.assetAddr, externalStatus: 'success' });
608
+ // `outboundDetails.externalTxHash` is the raw `0x`-hex form (kept
609
+ // internally so the R3 inbound tracker below can query Cosmos with
610
+ // it via `waitForInboundPushTx → findChildUtxIdFromExternalTx`).
611
+ // For every USER-FACING surface (progress hook event + receipt) we
612
+ // want the chain-native form so consumers can paste it straight
613
+ // into an explorer / RPC: base58 for SVM, hex for EVM. Build a
614
+ // display copy and use it for both the hook and the receipt.
615
+ const displayExternalTxHash = (_f = (0, helpers_1.toExternalTxHashDisplay)(outboundDetails.destinationChain, outboundDetails.externalTxHash)) !== null && _f !== void 0 ? _f : outboundDetails.externalTxHash;
616
+ const userFacingOutboundDetails = Object.assign(Object.assign({}, outboundDetails), { externalTxHash: displayExternalTxHash });
617
+ emit(hooks.success(userFacingOutboundDetails));
618
+ baseReceipt = Object.assign(Object.assign({}, baseReceipt), { externalTxHash: displayExternalTxHash, externalChain: outboundDetails.destinationChain, externalExplorerUrl: outboundDetails.explorerUrl, externalRecipient: outboundDetails.recipient, externalAmount: outboundDetails.amount, externalAssetAddr: outboundDetails.assetAddr, externalStatus: 'success' });
611
619
  // R3 round-trip: poll Push Chain for the inbound tx that the
612
620
  // source-chain CEA's sendUniversalTxToUEA call produces. Correlate
613
621
  // via the outbound external tx hash → child UTX id → child pcTx.
@@ -637,7 +645,7 @@ function transformToUniversalTxResponse(ctx_1, tx_1) {
637
645
  }
638
646
  else {
639
647
  // status === 'failed' (terminal failure status from chain)
640
- const failMsg = (_f = inbound.errorMessage) !== null && _f !== void 0 ? _f : 'inbound execution failed on Push Chain';
648
+ const failMsg = (_g = inbound.errorMessage) !== null && _g !== void 0 ? _g : 'inbound execution failed on Push Chain';
641
649
  emit(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_399_02](failMsg, 'inbound', targetChain));
642
650
  baseReceipt = Object.assign(Object.assign({}, baseReceipt), { pushInboundUtxId: inbound.childUtxId, pushInboundTxHash: inbound.pushTxHash || undefined, externalStatus: 'failed', externalError: failMsg });
643
651
  }
@@ -669,8 +677,16 @@ function transformToUniversalTxResponse(ctx_1, tx_1) {
669
677
  // observed tx hash is carried by OutboundFailedError — expose it
670
678
  // as externalTxHash so consumers can link to the explorer.
671
679
  const failedExternalTxHash = error instanceof outbound_sync_1.OutboundFailedError ? error.externalTxHash : undefined;
672
- baseReceipt = Object.assign(Object.assign(Object.assign({}, baseReceipt), { externalStatus: isTimeout ? 'timeout' : 'failed', externalError: errMsg }), (failedExternalTxHash
673
- ? { externalTxHash: failedExternalTxHash }
680
+ // Normalize to chain-native display form for SVM (base58); EVM hex
681
+ // passes through unchanged. `OutboundFailedError.destinationChain`
682
+ // is a chain namespace string (e.g., 'solana:devnet') — convert to
683
+ // the CHAIN enum via the same helper used in outbound-sync.
684
+ const failedChain = error instanceof outbound_sync_1.OutboundFailedError && error.destinationChain
685
+ ? (_h = (0, helpers_1.chainFromNamespace)(error.destinationChain)) !== null && _h !== void 0 ? _h : undefined
686
+ : undefined;
687
+ const failedExternalTxHashDisplay = (_j = (0, helpers_1.toExternalTxHashDisplay)(failedChain, failedExternalTxHash)) !== null && _j !== void 0 ? _j : failedExternalTxHash;
688
+ baseReceipt = Object.assign(Object.assign(Object.assign({}, baseReceipt), { externalStatus: isTimeout ? 'timeout' : 'failed', externalError: errMsg }), (failedExternalTxHashDisplay
689
+ ? { externalTxHash: failedExternalTxHashDisplay }
674
690
  : {}));
675
691
  // Outbound polling timed out or failed — return the annotated
676
692
  // receipt without throwing. Push Chain tx succeeded, external
@@ -281,7 +281,7 @@ function executeMultiChain(ctx, params, executeFn) {
281
281
  */
282
282
  function executeUoaToCea(ctx, params, executeFn) {
283
283
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
284
- var _a, _b, _c, _d;
284
+ var _a, _b, _c;
285
285
  const target = params.to;
286
286
  const targetChain = target.chain;
287
287
  const targetAddress = target.address;
@@ -430,12 +430,16 @@ function executeUoaToCea(ctx, params, executeFn) {
430
430
  }
431
431
  }
432
432
  }
433
- // Pool-quote + 10% safety buffer. Over-send is refunded by the contract.
433
+ // estimateNativeValueForSwap applies the full 2.2x safety buffer
434
+ // internally (pool-slippage cushion + the 10% headroom that used to be
435
+ // multiplied here). Folded into one place so the function's cap branch
436
+ // bounds the final value — the previous outer ×110/100 on top of the
437
+ // inner cap broke preflight whenever the cap fired. Over-send is refunded
438
+ // by the contract.
434
439
  let nativeValueForGas = BigInt(0);
435
440
  if (universalCoreAddress && gasFee > BigInt(0)) {
436
- const estimated = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, effectiveBalance);
437
- nativeValueForGas = (estimated * BigInt(110)) / BigInt(100);
438
- (0, context_1.printLog)(ctx, `executeUoaToCea — nativeValueForGas: pool-quote=${estimated.toString()}, with 10% buffer=${nativeValueForGas.toString()}`);
441
+ nativeValueForGas = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, effectiveBalance);
442
+ (0, context_1.printLog)(ctx, `executeUoaToCea — nativeValueForGas=${nativeValueForGas.toString()} (includes 2.2x buffer)`);
439
443
  }
440
444
  // Hard safety cap so a broken pool price can't drain the UEA.
441
445
  if (nativeValueForGas > EVM_NATIVE_VALUE_SAFETY_CAP) {
@@ -457,8 +461,7 @@ function executeUoaToCea(ctx, params, executeFn) {
457
461
  args: [ueaAddress],
458
462
  });
459
463
  }
460
- const allowUnderfundedR2Evm = ((_d = params.options) === null || _d === void 0 ? void 0 : _d.allowUnderfundedSwap) === true;
461
- const preflightR2Evm = (0, preflight_1.runPreflight)({
464
+ (0, preflight_1.runPreflight)({
462
465
  ctx,
463
466
  ueaAddress,
464
467
  ueaBalance: effectiveBalance,
@@ -468,13 +471,8 @@ function executeUoaToCea(ctx, params, executeFn) {
468
471
  burnToken: prc20Token,
469
472
  burnAmount,
470
473
  prc20Balance: prc20BalanceR2Evm,
471
- allowUnderfundedSwap: allowUnderfundedR2Evm,
472
474
  });
473
- if (!preflightR2Evm.ok) {
474
- nativeValueForGas = preflightR2Evm.legacyClampedValue;
475
- (0, context_1.printLog)(ctx, `executeUoaToCea — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
476
- }
477
- // ok=true → nativeValueForGas already covers required + reserve; no adjust needed.
475
+ // Preflight success means nativeValueForGas already covers required + reserve; no adjust needed.
478
476
  // Build outbound multicalls (approve burn + sendUniversalTxOutbound with native value)
479
477
  const outboundMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
480
478
  prc20Token,
@@ -536,7 +534,7 @@ function executeUoaToCea(ctx, params, executeFn) {
536
534
  */
537
535
  function executeUoaToCeaSvm(ctx, params, target, executeFn) {
538
536
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
539
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
537
+ var _a, _b, _c, _d, _e, _f, _g, _h;
540
538
  const targetChain = target.chain;
541
539
  const targetAddress = target.address; // 0x-prefixed, 32 bytes
542
540
  const ueaAddress = (0, uea_manager_1.computeUEAOffchain)(ctx);
@@ -648,8 +646,10 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
648
646
  }
649
647
  const ueaNonce = (!isUEADeployed || isNativePushEOASvm) ? BigInt(0) : yield (0, uea_manager_1.getUEANonce)(ctx, ueaAddress);
650
648
  // R2 does NOT use Case A/B/C gas abstraction (scoped to R1 + R3 only).
651
- // Size msg.value from the live WPC/gasToken Uniswap V3 pool quote plus
652
- // a 10% safety buffer. swapAndBurnGas refunds the excess as PC via
649
+ // Size msg.value from the live WPC/gasToken Uniswap V3 pool quote. The
650
+ // 2.2x safety buffer is applied inside estimateNativeValueForSwap (was
651
+ // split as inner 2x + outer 10% — combined to keep the cap branch
652
+ // mathematically correct). swapAndBurnGas refunds the excess as PC via
653
653
  // refundUnusedGas.
654
654
  //
655
655
  // SVM pools (WPC/pSOL) are thinner than EVM pools so the pool-quote can
@@ -664,9 +664,10 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
664
664
  const SVM_GAS_RESERVE = BigInt(3e18); // 3 UPC for outer-tx gas
665
665
  let nativeValueForGas = BigInt(0);
666
666
  if (universalCoreAddress && gasFee > BigInt(0)) {
667
- const estimated = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, ueaBalance);
668
- nativeValueForGas = (estimated * BigInt(110)) / BigInt(100);
669
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — nativeValueForGas: pool-quote=${estimated.toString()}, with 10% buffer=${nativeValueForGas.toString()}`);
667
+ // 2.2x safety buffer is applied inside estimateNativeValueForSwap so
668
+ // the function's balance-cap branch bounds the final value correctly.
669
+ nativeValueForGas = yield (0, gas_calculator_1.estimateNativeValueForSwap)(ctx, universalCoreAddress, gasToken, gasFee, ueaBalance);
670
+ (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — nativeValueForGas=${nativeValueForGas.toString()} (includes 2.2x buffer)`);
670
671
  (0, preflight_1.maybeFireSvmWarnThreshold)(ctx, nativeValueForGas, gasToken, 'R2_SVM');
671
672
  }
672
673
  // Pre-flight: throws InsufficientUEABalanceError when UEA cannot cover
@@ -688,8 +689,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
688
689
  args: [ueaAddress],
689
690
  });
690
691
  }
691
- const allowUnderfundedR2Svm = ((_j = params.options) === null || _j === void 0 ? void 0 : _j.allowUnderfundedSwap) === true;
692
- const preflightR2Svm = (0, preflight_1.runPreflight)({
692
+ (0, preflight_1.runPreflight)({
693
693
  ctx,
694
694
  ueaAddress,
695
695
  ueaBalance,
@@ -699,12 +699,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
699
699
  burnToken: prc20Token,
700
700
  burnAmount,
701
701
  prc20Balance: prc20BalanceR2Svm,
702
- allowUnderfundedSwap: allowUnderfundedR2Svm,
703
702
  });
704
- if (!preflightR2Svm.ok) {
705
- nativeValueForGas = preflightR2Svm.legacyClampedValue;
706
- (0, context_1.printLog)(ctx, `executeUoaToCeaSvm — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
707
- }
708
703
  // --- Build Push Chain multicalls (approve + sendUniversalTxOutbound) ---
709
704
  // Reuse the same builder as EVM — this part is identical
710
705
  const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
@@ -769,7 +764,7 @@ function executeUoaToCeaSvm(ctx, params, target, executeFn) {
769
764
  */
770
765
  function executeCeaToPush(ctx, params, executeFn) {
771
766
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
772
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
767
+ var _a, _b, _c, _d, _e, _f, _g, _h;
773
768
  // 1. Validate and extract source chain
774
769
  if (!((_a = params.from) === null || _a === void 0 ? void 0 : _a.chain)) {
775
770
  throw new Error('Route 3 (CEA -> Push) requires from.chain to specify the source CEA chain');
@@ -903,8 +898,8 @@ function executeCeaToPush(ctx, params, executeFn) {
903
898
  // so the inbound will arrive when the UEA nonce is already incremented.
904
899
  pushPayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: ueaNonce + BigInt(1) });
905
900
  }
906
- // Build sendUniversalTxToUEA self-call on CEA
907
- // CEA multicall: to=CEA (self-call), value=0
901
+ // Build sendUniversalTxToUEA self-call on CEA. CEA self-calls must always
902
+ // use value=0; native sends spend from the CEA's existing balance.
908
903
  // CEA internally calls gateway.sendUniversalTxFromCEA(...)
909
904
  // Uses bridgeAmount (= burn amount deposited by Vault)
910
905
  const sendUniversalTxCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, // to: CEA address (self-call)
@@ -929,13 +924,14 @@ function executeCeaToPush(ctx, params, executeFn) {
929
924
  // tokens FROM the CEA back to Push Chain, not to round-trip them.
930
925
  const prc20Token = (0, helpers_1.getNativePRC20ForChain)(sourceChain, ctx.pushNetwork);
931
926
  const burnAmount = BigInt(0);
927
+ const outboundGasLimit = (_f = params.gasLimit) !== null && _f !== void 0 ? _f : selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
932
928
  (0, context_1.printLog)(ctx, `executeCeaToPush — prc20Token: ${prc20Token}, burnAmount: ${burnAmount.toString()}`);
933
929
  // 9. Build outbound request (same structure as Route 2)
934
930
  // target = CEA address (for self-execution), value = 0 in payload
935
931
  const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, // target: CEA address (to=CEA for self-execution)
936
932
  prc20Token, // token: native PRC-20 for source chain (for namespace lookup)
937
933
  burnAmount, // amount: 0 (payload-only, CEA uses its own balance)
938
- (_f = params.gasLimit) !== null && _f !== void 0 ? _f : BigInt(0), ceaPayload, // payload: ABI-encoded CEA multicall
934
+ outboundGasLimit, ceaPayload, // payload: ABI-encoded CEA multicall
939
935
  ueaAddress // revertRecipient: UEA
940
936
  );
941
937
  (0, context_1.printLog)(ctx, `executeCeaToPush — outbound request: ${JSON.stringify({
@@ -957,10 +953,9 @@ function executeCeaToPush(ctx, params, executeFn) {
957
953
  let gasToken = selectors_1.ZERO_ADDRESS;
958
954
  let sizingDecisionR3;
959
955
  if (prc20Token !== selectors_1.ZERO_ADDRESS) {
960
- const gasLimit = outboundReq.gasLimit;
961
956
  (0, context_1.fireProgressHook)(ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01, sourceChain);
962
957
  try {
963
- const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimit, sourceChain);
958
+ const result = yield (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, outboundGasLimit, sourceChain);
964
959
  gasToken = result.gasToken;
965
960
  gasFee = result.gasFee;
966
961
  protocolFeeR3 = result.protocolFee;
@@ -1040,20 +1035,14 @@ function executeCeaToPush(ctx, params, executeFn) {
1040
1035
  // Pre-flight (R3 EVM): no PRC-20 burn check — `burnAmount = 0` makes this
1041
1036
  // payload-only (UEA never holds the source-chain PRC-20; see plan §9 #4).
1042
1037
  // Native UPC check still applies for the gas swap.
1043
- const allowUnderfundedR3Evm = ((_j = params.options) === null || _j === void 0 ? void 0 : _j.allowUnderfundedSwap) === true;
1044
- const preflightR3Evm = (0, preflight_1.runPreflight)({
1038
+ (0, preflight_1.runPreflight)({
1045
1039
  ctx,
1046
1040
  ueaAddress,
1047
1041
  ueaBalance: effectiveBalance,
1048
1042
  requiredValue: nativeValueForGas,
1049
1043
  gasReserve: OUTBOUND_GAS_RESERVE_R3,
1050
1044
  pathTag: 'R3_EVM',
1051
- allowUnderfundedSwap: allowUnderfundedR3Evm,
1052
1045
  });
1053
- if (!preflightR3Evm.ok) {
1054
- nativeValueForGas = preflightR3Evm.legacyClampedValue;
1055
- (0, context_1.printLog)(ctx, `executeCeaToPush — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
1056
- }
1057
1046
  // 12. Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1058
1047
  const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
1059
1048
  prc20Token,
@@ -1118,7 +1107,7 @@ function executeCeaToPush(ctx, params, executeFn) {
1118
1107
  */
1119
1108
  function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1120
1109
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
1121
- var _a, _b, _c, _d, _e;
1110
+ var _a, _b, _c, _d;
1122
1111
  if (typeof params.to !== 'string') {
1123
1112
  throw new Error('Route 3 SVM (CEA -> Push): to must be a Push Chain address (string), not a ChainTarget');
1124
1113
  }
@@ -1266,20 +1255,14 @@ function executeCeaToPushSvm(ctx, params, sourceChain, executeFn) {
1266
1255
  // Pre-flight (R3 SVM): no PRC-20 burn check — `burnAmount = 1` is a legacy
1267
1256
  // precompile-validation shim against a token UEAs structurally don't hold
1268
1257
  // (see plan §9 #4 / §10.2 latent-bug ticket). Native UPC check still applies.
1269
- const allowUnderfundedR3Svm = ((_e = params.options) === null || _e === void 0 ? void 0 : _e.allowUnderfundedSwap) === true;
1270
- const preflightR3Svm = (0, preflight_1.runPreflight)({
1258
+ (0, preflight_1.runPreflight)({
1271
1259
  ctx,
1272
1260
  ueaAddress,
1273
1261
  ueaBalance: currentBalance,
1274
1262
  requiredValue: nativeValueForGas,
1275
1263
  gasReserve: OUTBOUND_GAS_RESERVE_R3_SVM,
1276
1264
  pathTag: 'R3_SVM',
1277
- allowUnderfundedSwap: allowUnderfundedR3Svm,
1278
1265
  });
1279
- if (!preflightR3Svm.ok) {
1280
- nativeValueForGas = preflightR3Svm.legacyClampedValue;
1281
- (0, context_1.printLog)(ctx, `executeCeaToPushSvm — allowUnderfundedSwap=true; using legacy clamped value ${nativeValueForGas.toString()}`);
1282
- }
1283
1266
  // Build Push Chain multicalls (approvals + sendUniversalTxOutbound)
1284
1267
  const pushChainMulticalls = (0, payload_builders_1.buildOutboundApprovalAndCall)({
1285
1268
  prc20Token,
@@ -1559,7 +1542,8 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1559
1542
  const multicallPayload = (0, payload_builder_1.buildMulticallPayloadData)(ctx, pushDestination, multicallData);
1560
1543
  pushPayload = (0, payload_builders_1.buildInboundUniversalPayload)(multicallPayload, { nonce: ueaNonceHop + BigInt(1) });
1561
1544
  }
1562
- // Build sendUniversalTxToUEA self-call on CEA (to=CEA, value=0)
1545
+ // Build sendUniversalTxToUEA self-call on CEA. CEA self-calls must
1546
+ // always use value=0; native sends spend from the CEA balance.
1563
1547
  const sendCall = (0, payload_builders_1.buildSendUniversalTxToUEA)(ceaAddress, tokenAddress, amount, pushPayload, ceaAddress);
1564
1548
  ceaMulticalls.push(sendCall);
1565
1549
  const ceaPayload = (0, payload_builders_1.buildCeaMulticallPayload)(ceaMulticalls);
@@ -1567,7 +1551,8 @@ function buildPayloadForRoute(ctx, params, route, nonce) {
1567
1551
  // Route 3: CEA uses its own pre-existing balance — no PRC-20 burn needed.
1568
1552
  // burnAmount = 0 makes this a payload-only outbound relay.
1569
1553
  const burnAmount = BigInt(0);
1570
- const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, (_l = params.gasLimit) !== null && _l !== void 0 ? _l : BigInt(0), ceaPayload, ueaAddress);
1554
+ const outboundGasLimit = (_l = params.gasLimit) !== null && _l !== void 0 ? _l : selectors_1.DEFAULT_CEA_TO_PUSH_GAS_LIMIT;
1555
+ const outboundReq = (0, payload_builders_1.buildOutboundRequest)(ceaAddress, prc20Token, burnAmount, outboundGasLimit, ceaPayload, ueaAddress);
1571
1556
  return { payload: ceaPayload, gatewayRequest: outboundReq };
1572
1557
  }
1573
1558
  default:
@@ -176,7 +176,8 @@ function reconstructR3(universalTxResponse, universalTxData, isFailed, errorMsg)
176
176
  events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_02](universalTxResponse.from, ceaAddress, sourceChain));
177
177
  // R3 pre-flight reconstruction — R3 EVM burnAmount=0, R3 SVM burnAmount=0
178
178
  // (post-fix). Only the NATIVE pre-flight check fires; no PRC-20 second hook.
179
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_03](BigInt(0), BigInt(0), true, universalTxResponse.from, 'R3_EVM', { kind: 'NATIVE' }));
179
+ // R3 single-route uses 303-04 (R3 bucket); see runPreflight in preflight.ts.
180
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_04](BigInt(0), BigInt(0), true, universalTxResponse.from, 'R3_EVM', { kind: 'NATIVE' }));
180
181
  events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_01]());
181
182
  events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_02]());
182
183
  events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_03]());
@@ -85,9 +85,15 @@ export declare class Orchestrator {
85
85
  * The cascade composes all hops bottom-to-top into a single Push Chain tx.
86
86
  *
87
87
  * @param preparedTxs - Array of prepared transactions
88
+ * @param options - Optional per-call options. `progressHook` is additive with
89
+ * the init-time hook from `PushChain.initialize` and receives every
90
+ * `ProgressEvent` emitted during this cascade (pre-flight, broadcast, and
91
+ * tracking). Dedups by reference if it matches the init-time hook.
88
92
  * @returns Object with send() method
89
93
  */
90
- createCascadedBuilder(preparedTxs: PreparedUniversalTx[]): {
94
+ createCascadedBuilder(preparedTxs: PreparedUniversalTx[], options?: {
95
+ progressHook?: (event: ProgressEvent) => void;
96
+ }): {
91
97
  send: () => Promise<CascadedTxResponse>;
92
98
  };
93
99
  /**
@@ -30,6 +30,7 @@ class Orchestrator {
30
30
  return {
31
31
  executeFn: this.execute.bind(this),
32
32
  waitForOutboundTxFn: (hash, opts) => (0, internals_1.waitForOutboundTx)(this.ctx, hash, opts),
33
+ waitForInboundPushTxFn: (hash, sourceChain, opts) => (0, internals_1.waitForInboundPushTx)(this.ctx, hash, sourceChain, opts),
33
34
  waitForAllOutboundTxsFn: (hash, hops, opts) => (0, internals_1.waitForAllOutboundTxsV2)(this.ctx, hash, hops, opts),
34
35
  };
35
36
  }
@@ -296,10 +297,47 @@ class Orchestrator {
296
297
  * The cascade composes all hops bottom-to-top into a single Push Chain tx.
297
298
  *
298
299
  * @param preparedTxs - Array of prepared transactions
300
+ * @param options - Optional per-call options. `progressHook` is additive with
301
+ * the init-time hook from `PushChain.initialize` and receives every
302
+ * `ProgressEvent` emitted during this cascade (pre-flight, broadcast, and
303
+ * tracking). Dedups by reference if it matches the init-time hook.
299
304
  * @returns Object with send() method
300
305
  */
301
- createCascadedBuilder(preparedTxs) {
302
- return (0, internals_1.createCascadedBuilder)(this.ctx, preparedTxs, this._getCascadeCallbacks());
306
+ createCascadedBuilder(preparedTxs, options) {
307
+ const perCallHook = options === null || options === void 0 ? void 0 : options.progressHook;
308
+ if (!perCallHook) {
309
+ return (0, internals_1.createCascadedBuilder)(this.ctx, preparedTxs, this._getCascadeCallbacks());
310
+ }
311
+ // Wrap ctx.progressHook for the synchronous send() phase — captures every
312
+ // fireProgressHook(ctx, ...) call during pre-flight, composeCascade, and
313
+ // the Push Chain broadcast. Restored when send() resolves/rejects so the
314
+ // wrapper doesn't leak across calls.
315
+ //
316
+ // For the asynchronous tracking phase (waitForAll), we pass perCallHook as
317
+ // the cascade builder's `defaultEventHook` — waitForAll falls back to it
318
+ // when callers don't pass their own `eventHook`, and the existing
319
+ // `dispatchCascadeProgressEvent` path delivers events to BOTH that hook
320
+ // and ctx.progressHook (the init-time hook, restored by then) with
321
+ // reference-dedup.
322
+ const innerBuilder = (0, internals_1.createCascadedBuilder)(this.ctx, preparedTxs, this._getCascadeCallbacks(), perCallHook);
323
+ return {
324
+ send: () => tslib_1.__awaiter(this, void 0, void 0, function* () {
325
+ const originalHook = this.progressHook;
326
+ const wrapped = (event) => {
327
+ if (originalHook && originalHook !== perCallHook) {
328
+ originalHook(event);
329
+ }
330
+ perCallHook(event);
331
+ };
332
+ this.progressHook = wrapped;
333
+ try {
334
+ return yield innerBuilder.send();
335
+ }
336
+ finally {
337
+ this.progressHook = originalHook;
338
+ }
339
+ }),
340
+ };
303
341
  }
304
342
  /**
305
343
  * Queries gas fee for an outbound transaction from the UniversalCore contract.
@@ -397,20 +397,6 @@ export type UniversalExecuteParams = Omit<ExecuteParams, 'to'> & {
397
397
  * Only applicable for Route 2 (UOA_TO_CEA) on EVM chains.
398
398
  */
399
399
  migration?: boolean;
400
- /**
401
- * Per-call SDK behaviour overrides. Reserved for opt-out flags that must
402
- * not affect the global orchestrator config.
403
- */
404
- options?: {
405
- /**
406
- * Restore the legacy clamp-and-refund behaviour for outbound paths whose
407
- * UEA balance is below the buffered pool quote. Default `false` — the
408
- * SDK throws `InsufficientUEABalanceError` (see `runPreflight`) and does
409
- * NOT submit a cosmos tx. Set `true` only when the caller intentionally
410
- * relies on `swapAndBurnGas` refunds to power through low balances.
411
- */
412
- allowUnderfundedSwap?: boolean;
413
- };
414
400
  };
415
401
  /**
416
402
  * Account metadata for SVM CPI execution (Solana Gateway pattern).
@@ -514,6 +500,10 @@ export interface HopDescriptor {
514
500
  svmPayload?: `0x${string}`;
515
501
  /** Whether this hop is a CEA migration (raw MIGRATION_SELECTOR payload) */
516
502
  isMigration?: boolean;
503
+ /** Whether this Route 3 hop only seeds native PC into the UEA. @internal */
504
+ nativeSeedOnly?: boolean;
505
+ /** Native PC amount seeded by a value-only Route 3 hop. @internal */
506
+ nativeSeedAmount?: bigint;
517
507
  /** SDK 5.2 gas-abstraction sizing decision for this hop. @internal */
518
508
  sizing?: import('./internals/gas-usd-sizer').GasSizingDecision;
519
509
  }