@pushchain/core 5.1.5-alpha.0 → 5.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +1 -1
  3. package/src/lib/constants/abi/index.d.ts +2 -0
  4. package/src/lib/constants/abi/index.js +6 -1
  5. package/src/lib/constants/abi/uniswap-v3-router.evm.d.ts +58 -0
  6. package/src/lib/constants/abi/uniswap-v3-router.evm.js +41 -0
  7. package/src/lib/constants/abi/wpc.evm.d.ts +24 -0
  8. package/src/lib/constants/abi/wpc.evm.js +27 -0
  9. package/src/lib/constants/chain.d.ts +1 -0
  10. package/src/lib/constants/chain.js +8 -6
  11. package/src/lib/constants/tokens.js +3 -3
  12. package/src/lib/generated/uexecutor/v2/types.d.ts +14 -0
  13. package/src/lib/generated/uexecutor/v2/types.js +96 -3
  14. package/src/lib/orchestrator/internals/cascade.d.ts +1 -1
  15. package/src/lib/orchestrator/internals/cascade.js +556 -225
  16. package/src/lib/orchestrator/internals/confirmation.js +60 -15
  17. package/src/lib/orchestrator/internals/context.d.ts +23 -0
  18. package/src/lib/orchestrator/internals/context.js +54 -0
  19. package/src/lib/orchestrator/internals/execute-funds-only.js +47 -21
  20. package/src/lib/orchestrator/internals/execute-funds-payload.js +53 -31
  21. package/src/lib/orchestrator/internals/execute-standard.js +137 -26
  22. package/src/lib/orchestrator/internals/gas-calculator.d.ts +5 -2
  23. package/src/lib/orchestrator/internals/gas-calculator.js +59 -11
  24. package/src/lib/orchestrator/internals/gas-usd-sizer.d.ts +53 -0
  25. package/src/lib/orchestrator/internals/gas-usd-sizer.js +165 -0
  26. package/src/lib/orchestrator/internals/helpers.js +2 -0
  27. package/src/lib/orchestrator/internals/inbound-tracker.d.ts +83 -0
  28. package/src/lib/orchestrator/internals/inbound-tracker.js +322 -0
  29. package/src/lib/orchestrator/internals/index.d.ts +3 -1
  30. package/src/lib/orchestrator/internals/index.js +8 -2
  31. package/src/lib/orchestrator/internals/outbound-sync.d.ts +32 -1
  32. package/src/lib/orchestrator/internals/outbound-sync.js +154 -17
  33. package/src/lib/orchestrator/internals/pc-usd-oracle.d.ts +28 -0
  34. package/src/lib/orchestrator/internals/pc-usd-oracle.js +285 -0
  35. package/src/lib/orchestrator/internals/progress-route-hooks.d.ts +23 -0
  36. package/src/lib/orchestrator/internals/progress-route-hooks.js +72 -0
  37. package/src/lib/orchestrator/internals/push-chain-tx.d.ts +27 -1
  38. package/src/lib/orchestrator/internals/push-chain-tx.js +111 -15
  39. package/src/lib/orchestrator/internals/rescue.js +1 -1
  40. package/src/lib/orchestrator/internals/response-builder.d.ts +5 -0
  41. package/src/lib/orchestrator/internals/response-builder.js +252 -53
  42. package/src/lib/orchestrator/internals/route-handlers.js +480 -206
  43. package/src/lib/orchestrator/internals/tx-transformer.js +293 -35
  44. package/src/lib/orchestrator/orchestrator.d.ts +7 -1
  45. package/src/lib/orchestrator/orchestrator.js +108 -56
  46. package/src/lib/orchestrator/orchestrator.types.d.ts +120 -5
  47. package/src/lib/orchestrator/payload-builders.d.ts +16 -0
  48. package/src/lib/orchestrator/payload-builders.js +6 -5
  49. package/src/lib/orchestrator/route-detector.js +20 -8
  50. package/src/lib/orchestrator/svm-idl/normalize-address.d.ts +19 -0
  51. package/src/lib/orchestrator/svm-idl/normalize-address.js +56 -0
  52. package/src/lib/orchestrator/svm-idl/registry.d.ts +13 -4
  53. package/src/lib/orchestrator/svm-idl/registry.js +15 -14
  54. package/src/lib/orchestrator/svm-idl/resolve.js +5 -2
  55. package/src/lib/progress-hook/progress-hook.d.ts +28 -0
  56. package/src/lib/progress-hook/progress-hook.js +555 -101
  57. package/src/lib/progress-hook/progress-hook.types.d.ts +76 -24
  58. package/src/lib/progress-hook/progress-hook.types.js +74 -29
  59. package/src/lib/push-client/push-client.d.ts +7 -0
  60. package/src/lib/push-client/push-client.js +18 -1
  61. package/src/lib/universal-tx-detector/__tests__/fixtures/builders.d.ts +32 -0
  62. package/src/lib/universal-tx-detector/__tests__/fixtures/builders.js +65 -0
  63. package/src/lib/universal-tx-detector/__tests__/fixtures/scenarios.d.ts +72 -0
  64. package/src/lib/universal-tx-detector/__tests__/fixtures/scenarios.js +669 -0
  65. package/src/lib/universal-tx-detector/auto-detect.d.ts +37 -0
  66. package/src/lib/universal-tx-detector/auto-detect.js +112 -0
  67. package/src/lib/universal-tx-detector/cascade.d.ts +64 -0
  68. package/src/lib/universal-tx-detector/cascade.js +254 -0
  69. package/src/lib/universal-tx-detector/child-inbounds.d.ts +58 -0
  70. package/src/lib/universal-tx-detector/child-inbounds.js +145 -0
  71. package/src/lib/universal-tx-detector/classify.d.ts +33 -0
  72. package/src/lib/universal-tx-detector/classify.js +228 -0
  73. package/src/lib/universal-tx-detector/detector-svm.d.ts +46 -0
  74. package/src/lib/universal-tx-detector/detector-svm.js +324 -0
  75. package/src/lib/universal-tx-detector/detector.d.ts +13 -0
  76. package/src/lib/universal-tx-detector/detector.js +233 -0
  77. package/src/lib/universal-tx-detector/events.d.ts +765 -0
  78. package/src/lib/universal-tx-detector/events.js +241 -0
  79. package/src/lib/universal-tx-detector/index.d.ts +18 -0
  80. package/src/lib/universal-tx-detector/index.js +36 -0
  81. package/src/lib/universal-tx-detector/svm-events.d.ts +41 -0
  82. package/src/lib/universal-tx-detector/svm-events.js +68 -0
  83. package/src/lib/universal-tx-detector/types.d.ts +100 -0
  84. package/src/lib/universal-tx-detector/types.js +3 -0
  85. package/src/lib/utils.d.ts +30 -13
  86. package/src/lib/utils.js +47 -21
