@pushchain/core 5.1.5-alpha.1 → 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 +521 -239
  16. package/src/lib/orchestrator/internals/confirmation.js +60 -15
  17. package/src/lib/orchestrator/internals/context.d.ts +23 -0
  18. package/src/lib/orchestrator/internals/context.js +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 +242 -66
  42. package/src/lib/orchestrator/internals/route-handlers.js +480 -206
  43. package/src/lib/orchestrator/internals/tx-transformer.js +293 -35
  44. package/src/lib/orchestrator/orchestrator.d.ts +7 -1
  45. package/src/lib/orchestrator/orchestrator.js +108 -56
  46. package/src/lib/orchestrator/orchestrator.types.d.ts +111 -5
  47. package/src/lib/orchestrator/payload-builders.d.ts +16 -0
  48. package/src/lib/orchestrator/payload-builders.js +6 -5
  49. package/src/lib/orchestrator/route-detector.js +20 -8
  50. package/src/lib/orchestrator/svm-idl/normalize-address.d.ts +19 -0
  51. package/src/lib/orchestrator/svm-idl/normalize-address.js +56 -0
  52. package/src/lib/orchestrator/svm-idl/registry.d.ts +13 -4
  53. package/src/lib/orchestrator/svm-idl/registry.js +15 -14
  54. package/src/lib/orchestrator/svm-idl/resolve.js +5 -2
  55. package/src/lib/progress-hook/progress-hook.d.ts +28 -0
  56. package/src/lib/progress-hook/progress-hook.js +545 -131
  57. package/src/lib/progress-hook/progress-hook.types.d.ts +76 -29
  58. package/src/lib/progress-hook/progress-hook.types.js +74 -35
  59. package/src/lib/push-client/push-client.d.ts +7 -0
  60. package/src/lib/push-client/push-client.js +18 -1
  61. package/src/lib/universal-tx-detector/__tests__/fixtures/builders.d.ts +32 -0
  62. package/src/lib/universal-tx-detector/__tests__/fixtures/builders.js +65 -0
  63. package/src/lib/universal-tx-detector/__tests__/fixtures/scenarios.d.ts +72 -0
  64. package/src/lib/universal-tx-detector/__tests__/fixtures/scenarios.js +669 -0
  65. package/src/lib/universal-tx-detector/auto-detect.d.ts +37 -0
  66. package/src/lib/universal-tx-detector/auto-detect.js +112 -0
  67. package/src/lib/universal-tx-detector/cascade.d.ts +64 -0
  68. package/src/lib/universal-tx-detector/cascade.js +254 -0
  69. package/src/lib/universal-tx-detector/child-inbounds.d.ts +58 -0
  70. package/src/lib/universal-tx-detector/child-inbounds.js +145 -0
  71. package/src/lib/universal-tx-detector/classify.d.ts +33 -0
  72. package/src/lib/universal-tx-detector/classify.js +228 -0
  73. package/src/lib/universal-tx-detector/detector-svm.d.ts +46 -0
  74. package/src/lib/universal-tx-detector/detector-svm.js +324 -0
  75. package/src/lib/universal-tx-detector/detector.d.ts +13 -0
  76. package/src/lib/universal-tx-detector/detector.js +233 -0
  77. package/src/lib/universal-tx-detector/events.d.ts +765 -0
  78. package/src/lib/universal-tx-detector/events.js +241 -0
  79. package/src/lib/universal-tx-detector/index.d.ts +18 -0
  80. package/src/lib/universal-tx-detector/index.js +36 -0
  81. package/src/lib/universal-tx-detector/svm-events.d.ts +41 -0
  82. package/src/lib/universal-tx-detector/svm-events.js +68 -0
  83. package/src/lib/universal-tx-detector/types.d.ts +100 -0
  84. package/src/lib/universal-tx-detector/types.js +3 -0
  85. package/src/lib/utils.d.ts +30 -13
  86. package/src/lib/utils.js +47 -21
@@ -8,11 +8,15 @@ exports.transformToUniversalTxReceipt = transformToUniversalTxReceipt;
8
8
  exports.reconstructProgressEvents = reconstructProgressEvents;
9
9
  exports.detectRouteFromUniversalTxData = detectRouteFromUniversalTxData;
10
10
  const tslib_1 = require("tslib");
11
+ const viem_1 = require("viem");
11
12
  const chain_1 = require("../../constants/chain");
12
13
  const enums_1 = require("../../constants/enums");
14
+ const selectors_1 = require("../../constants/selectors");
13
15
  const types_1 = require("../../generated/uexecutor/v1/types");
16
+ const types_2 = require("../../generated/uexecutor/v2/types");
14
17
  const progress_hook_1 = tslib_1.__importDefault(require("../../progress-hook/progress-hook"));
15
18
  const progress_hook_types_1 = require("../../progress-hook/progress-hook.types");
19
+ const payload_builders_1 = require("../payload-builders");
16
20
  const route_detector_1 = require("../route-detector");