@@ -6,10 +6,11 @@
6
6
  * Extracted from Orchestrator.waitForOutboundTx / waitForAllOutboundTxsV2.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.OUTBOUND_MAX_TIMEOUT_MS = exports.OUTBOUND_POLL_INTERVAL_MS = exports.OUTBOUND_INITIAL_WAIT_MS = void 0;
9
+ exports.OutboundFailedError = exports.OutboundTimeoutError = exports.INBOUND_INITIAL_WAIT_MS = exports.INBOUND_MAX_TIMEOUT_MS = exports.OUTBOUND_MAX_TIMEOUT_MS = exports.OUTBOUND_POLL_INTERVAL_MS = exports.OUTBOUND_INITIAL_WAIT_MS = void 0;
10
10
  exports.waitForOutboundTx = waitForOutboundTx;
11
11
  exports.waitForAllOutboundTxsV2 = waitForAllOutboundTxsV2;
12
12
  const tslib_1 = require("tslib");
13
+ const viem_1 = require("viem");
13
14
  const bs58_1 = require("../../internal/bs58");
14
15
  const chain_1 = require("../../constants/chain");
15
16
  const enums_1 = require("../../constants/enums");
@@ -25,11 +26,107 @@ const outbound_tracker_1 = require("./outbound-tracker");
25
26
  exports.OUTBOUND_INITIAL_WAIT_MS = 20000; // 20s
26
27
  /** Interval between consecutive polls (ms). */
27
28
  exports.OUTBOUND_POLL_INTERVAL_MS = 5000; // 5s
28
- /** Maximum total timeout for the polling loop (ms). */
29
+ /** Maximum total timeout for the outbound polling loop (ms). */
29
30
  exports.OUTBOUND_MAX_TIMEOUT_MS = 180000; // 180s (3 min)
31
+ /** Maximum total timeout for the R3 inbound round-trip polling loop (ms). */
32
+ exports.INBOUND_MAX_TIMEOUT_MS = 300000; // 300s (5 min) — covers R3 inbound latency on testnet
33
+ /** Initial wait before the first inbound poll (ms). Set to 0 — the outbound just completed, so the indexer may already have the child UTX. */
34
+ exports.INBOUND_INITIAL_WAIT_MS = 0;
35
+ // ============================================================================
36
+ // Typed errors
37
+ // ============================================================================
38
+ /**
39
+ * Thrown by `waitForOutboundTx` when the polling loop exceeds the configured
40
+ * timeout without observing an external tx hash. Callers can `instanceof`
41
+ * check to distinguish between timeout (→ 299-03) and terminal failure
42
+ * (→ 299-02) without resorting to error-message prefix matching.
43
+ */
44
+ class OutboundTimeoutError extends Error {
45
+ constructor(pushChainTxHash, elapsedMs, timeoutMs) {
46
+ super(`Timeout waiting for outbound transaction. Push Chain TX: ${pushChainTxHash}. Timeout: ${timeoutMs}ms. The relay may still be processing.`);
47
+ this.code = 'OUTBOUND_TIMEOUT';
48
+ this.name = 'OutboundTimeoutError';
49
+ this.pushChainTxHash = pushChainTxHash;
50
+ this.elapsedMs = elapsedMs;
51
+ }
52
+ }
53
+ exports.OutboundTimeoutError = OutboundTimeoutError;
54
+ /**
55
+ * Thrown by `waitForOutboundTx` when the universal tx reaches a terminal
56
+ * failure state or an outbound leg reports REVERTED status. Distinguishes
57
+ * a *failure* (→ 299-02) from a *timeout* (→ 299-03).
58
+ */
59
+ class OutboundFailedError extends Error {
60
+ constructor(message, pushChainTxHash, destinationChain, externalTxHash) {
61
+ super(message);
62
+ this.code = 'OUTBOUND_FAILED';
63
+ this.name = 'OutboundFailedError';
64
+ this.pushChainTxHash = pushChainTxHash;
65
+ this.destinationChain = destinationChain;
66
+ this.externalTxHash = externalTxHash;
67
+ }
68
+ }
69
+ exports.OutboundFailedError = OutboundFailedError;
30
70
  // ============================================================================
31
71
  // waitForOutboundTx
32
72
  // ============================================================================
73
+ /**
74
+ * Source-chain RPC tiebreaker.
75
+ *
76
+ * Cosmos sometimes populates `observedTx.txHash` while the relayer is still
77
+ * voting on the outboundStatus update. The Push indexer can stay at
78
+ * UNSPECIFIED for the full 180s budget even though the destination tx mined
79
+ * minutes earlier. To avoid those false timeouts we ask the destination
80
+ * chain's own RPC: if it has a confirmed receipt with status=success, we
81
+ * trust it and return FOUND; if status=reverted, we throw OutboundFailedError
82
+ * (preserves the silent-success bug fix); if no receipt yet or RPC failure,
83
+ * we keep polling.
84
+ *
85
+ * EVM-only — SVM tiebreaker would need a separate signature lookup path.
86
+ */
87
+ function verifyExternalEvmReceipt(ctx, destinationChain, externalTxHash) {
88
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
89
+ var _a;
90
+ const chainInfo = chain_1.CHAIN_INFO[destinationChain];
91
+ if (!chainInfo || chainInfo.vm !== enums_1.VM.EVM) {
92
+ return 'unsupported';
93
+ }
94
+ // Guard against unit-test contexts that don't pass a real rpcUrls map —
95
+ // we'd otherwise hit live testnet RPC inside Jest.
96
+ if (!ctx.rpcUrls)
97
+ return 'unsupported';
98
+ const rpcUrls = ((_a = ctx.rpcUrls) === null || _a === void 0 ? void 0 : _a[destinationChain]) && ctx.rpcUrls[destinationChain].length > 0
99
+ ? ctx.rpcUrls[destinationChain]
100
+ : chainInfo.defaultRPC;
101
+ if (!(rpcUrls === null || rpcUrls === void 0 ? void 0 : rpcUrls.length))
102
+ return 'unsupported';
103
+ try {
104
+ const client = (0, viem_1.createPublicClient)({
105
+ transport: (0, viem_1.fallback)(rpcUrls.map((url) => (0, viem_1.http)(url))),
106
+ });
107
+ const receipt = yield client.getTransactionReceipt({
108
+ hash: externalTxHash,
109
+ });
110
+ if (!receipt)
111
+ return 'pending';
112
+ if (receipt.status === 'success')
113
+ return 'success';
114
+ if (receipt.status === 'reverted')
115
+ return 'reverted';
116
+ return 'pending';
117
+ }
118
+ catch (err) {
119
+ // viem throws TransactionReceiptNotFoundError when the tx isn't mined
120
+ // yet — treat as 'pending' rather than 'unsupported' so we keep polling.
121
+ const msg = err instanceof Error ? err.message : String(err);
122
+ if (/not found|TransactionReceiptNotFound|could not be found/i.test(msg)) {
123
+ return 'pending';
124
+ }
125
+ (0, context_1.printLog)(ctx, `[verifyExternalEvmReceipt] RPC error for ${destinationChain} tx ${externalTxHash}: ${msg}`);
126
+ return 'pending';
127
+ }
128
+ });
129
+ }
33
130
  /**
34
131
  * Wait for outbound transaction to complete and return external chain details.
35
132
  * @internal Used by .wait() for outbound routes - not part of public API.
@@ -45,7 +142,7 @@ exports.OUTBOUND_MAX_TIMEOUT_MS = 180000; // 180s (3 min)
45
142
  */
46
143
  function waitForOutboundTx(ctx_1, pushChainTxHash_1) {
47
144
  return tslib_1.__awaiter(this, arguments, void 0, function* (ctx, pushChainTxHash, options = {}) {
48
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
145
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
49
146
  const { initialWaitMs = exports.OUTBOUND_INITIAL_WAIT_MS, pollingIntervalMs = exports.OUTBOUND_POLL_INTERVAL_MS, timeout = exports.OUTBOUND_MAX_TIMEOUT_MS, progressHook, _resolvedSubTxId, _expectedDestinationChain, } = options;
50
147
  // Terminal failure states — fail fast instead of polling until timeout
51
148
  const TERMINAL_FAILURE_STATES = new Set([
@@ -56,8 +153,21 @@ function waitForOutboundTx(ctx_1, pushChainTxHash_1) {
56
153
  const startTime = Date.now();
57
154
  (0, context_1.printLog)(ctx, `[waitForOutboundTx] Starting | txHash: ${pushChainTxHash} | initialWait: ${initialWaitMs}ms | pollInterval: ${pollingIntervalMs}ms | timeout: ${timeout}ms`);
58
155
  progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'waiting', elapsed: 0 });
59
- (0, context_1.printLog)(ctx, `[waitForOutboundTx] Initial wait of ${initialWaitMs}ms...`);
60
- yield new Promise((resolve) => setTimeout(resolve, initialWaitMs));
156
+ // Clamp the initial wait to the configured timeout so a short per-call
157
+ // `outboundTimeoutMs` (e.g. from `tx.wait({ outboundTimeoutMs })`) isn't
158
+ // blocked by the default 20s settle-time. Mirrors the equivalent
159
+ // Math.min(initialWaitMs, timeout) clamp in inbound-tracker.ts.
160
+ const effectiveInitialWaitMs = Math.min(initialWaitMs, timeout);
161
+ (0, context_1.printLog)(ctx, `[waitForOutboundTx] Initial wait of ${effectiveInitialWaitMs}ms (configured: ${initialWaitMs}ms, clamped to timeout ${timeout}ms)...`);
162
+ yield new Promise((resolve) => setTimeout(resolve, effectiveInitialWaitMs));
163
+ // Fast-exit: if the clamped initial wait already consumed the full budget,
164
+ // skip the poll loop and go straight to the timeout throw below.
165
+ if (Date.now() - startTime >= timeout) {
166
+ const elapsedMs = Date.now() - startTime;
167
+ (0, context_1.printLog)(ctx, `[waitForOutboundTx] Timeout reached during initial wait (elapsed: ${elapsedMs}ms / ${timeout}ms)`);
168
+ progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'timeout', elapsed: elapsedMs });
169
+ throw new OutboundTimeoutError(pushChainTxHash, elapsedMs, timeout);
170
+ }
61
171
  // Start polling
62
172
  (0, context_1.printLog)(ctx, `[waitForOutboundTx] Initial wait done. Starting polling. Elapsed: ${Date.now() - startTime}ms`);
63
173
  progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'polling', elapsed: Date.now() - startTime });