17
21
  // ============================================================================
18
22
  // Receipt Transform
@@ -42,14 +46,66 @@ function transformToUniversalTxReceipt(receipt, originalTxResponse) {
42
46
  // Progress Event Reconstruction
43
47
  // ============================================================================
44
48
  function reconstructProgressEvents(universalTxResponse, universalTxData) {
45
- var _a;
49
+ var _a, _b, _c;
50
+ const route = universalTxResponse.route;
51
+ const pcTx = (_a = universalTxData === null || universalTxData === void 0 ? void 0 : universalTxData.pcTx) === null || _a === void 0 ? void 0 : _a[0];
52
+ const isOutboundFailed = (universalTxData === null || universalTxData === void 0 ? void 0 : universalTxData.universalStatus) === types_1.UniversalTxStatus.OUTBOUND_FAILED;
53
+ const isPcFailed = (pcTx === null || pcTx === void 0 ? void 0 : pcTx.status) === 'FAILED';
54
+ // R1 fallback keeps the inclusive view (any terminal failure → 199-02).
55
+ // R2 and R3 reconstructors narrow to Push-chain-only failures (see call
56
+ // sites below) — an outbound-leg revert is re-detected during wait() by
57
+ // `waitForOutboundTx` and emitted as 299-02 / 399-02 with the correctly
58
+ // scoped phase ('outbound' + source-chain). Emitting in both places
59
+ // would double-fire the terminal with a misleading 'push' title on top.
60
+ const isFailed = isPcFailed || isOutboundFailed;
61
+ const errorMsg = isOutboundFailed
62
+ ? 'Outbound transaction failed (status: OUTBOUND_FAILED)'
63
+ : ((pcTx === null || pcTx === void 0 ? void 0 : pcTx.errorMsg) || 'Unknown error');
64
+ // Route 2 / 3 reconstruction — keep parity with live emission from
65
+ // route-handlers.ts (which fires 201/203/207 or 301/303/307 then suppresses
66
+ // R1 IDs via fireProgressHook). Skip the R1 101–107 sequence here; it would
67
+ // only fire for R2/R3 because reconstructProgressEvents bypasses
68
+ // fireProgressHook (writes to events[] directly).
69
+ // For R2/R3 we only emit the reconstructed terminal when the Push-chain
70
+ // leg itself failed — so prefer pcTx.errorMsg over the outbound-failed
71
+ // sentinel. The outer `errorMsg` above carries the outbound text when
72
+ // `isOutboundFailed` is set; that path is handled by wait() instead.
73
+ const pcErrorMsg = (pcTx === null || pcTx === void 0 ? void 0 : pcTx.errorMsg) || 'Unknown error';
74
+ // Multichain cascade: a single Push-chain tx that fanned out into >1
75
+ // outbound legs (executeTransactions([tx1, tx2, …])). The cosmos record
76
+ // carries one UniversalTx with multiple outboundTx entries; wrap the
77
+ // per-leg R2/R3 backbones with the SEND-TX-001 / 002-xx / 999-xx markers
78
+ // so replay parity matches live cascade emission.
79
+ if (((_c = (_b = universalTxData === null || universalTxData === void 0 ? void 0 : universalTxData.outboundTx) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 1) {
80
+ return reconstructMultichain(universalTxResponse, universalTxData, isFailed, errorMsg);
81
+ }
82
+ if (route === route_detector_1.TransactionRoute.UOA_TO_CEA) {
83
+ // R2: only Push-chain revert triggers the 299-02 terminal here; a
84
+ // cosmos OUTBOUND_FAILED without pcTx.FAILED is handled by wait().
85
+ return reconstructR2(universalTxResponse, universalTxData, isPcFailed, pcErrorMsg);
86
+ }
87
+ if (route === route_detector_1.TransactionRoute.CEA_TO_PUSH) {
88
+ // R3: same narrowing — only pcTx.FAILED triggers reconstructed 399-02.
89
+ return reconstructR3(universalTxResponse, universalTxData, isPcFailed, pcErrorMsg);
90
+ }
91
+ // Route 1 (and Route 4 fallback while R4 has no spec'd IDs) — emit the
92
+ // safe R1 backbone that applies to every sub-path: origin → gas → UEA
93
+ // (non-Push only) → broadcast → terminal. The sub-path-specific hooks
94
+ // (104-xx signature, 105-xx fee-lock, 106-xx funds-bridge) are intentionally
95
+ // omitted from reconstruction because R1 doesn't register a UniversalTx
96
+ // on Push Chain, so `universalTxData` is usually undefined here and we
97
+ // have no reliable signal to tell the three paths apart. Callers who want
98
+ // the full live sequence should either register `progressHook` at
99
+ // `initialize()` time (client-level) or call `tx.progressHook(cb)` after
100
+ // sendTransaction — both deliver the full stream from the execute-phase
101
+ // event buffer.
46
102
  const events = [];
47
103
  const originParts = universalTxResponse.origin.split(':');
48
104
  const chainNamespace = originParts.length >= 2 ? `${originParts[0]}:${originParts[1]}` : originParts[0];
49
105
  const originAddress = originParts.length >= 3 ? originParts[2] : universalTxResponse.from;
50
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_01](chainNamespace, originAddress));
51
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_02_01]());
52
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_02_02](universalTxResponse.gasLimit));
106
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_101](chainNamespace, originAddress));
107
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_102_01]());
108
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_102_02](universalTxResponse.gasLimit));
53
109
  const pushChainIds = [
54
110
  chain_1.CHAIN_INFO[enums_1.CHAIN.PUSH_MAINNET].chainId,
55
111
  chain_1.CHAIN_INFO[enums_1.CHAIN.PUSH_TESTNET_DONUT].chainId,
@@ -57,47 +113,239 @@ function reconstructProgressEvents(universalTxResponse, universalTxData) {
57
113
  ];
58
114
  const isPushOrigin = pushChainIds.some((id) => chainNamespace.includes(`eip155:${id}`));
59
115
  if (!isPushOrigin) {
60
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_03_01]());
61
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_03_02](universalTxResponse.from, true));
116
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_103_01]());
117
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_103_02](universalTxResponse.from, true));
62
118
  }
63
- const inboundTx = universalTxData === null || universalTxData === void 0 ? void 0 : universalTxData.inboundTx;
64
- const hasFundsFlow = inboundTx && BigInt(inboundTx.amount || '0') > BigInt(0);
65
- if (!isPushOrigin) {
66
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_04_02]());
67
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_04_03]());
68
- }
69
- if (hasFundsFlow && inboundTx) {
70
- const amount = BigInt(inboundTx.amount);
71
- const decimals = 18;
72
- const symbol = 'TOKEN';
73
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_06_01](amount, decimals, symbol));
74
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_06_02](inboundTx.txHash, amount, decimals, symbol));
75
- const confirmations = 1;
76
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_06_03](confirmations));
77
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_06_03_02](confirmations, confirmations));
78
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_06_04]());
79
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_06_05]());
80
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_06_06](amount, decimals, symbol));
81
- }
82
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_07]());
83
- const pcTx = (_a = universalTxData === null || universalTxData === void 0 ? void 0 : universalTxData.pcTx) === null || _a === void 0 ? void 0 : _a[0];
84
- const isOutboundFailed = (universalTxData === null || universalTxData === void 0 ? void 0 : universalTxData.universalStatus) === types_1.UniversalTxStatus.OUTBOUND_FAILED;
85
- const isPcFailed = (pcTx === null || pcTx === void 0 ? void 0 : pcTx.status) === 'FAILED';
86
- const isFailed = isPcFailed || isOutboundFailed;
119
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_107]());
87
120
  if (isFailed) {
88
- const errorMsg = isOutboundFailed
89
- ? 'Outbound transaction failed (status: OUTBOUND_FAILED)'
90
- : ((pcTx === null || pcTx === void 0 ? void 0 : pcTx.errorMsg) || 'Unknown error');
91
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_99_02](errorMsg));
121
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_199_02](errorMsg));
92
122
  }