@@ -90,7 +200,7 @@ function waitForOutboundTx(ctx_1, pushChainTxHash_1) {
90
200
  if (TERMINAL_FAILURE_STATES.has(statusNum)) {
91
201
  (0, context_1.printLog)(ctx, `[waitForOutboundTx] Terminal failure state: ${statusName}`);
92
202
  progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'failed', elapsed: Date.now() - startTime });
93
- throw new Error(`Outbound transaction failed with status ${statusName}. Push Chain TX: ${pushChainTxHash}.`);
203
+ throw new OutboundFailedError(`Outbound transaction failed with status ${statusName}. Push Chain TX: ${pushChainTxHash}.`, pushChainTxHash);
94
204
  }
95
205
  // Iterate V2 outbound array
96
206
  for (const ob of outbounds) {
@@ -98,18 +208,43 @@ function waitForOutboundTx(ctx_1, pushChainTxHash_1) {
98
208
  if (ob.outboundStatus === types_2.OutboundStatus.REVERTED) {
99
209
  (0, context_1.printLog)(ctx, `[waitForOutboundTx] Outbound to ${ob.destinationChain} REVERTED`);
100
210
  progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'failed', elapsed: Date.now() - startTime });
101
- throw new Error(`Outbound to ${ob.destinationChain} reverted: ${((_h = ob.observedTx) === null || _h === void 0 ? void 0 : _h.errorMsg) || 'Unknown'}. Push Chain TX: ${pushChainTxHash}.`);
211
+ throw new OutboundFailedError(`Outbound to ${ob.destinationChain} reverted: ${((_h = ob.observedTx) === null || _h === void 0 ? void 0 : _h.errorMsg) || 'Unknown'}. Push Chain TX: ${pushChainTxHash}.`, pushChainTxHash, ob.destinationChain, (_j = ob.observedTx) === null || _j === void 0 ? void 0 : _j.txHash);
102
212
  }
103
- if ((_j = ob.observedTx) === null || _j === void 0 ? void 0 : _j.txHash) {
213
+ // FOUND gating logic:
214
+ // 1. Primary path — cosmos has transitioned to OBSERVED (the
215
+ // authoritative success signal from the indexer).
216
+ // 2. Tiebreaker — cosmos has the txHash but outboundStatus is
217
+ // still UNSPECIFIED/PENDING (indexer lag). We ask the
218
+ // destination chain's own RPC: if its receipt is success,
219
+ // treat as FOUND; if reverted, throw OutboundFailedError.
220
+ // Pending receipts keep polling.
221
+ if ((_k = ob.observedTx) === null || _k === void 0 ? void 0 : _k.txHash) {
104
222
  // If a destination chain filter is set, skip outbound entries that don't match
105
223
  if (_expectedDestinationChain && ob.destinationChain !== _expectedDestinationChain) {
106
224
  (0, context_1.printLog)(ctx, `[waitForOutboundTx] Poll #${pollCount} | outbound chain '${ob.destinationChain}' does not match expected '${_expectedDestinationChain}', skipping`);
107
225
  continue;
108
226
  }
109
227
  const chain = (0, helpers_1.chainFromNamespace)(ob.destinationChain);
110
- if (chain) {
111
- const explorerBaseUrl = (_k = chain_1.CHAIN_INFO[chain]) === null || _k === void 0 ? void 0 : _k.explorerUrl;
112
- const isSvm = ((_l = chain_1.CHAIN_INFO[chain]) === null || _l === void 0 ? void 0 : _l.vm) === enums_1.VM.SVM;
228
+ if (!chain)
229
+ continue;
230
+ const isCosmosObserved = ob.outboundStatus === types_2.OutboundStatus.OBSERVED;
231
+ if (!isCosmosObserved) {
232
+ // Tiebreaker: ask the destination-chain RPC directly.
233
+ const verdict = yield verifyExternalEvmReceipt(ctx, chain, ob.observedTx.txHash);
234
+ if (verdict === 'reverted') {
235
+ (0, context_1.printLog)(ctx, `[waitForOutboundTx] Source-chain RPC reports REVERTED for ${ob.destinationChain} tx ${ob.observedTx.txHash} (cosmos still ${(_l = types_1.UniversalTxStatus[statusNum]) !== null && _l !== void 0 ? _l : statusNum})`);
236
+ progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'failed', elapsed: Date.now() - startTime });
237
+ throw new OutboundFailedError(`Outbound to ${ob.destinationChain} reverted on source-chain RPC (tx: ${ob.observedTx.txHash}). Push Chain TX: ${pushChainTxHash}.`, pushChainTxHash, ob.destinationChain, ob.observedTx.txHash);
238
+ }
239
+ if (verdict !== 'success') {
240
+ // 'pending' or 'unsupported' — keep polling, cosmos will catch up.
241
+ continue;
242
+ }
243
+ (0, context_1.printLog)(ctx, `[waitForOutboundTx] Source-chain RPC tiebreaker: ${chain} tx ${ob.observedTx.txHash} succeeded; treating as FOUND despite cosmos status ${(_m = types_1.UniversalTxStatus[statusNum]) !== null && _m !== void 0 ? _m : statusNum}`);
244
+ }
245
+ {
246
+ const explorerBaseUrl = (_o = chain_1.CHAIN_INFO[chain]) === null || _o === void 0 ? void 0 : _o.explorerUrl;
247
+ const isSvm = ((_p = chain_1.CHAIN_INFO[chain]) === null || _p === void 0 ? void 0 : _p.vm) === enums_1.VM.SVM;
113
248
  // For SVM chains, convert hex txHash to base58 and append cluster param
114
249
  let displayTxHash = ob.observedTx.txHash;
115
250
  let explorerUrl = '';
@@ -139,8 +274,10 @@ function waitForOutboundTx(ctx_1, pushChainTxHash_1) {
139
274
  }
140
275
  }
141
276
  catch (error) {
142
- // Re-throw terminal failure and reverted errors
143
- if (error instanceof Error && (error.message.includes('Outbound transaction failed') || error.message.includes('reverted'))) {
277
+ // Re-throw typed terminal failure so the caller can classify it as
278
+ // 299-02. Transient errors (RPC hiccups, deserialization) are logged
279
+ // and the loop continues polling until timeout.
280
+ if (error instanceof OutboundFailedError) {
144
281
  throw error;
145
282
  }
146
283
  (0, context_1.printLog)(ctx, `[waitForOutboundTx] Poll #${pollCount} ERROR: ${error instanceof Error ? error.message : String(error)}`);
@@ -149,10 +286,10 @@ function waitForOutboundTx(ctx_1, pushChainTxHash_1) {
149
286
  // Wait before next poll
150
287
  yield new Promise((resolve) => setTimeout(resolve, pollingIntervalMs));
151
288
  }
152
- (0, context_1.printLog)(ctx, `[waitForOutboundTx] TIMEOUT after ${pollCount} polls | elapsed: ${Date.now() - startTime}ms`);
153
- progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'timeout', elapsed: Date.now() - startTime });
154
- throw new Error(`Timeout waiting for outbound transaction. Push Chain TX: ${pushChainTxHash}. ` +
155
- `Timeout: ${timeout}ms. The relay may still be processing.`);
289
+ const elapsedMs = Date.now() - startTime;
290
+ (0, context_1.printLog)(ctx, `[waitForOutboundTx] TIMEOUT after ${pollCount} polls | elapsed: ${elapsedMs}ms`);
291
+ progressHook === null || progressHook === void 0 ? void 0 : progressHook({ status: 'timeout', elapsed: elapsedMs });
292
+ throw new OutboundTimeoutError(pushChainTxHash, elapsedMs, timeout);
156
293
  });
157
294
  }
158
295
  // ============================================================================
@@ -0,0 +1,28 @@
1
+ import { CHAIN } from '../../constants/enums';
2
+ import type { OrchestratorContext } from './context';
3
+ type StableKey = 'USDT_ETH' | 'USDT_ARB' | 'USDT_BASE' | 'USDT_BNB' | 'USDT_SOL';
4
+ /**
5
+ * Get $PC/USD price (8 decimals) for the given origin chain's route.
6
+ * Returns 0 on any failure — caller is expected to fall back to
7
+ * pushClient.pushToUSDC (the hardcoded $0.10 rate).
8
+ */
9
+ export declare function getPcUsdPrice(ctx: OrchestratorContext, originChain: CHAIN): Promise<bigint>;
10
+ /**
11
+ * Convert a nPUSH (18-dec) amount to USD (8-dec) using the per-route oracle.
12
+ * Falls back to the fixed pushToUSDC rate if the oracle returns 0.
13
+ */
14
+ export declare function pcToUsd(ctx: OrchestratorContext, pcAmount: bigint, originChain: CHAIN): Promise<bigint>;
15
+ /**
16
+ * Convert a USD (8-dec) amount to nPUSH (18-dec) using the per-route oracle.
17
+ * Falls back to the fixed usdcToPush rate if the oracle returns 0.
18
+ */
19
+ export declare function usdToPc(ctx: OrchestratorContext, usdAmount: bigint, originChain: CHAIN): Promise<bigint>;
20
+ /**
21
+ * Test-only: clear the module-level cache. Unit tests mock `readContract`
22
+ * and need a fresh slate between runs.
23
+ * @internal
24
+ */
25
+ export declare function __resetPcUsdCache(): void;
26
+ /** Re-exported for tests that need to assert chain coverage. */
27
+ export declare const __ORIGIN_TO_STABLE: Partial<Record<CHAIN, StableKey>>;
28
+ export {};
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.__ORIGIN_TO_STABLE = void 0;
4
+ exports.getPcUsdPrice = getPcUsdPrice;
5
+ exports.pcToUsd = pcToUsd;
6
+ exports.usdToPc = usdToPc;
7
+ exports.__resetPcUsdCache = __resetPcUsdCache;
8
+ const tslib_1 = require("tslib");
9
+ /**
10
+ * Per-route $PC/USD oracle.
11
+ *
12
+ * There is no on-chain $PC/USD price feed today. Per Harsh (2026-04-17),
13
+ * derive $PC/USD from the WPC/stable Uniswap V3 pool on Push Chain, with
14
+ * the stable leg chosen by the origin route:
15
+ * - Ethereum / Arbitrum / Base / BNB → WPC/USDT.<route>
16
+ * - Solana → WPC/USDT.sol
17
+ *
18
+ * Pool pair (USDT vs USDC) is pending Zaryab/Zartaj confirmation — flip the
19
+ * ORIGIN_TO_STABLE map below if they pick USDC.
20
+ *
21
+ * Returns price in **8 decimals** (USD per 1 WPC) to match the convention
22
+ * used by PriceFetch.getPrice.
23
+ */
24
+ const viem_1 = require("viem");
25
+ const abi_1 = require("../../constants/abi");
26
+ const chain_1 = require("../../constants/chain");
27
+ const enums_1 = require("../../constants/enums");
28
+ const context_1 = require("./context");
29
+ const helpers_1 = require("./helpers");
30
+ /**
31
+ * Chosen stable token per origin chain. Change these values to switch to USDC.
32
+ * Decimals: assume 6 (standard USDT/USDC). If a PRC-20 deviates, add an entry
33
+ * to STABLE_DECIMALS override below.
34
+ */
35
+ const ORIGIN_TO_STABLE = {
36
+ [enums_1.CHAIN.ETHEREUM_SEPOLIA]: 'USDT_ETH',
37
+ [enums_1.CHAIN.ARBITRUM_SEPOLIA]: 'USDT_ARB',
38
+ [enums_1.CHAIN.BASE_SEPOLIA]: 'USDT_BASE',
39
+ [enums_1.CHAIN.BNB_TESTNET]: 'USDT_BNB',
40
+ [enums_1.CHAIN.SOLANA_DEVNET]: 'USDT_SOL',
41
+ };
42
+ /** Default decimals for stable PRC-20s on Push Chain. */
43
+ const DEFAULT_STABLE_DECIMALS = 6;
44
+ // ============================================================================
45
+ // QuoterV2 ABI (same subset used elsewhere in gas-calculator.ts)
46
+ // ============================================================================
47
+ const QUOTER_V2_ABI = [
48
+ {
49
+ type: 'function',
50
+ name: 'quoteExactInputSingle',
51
+ inputs: [
52
+ {
53
+ type: 'tuple',
54
+ components: [
55
+ { name: 'tokenIn', type: 'address' },
56
+ { name: 'tokenOut', type: 'address' },
57
+ { name: 'amountIn', type: 'uint256' },
58
+ { name: 'fee', type: 'uint24' },
59
+ { name: 'sqrtPriceLimitX96', type: 'uint160' },
60
+ ],
61
+ },
62
+ ],
63
+ outputs: [
64
+ { name: 'amountOut', type: 'uint256' },
65
+ { name: 'sqrtPriceX96After', type: 'uint160' },
66
+ { name: 'initializedTicksCrossed', type: 'uint32' },
67
+ { name: 'gasEstimate', type: 'uint256' },
68
+ ],
69
+ stateMutability: 'view',
70
+ },
71
+ ];
72
+ const UNIVERSAL_CORE_SWAP_HELPERS_ABI = [
73
+ {
74
+ type: 'function',
75
+ name: 'WPC',
76
+ inputs: [],
77
+ outputs: [{ name: '', type: 'address' }],
78
+ stateMutability: 'view',
79
+ },
80
+ {
81
+ type: 'function',
82
+ name: 'uniswapV3Factory',
83
+ inputs: [],
84
+ outputs: [{ name: '', type: 'address' }],
85
+ stateMutability: 'view',
86
+ },
87
+ {
88
+ type: 'function',
89
+ name: 'defaultFeeTier',
90
+ inputs: [{ name: '', type: 'address' }],
91
+ outputs: [{ name: '', type: 'uint24' }],
92
+ stateMutability: 'view',
93
+ },
94
+ ];
95
+ const UNISWAP_V3_FACTORY_ABI = [
96
+ {
97
+ type: 'function',
98
+ name: 'getPool',
99
+ inputs: [
100
+ { name: 'tokenA', type: 'address' },
101
+ { name: 'tokenB', type: 'address' },
102
+ { name: 'fee', type: 'uint24' },
103
+ ],
104
+ outputs: [{ name: 'pool', type: 'address' }],
105
+ stateMutability: 'view',
106
+ },
107
+ ];
108
+ /**
109
+ * Fee tiers to probe when `defaultFeeTier(stable)` returns 0 on UniversalCore.
110
+ * Per the 2026-04-17 testnet probe, all WPC/USDT.* stable pools live at 500.
111
+ * Fall back through the standard Uniswap V3 tier set if 500 misses.
112
+ */
113
+ const STABLE_FEE_TIER_FALLBACKS = [500, 3000, 100, 10000];
114
+ const ZERO_ADDR = '0x0000000000000000000000000000000000000000';
115
+ // ============================================================================
116
+ // Cache (module-level, 30s TTL — mirrors price-fetch.ts)
117
+ // ============================================================================
118
+ const PRICE_CACHE_TTL_MS = 30000;
119
+ const pcUsdPriceCache = new Map();
120
+ // ============================================================================
121
+ // Public API
122
+ // ============================================================================
123
+ /**
124
+ * Get $PC/USD price (8 decimals) for the given origin chain's route.
125
+ * Returns 0 on any failure — caller is expected to fall back to
126
+ * pushClient.pushToUSDC (the hardcoded $0.10 rate).
127
+ */
128
+ function getPcUsdPrice(ctx, originChain) {
129
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
130
+ var _a, _b, _c;
131
+ const cached = pcUsdPriceCache.get(originChain);
132
+ if (cached && Date.now() < cached.expiry) {
133
+ return cached.price;
134
+ }
135
+ const stableKey = ORIGIN_TO_STABLE[originChain];
136
+ if (!stableKey) {
137
+ (0, context_1.printLog)(ctx, `getPcUsdPrice — no stable token mapped for origin ${originChain}; returning 0`);
138
+ return BigInt(0);
139
+ }
140
+ const pushChain = (0, helpers_1.getPushChainForNetwork)(ctx.pushNetwork);
141
+ const quoterAddress = (_b = (_a = chain_1.CHAIN_INFO[pushChain]) === null || _a === void 0 ? void 0 : _a.dex) === null || _b === void 0 ? void 0 : _b.uniV3QuoterV2;
142
+ if (!quoterAddress || quoterAddress.startsWith('0xTBD')) {
143
+ (0, context_1.printLog)(ctx, `getPcUsdPrice — no QuoterV2 configured for ${pushChain}; returning 0`);
144
+ return BigInt(0);
145
+ }
146
+ const stableAddress = (_c = chain_1.SYNTHETIC_PUSH_ERC20[ctx.pushNetwork]) === null || _c === void 0 ? void 0 : _c[stableKey];
147
+ if (!stableAddress || stableAddress.startsWith('0xTBD')) {
148
+ (0, context_1.printLog)(ctx, `getPcUsdPrice — no ${stableKey} mapped on ${ctx.pushNetwork}; returning 0`);
149
+ return BigInt(0);
150
+ }
151
+ try {
152
+ const gatewayPcAddress = (0, helpers_1.getUniversalGatewayPCAddress)();
153
+ const universalCoreAddress = yield ctx.pushClient.readContract({
154
+ address: gatewayPcAddress,
155
+ abi: abi_1.UNIVERSAL_GATEWAY_PC,
156
+ functionName: 'UNIVERSAL_CORE',
157
+ args: [],
158
+ });
159
+ const [wpcAddress, factoryAddress, defaultFeeTier] = yield Promise.all([
160
+ ctx.pushClient.readContract({
161
+ address: universalCoreAddress,
162
+ abi: UNIVERSAL_CORE_SWAP_HELPERS_ABI,
163
+ functionName: 'WPC',
164
+ args: [],
165
+ }),
166
+ ctx.pushClient.readContract({
167
+ address: universalCoreAddress,
168
+ abi: UNIVERSAL_CORE_SWAP_HELPERS_ABI,
169
+ functionName: 'uniswapV3Factory',
170
+ args: [],
171
+ }),
172
+ ctx.pushClient.readContract({
173
+ address: universalCoreAddress,
174
+ abi: UNIVERSAL_CORE_SWAP_HELPERS_ABI,
175
+ functionName: 'defaultFeeTier',
176
+ args: [stableAddress],
177
+ }),
178
+ ]);
179
+ if (!wpcAddress) {
180
+ (0, context_1.printLog)(ctx, `getPcUsdPrice — missing WPC; returning 0`);
181
+ return BigInt(0);
182
+ }
183
+ // UniversalCore's defaultFeeTier is set only for gas tokens (pETH*, pSOL).
184
+ // For stables, probe the factory for the first tier with a live pool.
185
+ let feeTier = defaultFeeTier;
186
+ if (!feeTier) {
187
+ for (const candidate of STABLE_FEE_TIER_FALLBACKS) {
188
+ const pool = yield ctx.pushClient.readContract({
189
+ address: factoryAddress,
190
+ abi: UNISWAP_V3_FACTORY_ABI,
191
+ functionName: 'getPool',
192
+ args: [wpcAddress, stableAddress, candidate],
193
+ });
194
+ if (pool && pool !== ZERO_ADDR) {
195
+ feeTier = candidate;
196
+ (0, context_1.printLog)(ctx, `getPcUsdPrice — resolved fee tier ${candidate} via factory probe for WPC/${stableKey}`);
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ if (!feeTier) {
202
+ (0, context_1.printLog)(ctx, `getPcUsdPrice — no WPC/${stableKey} pool found at any fee tier; returning 0`);
203
+ return BigInt(0);
204
+ }
205
+ // Quote: 1 WPC (1e18) → X stable (DEFAULT_STABLE_DECIMALS)
206
+ const oneWpc = (0, viem_1.parseUnits)('1', 18);
207
+ const result = yield ctx.pushClient.readContract({
208
+ address: quoterAddress,
209
+ abi: QUOTER_V2_ABI,
210
+ functionName: 'quoteExactInputSingle',
211
+ args: [
212
+ {
213
+ tokenIn: wpcAddress,
214
+ tokenOut: stableAddress,
215
+ amountIn: oneWpc,
216
+ fee: feeTier,
217
+ sqrtPriceLimitX96: BigInt(0),
218
+ },
219
+ ],
220
+ });
221
+ const stableOut = result[0];
222
+ if (stableOut === BigInt(0)) {
223
+ (0, context_1.printLog)(ctx, `getPcUsdPrice — quoter returned 0 for WPC→${stableKey}`);
224
+ return BigInt(0);
225
+ }
226
+ // Normalize stableOut (DEFAULT_STABLE_DECIMALS) to 8-decimal USD.
227
+ // If stable decimals > 8, divide; otherwise multiply.
228
+ const decDelta = 8 - DEFAULT_STABLE_DECIMALS;
229
+ const price = decDelta >= 0
230
+ ? stableOut * BigInt(Math.pow(10, decDelta))
231
+ : stableOut / BigInt(Math.pow(10, -decDelta));
232
+ (0, context_1.printLog)(ctx, `getPcUsdPrice — origin=${originChain} stable=${stableKey} quote: 1 WPC = ${stableOut.toString()} (${DEFAULT_STABLE_DECIMALS}d) = ${price.toString()} (1e8 USD)`);
233
+ pcUsdPriceCache.set(originChain, {
234
+ price,
235
+ expiry: Date.now() + PRICE_CACHE_TTL_MS,
236
+ });
237
+ return price;
238
+ }
239
+ catch (err) {
240
+ (0, context_1.printLog)(ctx, `getPcUsdPrice — failed: ${err instanceof Error ? err.message : String(err)}`);
241
+ return BigInt(0);
242
+ }
243
+ });
244
+ }
245
+ /**
246
+ * Convert a nPUSH (18-dec) amount to USD (8-dec) using the per-route oracle.
247
+ * Falls back to the fixed pushToUSDC rate if the oracle returns 0.
248
+ */
249
+ function pcToUsd(ctx, pcAmount, originChain) {
250
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
251
+ const price = yield getPcUsdPrice(ctx, originChain);
252
+ if (price === BigInt(0)) {
253
+ return ctx.pushClient.pushToUSDC(pcAmount);
254
+ }
255
+ const oneWpc = (0, viem_1.parseUnits)('1', 18);
256
+ return (pcAmount * price) / oneWpc;
257
+ });
258
+ }
259
+ /**
260
+ * Convert a USD (8-dec) amount to nPUSH (18-dec) using the per-route oracle.
261
+ * Falls back to the fixed usdcToPush rate if the oracle returns 0.
262
+ */
263
+ function usdToPc(ctx, usdAmount, originChain) {
264
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
265
+ const price = yield getPcUsdPrice(ctx, originChain);
266
+ if (price === BigInt(0)) {
267
+ return ctx.pushClient.usdcToPush(usdAmount);
268
+ }
269
+ const oneWpc = (0, viem_1.parseUnits)('1', 18);
270
+ return (usdAmount * oneWpc + (price - BigInt(1))) / price;
271
+ });
272
+ }
273
+ /**
274
+ * Test-only: clear the module-level cache. Unit tests mock `readContract`
275
+ * and need a fresh slate between runs.
276
+ * @internal
277
+ */
278
+ function __resetPcUsdCache() {
279
+ pcUsdPriceCache.clear();
280
+ }
281
+ /** Re-exported for tests that need to assert chain coverage. */
282
+ exports.__ORIGIN_TO_STABLE = ORIGIN_TO_STABLE;
283
+ // Reference to keep VM import used (stable decimals may differ by VM later).
284
+ void enums_1.VM;
285
+ //# sourceMappingURL=pc-usd-oracle.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Per-route progress hook builders for the outbound polling phase
3
+ * (.wait() in response-builder.ts and per-hop loops in cascade.waitForAll).
4
+ *
5
+ * Splitting this out gives both call sites a single source of truth for
6
+ * which IDs fire on each route, so cascade hops and single-tx outbound
7
+ * stay in lockstep.
8
+ */
9
+ import type { OutboundTxDetails } from '../orchestrator.types';
10
+ import type { ProgressEvent } from '../../progress-hook/progress-hook.types';
11
+ import { TransactionRoute } from '../route-detector';
12
+ export type WaitHookSet = {
13
+ intermediatePushOk?: (chain: string, pushTxHash: string) => ProgressEvent;
14
+ awaiting: (chain: string) => ProgressEvent;
15
+ polling: (chain: string, elapsedMs: number) => ProgressEvent;
16
+ success: (details: OutboundTxDetails) => ProgressEvent;
17
+ timeout: (chain: string, elapsedMs: number) => ProgressEvent;
18
+ failed: (chain: string, errMsg: string) => ProgressEvent;
19
+ };
20
+ export declare function pickWaitHooks(route: TransactionRoute | string | undefined): WaitHookSet;
21
+ /** True when the hook set is the no-op placeholder (route has no spec'd
22
+ * ID range). Emission sites should skip firing and receipt-mutation blocks. */
23
+ export declare function isNoopHookSet(hooks: WaitHookSet): boolean;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * Per-route progress hook builders for the outbound polling phase
4
+ * (.wait() in response-builder.ts and per-hop loops in cascade.waitForAll).
5
+ *
6
+ * Splitting this out gives both call sites a single source of truth for
7
+ * which IDs fire on each route, so cascade hops and single-tx outbound
8
+ * stay in lockstep.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.pickWaitHooks = pickWaitHooks;
12
+ exports.isNoopHookSet = isNoopHookSet;
13
+ const tslib_1 = require("tslib");
14
+ const progress_hook_types_1 = require("../../progress-hook/progress-hook.types");
15
+ const progress_hook_1 = tslib_1.__importDefault(require("../../progress-hook/progress-hook"));
16
+ const route_detector_1 = require("../route-detector");
17
+ function pickWaitHooks(route) {
18
+ if (route === route_detector_1.TransactionRoute.UOA_TO_CEA) {
19
+ return {
20
+ intermediatePushOk: (chain, txHash) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_299_99](chain, txHash),
21
+ awaiting: (chain) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_209_01](chain),
22
+ polling: (chain, elapsed) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_209_02](chain, elapsed),
23
+ success: (details) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_299_01](details),
24
+ timeout: (chain, elapsed) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_299_03](chain, elapsed),
25
+ failed: (chain, msg) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_299_02](chain, msg),
26
+ };
27
+ }
28
+ if (route === route_detector_1.TransactionRoute.CEA_TO_PUSH) {
29
+ return {
30
+ intermediatePushOk: (_chain, txHash) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_199_99_99](txHash),
31
+ awaiting: (chain) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_309_01](chain),
32
+ polling: (chain, elapsed) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_309_02](chain, elapsed),
33
+ // For R3, the outbound-poll "success" is the source-chain CEA executing
34
+ // (309-03 INFO). Terminal 399-01 fires after the inbound-to-Push poll
35
+ // (handled by waitForInboundPushTx in response-builder).
36
+ success: (details) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_309_03](details.destinationChain, details.externalTxHash),
37
+ // R3 outbound (source-chain CEA) failures share the 399-02/03 IDs with
38
+ // R3 inbound; pass phase='outbound' so the title reflects the source
39
+ // chain instead of "Push Chain Inbound …".
40
+ timeout: (chain, elapsed) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_399_03](chain, elapsed, 'outbound'),
41
+ failed: (chain, msg) => progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_399_02](msg, 'outbound', chain),
42
+ };
43
+ }
44
+ // Route 4 (CEA_TO_CEA) has no spec'd ID range yet; cascade hops and
45
+ // trackTransaction of pre-existing R4 UTXs still flow through here. Return
46
+ // a no-op set so wait() / cascade progress without emitting progress
47
+ // events for R4. When R4 gets its own ID range, add a branch above.
48
+ return NOOP_WAIT_HOOKS;
49
+ }
50
+ /** Sentinel event returned by NOOP_WAIT_HOOKS. Emission sites can detect
51
+ * and skip it by id === '' if they want to suppress no-op events. */
52
+ const NOOP_EVENT = Object.freeze({
53
+ id: '',
54
+ title: '',
55
+ message: '',
56
+ level: 'INFO',
57
+ response: null,
58
+ timestamp: '',
59
+ });
60
+ const NOOP_WAIT_HOOKS = Object.freeze({
61
+ awaiting: () => NOOP_EVENT,
62
+ polling: () => NOOP_EVENT,
63
+ success: () => NOOP_EVENT,
64
+ timeout: () => NOOP_EVENT,
65
+ failed: () => NOOP_EVENT,
66
+ });
67
+ /** True when the hook set is the no-op placeholder (route has no spec'd
68
+ * ID range). Emission sites should skip firing and receipt-mutation blocks. */
69
+ function isNoopHookSet(hooks) {
70
+ return hooks === NOOP_WAIT_HOOKS;
71
+ }
72
+ //# sourceMappingURL=progress-route-hooks.js.map