93
123
  else {
94
- events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_99_01]([universalTxResponse]));
124
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_199_01]([universalTxResponse]));
125
+ }
126
+ return events;
127
+ }
128
+ function reconstructR2(universalTxResponse, universalTxData, isFailed, errorMsg) {
129
+ var _a, _b, _c, _d;
130
+ const events = [];
131
+ const targetChain = ((_a = universalTxResponse.chain) !== null && _a !== void 0 ? _a : 'external');
132
+ const targetAddress = (_d = (_c = (_b = universalTxData === null || universalTxData === void 0 ? void 0 : universalTxData.outboundTx) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.recipient) !== null && _d !== void 0 ? _d : universalTxResponse.to;
133
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_201](targetChain, targetAddress));
134
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_01](targetChain));
135
+ // Reconstruction: gas split unavailable from historical data — pass zeroes
136
+ // so the shape matches live emission. totalCost derives to 0 in the message,
137
+ // which is acceptable for a tracked (past) transaction.
138
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_202_02](targetChain, BigInt(0), BigInt(0)));
139
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_01](targetChain));
140
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_203_02](universalTxResponse.from, targetAddress, targetChain, true));
141
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_01]());
142
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_02]());
143
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_204_03]());
144
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_207](targetChain));
145
+ if (isFailed) {
146
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_299_02](errorMsg));
147
+ }
148
+ else {
149
+ // Push Chain success is intermediate for R2 — the wait() outbound poll
150
+ // appends the real terminal (299-01/02/03) once the external leg resolves.
151
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_299_99](targetChain, universalTxResponse.hash));
152
+ }
153
+ return events;
154
+ }
155
+ function reconstructR3(universalTxResponse, universalTxData, isFailed, errorMsg) {
156
+ var _a, _b, _c, _d;
157
+ const events = [];
158
+ const sourceChain = ((_a = universalTxResponse.chain) !== null && _a !== void 0 ? _a : 'source');
159
+ const ceaAddress = (_d = (_c = (_b = universalTxData === null || universalTxData === void 0 ? void 0 : universalTxData.outboundTx) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.recipient) !== null && _d !== void 0 ? _d : universalTxResponse.to;
160
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_301](sourceChain, ceaAddress));
161
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_01](sourceChain));
162
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_302_02](sourceChain, BigInt(0), BigInt(0)));
163
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_01](sourceChain));
164
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_303_02](universalTxResponse.from, ceaAddress, sourceChain));
165
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_01]());
166
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_02]());
167
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_304_03]());
168
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_307](sourceChain));
169
+ if (isFailed) {
170
+ // Reconstructed Push-chain-tx failure — phase='push' so the title reads
171
+ // "Push Chain Tx Failed" rather than the inbound copy.
172
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_399_02](errorMsg, 'push'));
173
+ }
174
+ else {
175
+ // Push Chain success is intermediate for R3 — wait() drives the source-chain
176
+ // CEA poll (309-xx) and the inbound-to-Push poll (310-xx / 399-xx) on top.
177
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_199_99_99](universalTxResponse.hash));
178
+ }
179
+ return events;
180
+ }
181
+ // ============================================================================
182
+ // Multichain cascade reconstruction
183
+ // ============================================================================
184
+ function decodeUeaMulticall(data) {
185
+ if (!data)
186
+ return null;
187
+ const normalized = data.startsWith('0x') ? data : `0x${data}`;
188
+ if (!normalized.toLowerCase().startsWith(selectors_1.UEA_MULTICALL_SELECTOR))
189
+ return null;
190
+ try {
191
+ const body = `0x${normalized.slice(10)}`;
192
+ const [tuples] = (0, viem_1.decodeAbiParameters)([payload_builders_1.MULTICALL_TUPLE_TYPE], body);
193
+ return tuples;
194
+ }
195
+ catch (_a) {
196
+ return null;
197
+ }
198
+ }
199
+ /**
200
+ * Count the merged R1 calls embedded inside an R3 outbound's payload.
201
+ *
202
+ * The outbound payload on-chain is UEA_MULTICALL_SELECTOR + abi-encoded
203
+ * MultiCall[] targeting the CEA. One of those inner calls is
204
+ * `sendUniversalTxToUEA(token, amount, payload, revertRecipient)` whose
205
+ * `payload` argument is itself a UEA_MULTICALL_SELECTOR + MultiCall[] blob
206
+ * — that blob is what will execute on Push Chain after the round-trip. Its
207
+ * tuple count equals the number of user R1 hops merged into this R3 leg.
208
+ */
209
+ function decodeR3MergedInnerCount(outboundPayload) {
210
+ const outerCalls = decodeUeaMulticall(outboundPayload);
211
+ if (!outerCalls)
212
+ return 0;
213
+ for (const call of outerCalls) {
214
+ const d = (call.data || '').toLowerCase();
215
+ if (!d.startsWith(`0x${SEND_UNIVERSAL_TX_TO_UEA_SELECTOR}`))
216
+ continue;
217
+ try {
218
+ const innerBody = `0x${d.slice(10)}`;
219
+ // sendUniversalTxToUEA(token, amount, payload, revertRecipient).
220
+ // The `payload` arg is an ABI-encoded UniversalPayload struct
221
+ // produced by buildInboundUniversalPayload() — its `.data` field is
222
+ // the UEA_MULTICALL bytes that will execute on Push after the
223
+ // round-trip.
224
+ const [, , upStructBytes] = (0, viem_1.decodeAbiParameters)([
225
+ { type: 'address' },
226
+ { type: 'uint256' },
227
+ { type: 'bytes' },
228
+ { type: 'address' },
229
+ ], innerBody);
230
+ const [universalPayload] = (0, viem_1.decodeAbiParameters)([
231
+ {
232
+ type: 'tuple',
233
+ components: [
234
+ { name: 'to', type: 'address' },
235
+ { name: 'value', type: 'uint256' },
236
+ { name: 'data', type: 'bytes' },
237
+ { name: 'gasLimit', type: 'uint256' },
238
+ { name: 'maxFeePerGas', type: 'uint256' },
239
+ { name: 'maxPriorityFeePerGas', type: 'uint256' },
240
+ { name: 'nonce', type: 'uint256' },
241
+ { name: 'deadline', type: 'uint256' },
242
+ { name: 'vType', type: 'uint8' },
243
+ ],
244
+ },
245
+ ], upStructBytes);
246
+ const pushCalls = decodeUeaMulticall(universalPayload.data);
247
+ if (pushCalls)
248
+ return pushCalls.length;
249
+ }
250
+ catch (_a) {
251
+ // fall through to next outer call
252
+ }
253
+ }
254
+ return 0;
255
+ }
256
+ function reconstructMultichain(universalTxResponse, universalTxData, isFailed, errorMsg) {
257
+ const events = [];
258
+ const outbounds = universalTxData.outboundTx;
259
+ // Merged-R1 recovery: each R3 outbound's payload embeds the
260
+ // sendUniversalTxToUEA call whose `payload` arg is the UEA multicall that
261
+ // will execute on Push after the round-trip. Its tuple count equals the
262
+ // number of user R1 hops the orchestrator folded into that R3 leg.
263
+ // Total user-level hopCount =
264
+ // (outbounds that are NOT R3) + Σ (merged inner-call count per R3 leg).
265
+ // Falls back to `outbounds.length` when no R3 legs or decode fails
266
+ // (pure R2 cascade).
267
+ let r3LegCount = 0;
268
+ let totalMergedInner = 0;
269
+ for (const ob of outbounds) {
270
+ const isR3 = (ob.payload || '')
271
+ .toLowerCase()
272
+ .includes(SEND_UNIVERSAL_TX_TO_UEA_SELECTOR);
273
+ if (!isR3)
274
+ continue;
275
+ r3LegCount += 1;
276
+ const inner = decodeR3MergedInnerCount(ob.payload || '');
277
+ // When decode fails, assume the R3 leg represents 1 user hop so the
278
+ // fallback converges on outbounds.length.
279
+ totalMergedInner += inner > 0 ? inner : 1;
280
+ }
281
+ const recovered = r3LegCount > 0
282
+ ? outbounds.length - r3LegCount + totalMergedInner
283
+ : outbounds.length;
284
+ // Defensive clamp: never under-count what's already visible on-chain.
285
+ const hopCount = Math.max(recovered, outbounds.length);
286
+ // Source chain for SEND-TX-001: the Push Chain (initiator of a cascade
287
+ // always executes a multicall on Push first). Fall back to the response's
288
+ // chain if origin parsing fails.
289
+ const pushChainId = chain_1.CHAIN_INFO[enums_1.CHAIN.PUSH_TESTNET_DONUT].chainId;
290
+ const sourceChain = `eip155:${pushChainId}`;
291
+ const destChains = outbounds.map((ob) => ob.destinationChain);
292
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_001](hopCount, [
293
+ sourceChain,
294
+ ...destChains,
295
+ ]));
296
+ let failedAt = null;
297
+ let failureMsg = errorMsg;
298
+ outbounds.forEach((ob, i) => {
299
+ var _a;
300
+ const hopNum = i + 1;
301
+ const fromChain = i === 0 ? sourceChain : outbounds[i - 1].destinationChain;
302
+ const toChain = ob.destinationChain;
303
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_002_01](hopNum, hopCount, fromChain, toChain));
304
+ // Build a single-leg view so the per-leg reconstructors (R2/R3) emit
305
+ // their usual backbone. We narrow `outboundTx` to this leg so any field
306
+ // reads (e.g. `outboundTx[0].recipient`) resolve to this leg's data.
307
+ const legResponse = Object.assign(Object.assign({}, universalTxResponse), { chain: toChain, chainNamespace: toChain });
308
+ const legData = Object.assign(Object.assign({}, universalTxData), { outboundTx: [ob] });
309
+ const payload = (ob.payload || '').toLowerCase();
310
+ const isR3Leg = payload.includes(SEND_UNIVERSAL_TX_TO_UEA_SELECTOR);
311
+ const legFailed = ob.outboundStatus === types_2.OutboundStatus.REVERTED;
312
+ const legError = ob.abortReason || ((_a = ob.observedTx) === null || _a === void 0 ? void 0 : _a.errorMsg) || 'leg execution failed';
313
+ if (isR3Leg) {
314
+ events.push(...reconstructR3(legResponse, legData, legFailed, legError));
315
+ }
316
+ else {
317
+ events.push(...reconstructR2(legResponse, legData, legFailed, legError));
318
+ }
319
+ if (!legFailed) {
320
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_002_99_99](hopNum, hopCount));
321
+ }
322
+ else if (failedAt === null) {
323
+ failedAt = hopNum;
324
+ failureMsg = legError;
325
+ }
326
+ });
327
+ if (failedAt !== null) {
328
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_02](failedAt, hopCount, failureMsg));
329
+ }
330
+ else if (isFailed) {
331
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_02](hopCount, hopCount, errorMsg));
332
+ }
333
+ else {
334
+ events.push(progress_hook_1.default[progress_hook_types_1.PROGRESS_HOOK.SEND_TX_999_01](hopCount));
95
335
  }
96
336
  return events;
97
337
  }
98
338
  // ============================================================================
99
339
  // Route Detection from V2 Data
100
340
  // ============================================================================
341
+ /**
342
+ * Function selector of `sendUniversalTxToUEA(address,uint256,bytes,address)`.
343
+ * R3 (CEA → Push round-trip) outbound payloads always contain a CEA
344
+ * self-call to this method — its presence in the outbound payload bytes
345
+ * disambiguates a fresh R3 outbound (no inbound leg yet) from R2.
346
+ * Verified via `keccak256(toBytes('sendUniversalTxToUEA(address,uint256,bytes,address)')).slice(0,10)`.
347
+ */
348
+ const SEND_UNIVERSAL_TX_TO_UEA_SELECTOR = 'e7c1e3fc';
101
349
  function detectRouteFromUniversalTxData(universalTxData) {
102
350
  if (!universalTxData)
103
351
  return undefined;
@@ -109,6 +357,16 @@ function detectRouteFromUniversalTxData(universalTxData) {
109
357
  types_1.UniversalTxStatus.OUTBOUND_FAILED,
110
358
  ].includes(universalTxData.universalStatus);
111
359
  if (hasOutbound || statusIndicatesOutbound) {
360
+ // Disambiguate R3 from R2 when the inbound leg of an R3 round-trip
361
+ // hasn't materialized yet (fresh tx). R3 outbounds always carry a CEA
362
+ // self-call to `sendUniversalTxToUEA(...)` — its selector in the
363
+ // payload bytes is a deterministic signal.
364
+ const r3Signal = universalTxData.outboundTx.some((ob) => {
365
+ const payload = (ob.payload || '').toLowerCase();
366
+ return payload.includes(SEND_UNIVERSAL_TX_TO_UEA_SELECTOR);
367
+ });
368
+ if (r3Signal)
369
+ return route_detector_1.TransactionRoute.CEA_TO_PUSH;
112
370
  return hasInbound
113
371
  ? route_detector_1.TransactionRoute.CEA_TO_CEA
114
372
  : route_detector_1.TransactionRoute.UOA_TO_CEA;
@@ -5,6 +5,7 @@ import { ProgressEvent } from '../progress-hook/progress-hook.types';
5
5
  import { PushClient } from '../push-client/push-client';
6
6
  import type { UniversalAccount, UniversalSigner } from '../universal/universal.types';
7
7
  import { ExecuteParams, UniversalExecuteParams, PreparedUniversalTx, CascadedTxResponse, RescueFundsParams } from './orchestrator.types';
8
+ import { TransactionRoute } from './route-detector';
8
9
  export declare class Orchestrator {
9
10
  private readonly universalSigner;
10
11
  private pushNetwork;
@@ -16,6 +17,11 @@ export declare class Orchestrator {
16
17
  get pushClient(): PushClient;
17
18
  accountStatusCache: AccountStatus | null;
18
19
  accountStatusReadyPromise?: Promise<void>;
20
+ /**
21
+ * @internal Mirrors `OrchestratorContext.currentRoute` — set at the top of
22
+ * execute()/trackTransaction() so emission sites pick the right ID range.
23
+ */
24
+ currentRoute?: TransactionRoute;
19
25
  private get ctx();
20
26
  private _getResponseCallbacks;
21
27
  private _getCascadeCallbacks;
@@ -87,7 +93,7 @@ export declare class Orchestrator {
87
93
  /**
88
94
  * Queries gas fee for an outbound transaction from the UniversalCore contract.
89
95
  */
90
- queryOutboundGasFee(prc20Token: `0x${string}`, gasLimit: bigint): Promise<{
96
+ queryOutboundGasFee(prc20Token: `0x${string}`, gasLimit: bigint, destinationChain?: CHAIN): Promise<{
91
97
  gasToken: `0x${string}`;
92
98
  gasFee: bigint;
93
99
  protocolFee: bigint;
@@ -23,6 +23,7 @@ class Orchestrator {
23
23
  transformToUniversalTxReceipt: (receipt, resp) => (0, internals_1.transformToUniversalTxReceipt)(receipt, resp),
24
24
  printLog: (msg) => (0, internals_1.printLog)(this.ctx, msg),
25
25
  outboundConstants: { initialWaitMs: internals_1.OUTBOUND_INITIAL_WAIT_MS, pollingIntervalMs: internals_1.OUTBOUND_POLL_INTERVAL_MS, maxTimeoutMs: internals_1.OUTBOUND_MAX_TIMEOUT_MS },
26
+ inboundConstants: { initialWaitMs: internals_1.INBOUND_INITIAL_WAIT_MS, pollingIntervalMs: internals_1.OUTBOUND_POLL_INTERVAL_MS, maxTimeoutMs: internals_1.INBOUND_MAX_TIMEOUT_MS },
26
27
  };
27
28
  }
28
29
  _getCascadeCallbacks() {
@@ -130,67 +131,118 @@ class Orchestrator {
130
131
  execute(params) {
131
132
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
132
133
  var _a, _b, _c, _d;
133
- // Lazy UEA upgrade check
134
+ // Snapshot the caller's active route — `executeMultiChain` recurses into
135
+ // this same method for R2/R3's inner Push-side execution, and without
136
+ // snapshot/restore the recursive `detectRoute(params)` would clobber the
137
+ // outer R2/R3 route with UOA_TO_PUSH (the inner `to` is a string, no
138
+ // `from`), defeating R1-suppression in fireProgressHook.
139
+ const previousRoute = this.currentRoute;
140
+ const isRecursiveInnerCall = previousRoute !== undefined;
141
+ let detectedRoute;
134
142
  try {
135
- if (!this.accountStatusCache || !this.accountStatusCache.uea.loaded) {
136
- yield ((_a = this.accountStatusReadyPromise) !== null && _a !== void 0 ? _a : this.getAccountStatus());
137
- }
138
- // If background resolved but cache still empty (timeout/error), fetch fresh
139
- if (!this.accountStatusCache || !this.accountStatusCache.uea.loaded) {
140
- yield this.getAccountStatus();
141
- }
142
- if (((_b = this.accountStatusCache) === null || _b === void 0 ? void 0 : _b.uea.deployed) &&
143
- ((_c = this.accountStatusCache) === null || _c === void 0 ? void 0 : _c.uea.requiresUpgrade)) {
144
- yield this.upgradeAccount({ progressHook: this.progressHook });
145
- }
143
+ detectedRoute = (0, route_detector_1.detectRoute)(params);
146
144
  }
147
- catch (err) {
148
- (0, internals_1.printLog)(this.ctx, `Lazy UEA upgrade check failed: ${err instanceof Error ? err.message : String(err)}. Proceeding with transaction.`);
145
+ catch (_e) {
146
+ detectedRoute = route_detector_1.TransactionRoute.UOA_TO_PUSH;
149
147
  }
150
- // Check if this is a multi-chain request (has ChainTarget or from.chain)
151
- const isMultiChain = (0, route_detector_1.isChainTarget)(params.to) || ('from' in params && ((_d = params.from) === null || _d === void 0 ? void 0 : _d.chain));
152
- if (isMultiChain) {
153
- return (0, internals_1.executeMultiChain)(this.ctx, params, this.execute.bind(this));
154
- }
155
- // Standard Push Chain execution (Route 1)
156
- const execute = params;
157
- const hasFunds = !!execute.funds;
158
- const hasData = execute.data && execute.data !== '0x';
159
- const eventBuffer = [];
160
- const originalHook = this.progressHook;
161
- this.progressHook = (event) => {
162
- eventBuffer.push(event);
163
- if (originalHook)
164
- originalHook(event);
165
- };
148
+ // Don't downgrade an outer R2/R3/R4 route when the recursive inner call
149
+ // resolves to R1 — keep the outer perspective so emission stays consistent.
150
+ const shouldPreserveOuter = isRecursiveInnerCall &&
151
+ previousRoute !== route_detector_1.TransactionRoute.UOA_TO_PUSH &&
152
+ detectedRoute === route_detector_1.TransactionRoute.UOA_TO_PUSH;
153
+ this.currentRoute = shouldPreserveOuter ? previousRoute : detectedRoute;
154
+ // Reset the terminal-emitted flag at the start of each execute call so
155
+ // the outer catch below can tell whether a route handler already fired
156
+ // a terminal-ish error hook (104-04 / 204-04 / 304-04 / 199-02) and
157
+ // should suppress the second terminal (199-02 / 299-02 / 399-02).
158
+ this.ctx._routeTerminalEmitted = false;
166
159
  try {
167
- const rcb = () => this._getResponseCallbacks();
168
- if (execute.funds) {
169
- return !hasData
170
- ? yield (0, internals_1.executeFundsOnly)(this.ctx, execute, eventBuffer, rcb)
171
- : yield (0, internals_1.executeFundsWithPayload)(this.ctx, execute, eventBuffer, rcb);
160
+ // Lazy UEA upgrade check
161
+ try {
162
+ if (!this.accountStatusCache || !this.accountStatusCache.uea.loaded) {
163
+ yield ((_a = this.accountStatusReadyPromise) !== null && _a !== void 0 ? _a : this.getAccountStatus());
164
+ }
165
+ // If background resolved but cache still empty (timeout/error), fetch fresh
166
+ if (!this.accountStatusCache || !this.accountStatusCache.uea.loaded) {
167
+ yield this.getAccountStatus();
168
+ }
169
+ if (((_b = this.accountStatusCache) === null || _b === void 0 ? void 0 : _b.uea.deployed) &&
170
+ ((_c = this.accountStatusCache) === null || _c === void 0 ? void 0 : _c.uea.requiresUpgrade)) {
171
+ yield this.upgradeAccount({ progressHook: this.progressHook });
172
+ }
173
+ }
174
+ catch (err) {
175
+ (0, internals_1.printLog)(this.ctx, `Lazy UEA upgrade check failed: ${err instanceof Error ? err.message : String(err)}. Proceeding with transaction.`);
176
+ }
177
+ // Check if this is a multi-chain request (has ChainTarget or from.chain)
178
+ const isMultiChain = (0, route_detector_1.isChainTarget)(params.to) || ('from' in params && ((_d = params.from) === null || _d === void 0 ? void 0 : _d.chain));
179
+ if (isMultiChain) {
180
+ return yield (0, internals_1.executeMultiChain)(this.ctx, params, this.execute.bind(this));
181
+ }
182
+ // Standard Push Chain execution (Route 1)
183
+ const execute = params;
184
+ const hasFunds = !!execute.funds;
185
+ const hasData = execute.data && execute.data !== '0x';
186
+ const eventBuffer = [];
187
+ const originalHook = this.progressHook;
188
+ this.progressHook = (event) => {
189
+ eventBuffer.push(event);
190
+ if (originalHook)
191
+ originalHook(event);
192
+ };
193
+ try {
194
+ const rcb = () => this._getResponseCallbacks();
195
+ if (execute.funds) {
196
+ return !hasData
197
+ ? yield (0, internals_1.executeFundsOnly)(this.ctx, execute, eventBuffer, rcb)
198
+ : yield (0, internals_1.executeFundsWithPayload)(this.ctx, execute, eventBuffer, rcb);
199
+ }
200
+ return yield (0, internals_1.executeStandardPayload)(this.ctx, execute, eventBuffer, rcb);
201
+ }
202
+ catch (err) {
203
+ const errMessage = err instanceof Error
204
+ ? err.message
205
+ : typeof err === 'string'
206
+ ? err
207
+ : (() => {
208
+ try {
209
+ return JSON.stringify(err);
210
+ }
211
+ catch (_a) {
212
+ return 'Unknown error';
213
+ }
214
+ })();
215
+ // Only the outermost frame emits the terminal error hook, and it picks
216
+ // the route-correct ID. Recursive inner frames just re-throw so the
217
+ // outer catch drives emission — avoids double-firing 199/299/399 events.
218
+ // Additionally, if an inner route handler already fired a terminal-ish
219
+ // error (104-04/204-04/304-04/199-02 via execute-standard), the flag is
220
+ // set on ctx and we skip emitting a second terminal here.
221
+ if (!isRecursiveInnerCall && !this.ctx._routeTerminalEmitted) {
222
+ // For R3, the failure surfaced during execute-phase (pre-wait) is a
223
+ // Push-chain-side failure — tag phase='push' so 399-02 renders the
224
+ // correct "Push Chain Tx Failed" title instead of the inbound copy.
225
+ if (this.currentRoute === route_detector_1.TransactionRoute.CEA_TO_PUSH) {
226
+ (0, internals_1.fireProgressHook)(this.ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_399_02, errMessage, 'push');
227
+ }
228
+ else {
229
+ const terminalId = this.currentRoute === route_detector_1.TransactionRoute.UOA_TO_CEA
230
+ ? progress_hook_types_1.PROGRESS_HOOK.SEND_TX_299_02
231
+ : progress_hook_types_1.PROGRESS_HOOK.SEND_TX_199_02;
232
+ (0, internals_1.fireProgressHook)(this.ctx, terminalId, errMessage);
233
+ }
234
+ }
235
+ throw err;
236
+ }
237
+ finally {
238
+ // Restore original progressHook
239
+ this.progressHook = originalHook;
172
240
  }
173
- return yield (0, internals_1.executeStandardPayload)(this.ctx, execute, eventBuffer, rcb);
174
- }
175
- catch (err) {
176
- const errMessage = err instanceof Error
177
- ? err.message
178
- : typeof err === 'string'
179
- ? err
180
- : (() => {
181
- try {
182
- return JSON.stringify(err);
183
- }
184
- catch (_a) {
185
- return 'Unknown error';
186
- }
187
- })();
188
- (0, internals_1.fireProgressHook)(this.ctx, progress_hook_types_1.PROGRESS_HOOK.SEND_TX_99_02, errMessage);
189
- throw err;
190
241
  }
191
242
  finally {
192
- // Restore original progressHook
193
- this.progressHook = originalHook;
243
+ // Restore the caller's route so a recursive inner R1 execute doesn't
244
+ // leak its UOA_TO_PUSH route back to the outer R2/R3 frame.
245
+ this.currentRoute = previousRoute;
194
246
  }
195
247
  });
196
248
  }
@@ -219,9 +271,9 @@ class Orchestrator {
219
271
  /**
220
272
  * Queries gas fee for an outbound transaction from the UniversalCore contract.
221
273
  */
222
- queryOutboundGasFee(prc20Token, gasLimit) {
274
+ queryOutboundGasFee(prc20Token, gasLimit, destinationChain) {
223
275
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
224
- return (0, internals_1.queryOutboundGasFee)(this.ctx, prc20Token, gasLimit);
276
+ return (0, internals_1.queryOutboundGasFee)(this.ctx, prc20Token, gasLimit, destinationChain);
225
277
  });
226
278
  }
227
279
  /**