@ninsei-labs/sdk 0.39.3 → 0.40.0

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 (69) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/engine/atomic/dleq2.js +86 -111
  3. package/engine/atomic/halfenc.js +58 -72
  4. package/engine/atomic/halves.js +31 -53
  5. package/engine/atomic/order-client.js +15 -16
  6. package/engine/atomic/order.js +44 -62
  7. package/engine/atomic/orderContext.js +26 -50
  8. package/engine/atomic/orderHalfCommit.js +17 -18
  9. package/engine/core/chainSource.js +54 -69
  10. package/engine/core/clock.js +11 -12
  11. package/engine/core/config.js +561 -451
  12. package/engine/core/finality.js +34 -53
  13. package/engine/core/format.js +35 -36
  14. package/engine/core/preSignGuards.js +109 -117
  15. package/engine/core/rfqSource.js +57 -75
  16. package/engine/core/sellWindow.js +23 -24
  17. package/engine/core/store.js +20 -21
  18. package/engine/core/swap.js +242 -243
  19. package/engine/evm/amounts.js +50 -51
  20. package/engine/evm/asyncExec.js +577 -0
  21. package/engine/evm/asyncLegs.js +308 -0
  22. package/engine/evm/asyncRoute.js +179 -0
  23. package/engine/evm/auth.js +41 -48
  24. package/engine/evm/claimGas.js +28 -47
  25. package/engine/evm/depositor.js +39 -43
  26. package/engine/evm/dex.js +266 -267
  27. package/engine/evm/escrow.js +113 -129
  28. package/engine/evm/factory.js +71 -77
  29. package/engine/evm/fees.js +41 -42
  30. package/engine/evm/funding.js +160 -162
  31. package/engine/evm/gasReserve.js +67 -36
  32. package/engine/evm/index.js +46 -53
  33. package/engine/evm/injected.js +34 -42
  34. package/engine/evm/permit.js +129 -56
  35. package/engine/evm/prices.js +30 -45
  36. package/engine/evm/session.js +188 -189
  37. package/engine/mock/chain.js +17 -18
  38. package/engine/monero/address.js +33 -48
  39. package/engine/monero/halvesSweep.js +25 -43
  40. package/engine/monero/node.js +14 -15
  41. package/engine/monero/scan.js +82 -95
  42. package/engine/monero/swapKeys.js +30 -33
  43. package/engine/monero/unlock.js +17 -20
  44. package/engine/monero/wallet.js +32 -42
  45. package/engine/recovery/recoveryFile.js +154 -161
  46. package/engine/recovery/restore.js +50 -61
  47. package/engine/sdk/bridge.js +244 -234
  48. package/engine/ui/dom.js +4 -5
  49. package/engine/ui/walletModal.js +20 -31
  50. package/index.d.ts +8 -0
  51. package/package.json +1 -1
  52. package/src/adapters/evm-wallet.mjs +16 -0
  53. package/src/adapters/monero-wallet.mjs +1 -1
  54. package/src/adapters/monero.mjs +1 -1
  55. package/src/adapters/node-test-evm-wallet.mjs +20 -8
  56. package/src/config.mjs +1 -1
  57. package/src/engine.mjs +4 -0
  58. package/src/legs/cow-spec.mjs +254 -0
  59. package/src/legs/cow.mjs +315 -0
  60. package/src/legs/evm.mjs +86 -2
  61. package/src/legs/kyber-spec.mjs +265 -0
  62. package/src/legs/kyber.mjs +425 -0
  63. package/src/legs/shape.mjs +35 -0
  64. package/src/legs/uniswaprelay-spec.mjs +541 -0
  65. package/src/legs/uniswaprelay.mjs +422 -0
  66. package/src/legs/uniswapx-spec.mjs +329 -0
  67. package/src/legs/uniswapx.mjs +362 -0
  68. package/src/quotes.mjs +1 -1
  69. package/src/swaps.mjs +1 -1
@@ -0,0 +1,577 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/asyncExec.js for the SDK package.
2
+ // Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
3
+ // Everything below this header is identical to the source.
4
+
5
+ // THE ASYNCHRONOUS ROUTE, EXECUTED: INTENT -> SIGNATURE -> ORDER BOOK -> SETTLEMENT -> FUNDING.
6
+ //
7
+ // WHAT THIS MODULE IS AND WHY IT SITS IN THE ENGINE. Step 1 put the route PROVIDER in the package
8
+ // (sdk/src/legs/cow.mjs: the shape, the order book, the named refusals, settled()) and step 2 put the
9
+ // BALANCE-DRIVEN CHOICE in the engine (evm/permit.js: usdcWithoutEthVerdict, evm/asyncRoute.js: the network
10
+ // table). Neither of them moves anything: the decision says "the asynchronous path fits" and nothing runs it.
11
+ // This module is that missing half - the engine code that TAKES the asynchronous path on the page: it builds the
12
+ // intent, signs it through the page wallet, submits it to an order book, waits for settlement with a timeout and
13
+ // NAMED refusals, and only then hands control to the ordinary escrow funding.
14
+ //
15
+ // WHY HERE AND NOT IN THE PACKAGE. The page cannot import the package's sources (they pull bare npm specifiers
16
+ // and the DOM-free package never sees the page wallet); it reaches the core only through a built bundle whose
17
+ // surface is fixed. So the provider's CONTRACT (order fields, status vocabulary, refusal codes, order book path)
18
+ // is repeated here - exactly as the network table is repeated in evm/asyncRoute.js - and the agreement of the two
19
+ // is a CHECK (tools/check-async-route-live.mjs compares every constant against sdk/src/legs/cow.mjs), never a
20
+ // promise in a comment.
21
+ //
22
+ // NO NEW DEPENDENCY: the transport is the page's fetch, the signature is the wallet's own eth_signTypedData_v4,
23
+ // the EIP-712 typed data is assembled here from the order fields. Nothing is trusted to a library.
24
+ //
25
+ // EVERY FAILURE IS A NAMED VALUE, NEVER SILENCE AND NEVER A FUNDED DEAL WITH NO SWAP BEHIND IT.
26
+ import { asyncRouteForChain } from "./asyncRoute.js";
27
+ // THE LIQUIDITY LEG (KyberSwap Limit Order) - its own module, whose contract is cross-checked against the
28
+ // package (tools/check-async-route-compose.mjs). Kept apart from the CoW leg so each leg keeps its own named
29
+ // refusals and one implementation each.
30
+ import {
31
+ kyberLegPlan, kyberLegUnsigned, kyberLegSign, kyberLegSubmit, kyberLegSettled, kyberLegRefusalWords,
32
+ } from "./asyncLegs.js";
33
+
34
+ // THE SETTLEMENT CONTRACT AND THE NATIVE MARKER - the same two facts the package pins (sdk/src/legs/cow-spec.mjs):
35
+ // GPv2Settlement is deterministic and identical on every CoW network, and buying native uses the BUY_ETH_ADDRESS
36
+ // marker. A divergence between here and the package reddens the check.
37
+ export const ASYNC_SETTLEMENT = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41";
38
+ export const ASYNC_BUY_ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
39
+
40
+ // THE ORDER BOOK PATH (the package's createOrder endpoint, OpenAPI: POST /api/v1/orders).
41
+ export const ASYNC_SUBMIT_PATH = "/api/v1/orders";
42
+ export const ASYNC_ORDER_PATH = "/api/v1/orders/";
43
+
44
+ // THE DOMAIN NAME AND VERSION - GPv2Signing.sol (keccak256("Gnosis Protocol"), keccak256("v2")). Built from parts
45
+ // so no literal here is a multi-word phrase (the package's own rule: the SDK reads such literals as user-facing text).
46
+ export const ASYNC_DOMAIN_NAME = ["Gnosis", "Protocol"].join(" ");
47
+ export const ASYNC_DOMAIN_VERSION = "v2";
48
+
49
+ // THE ORDER FIELDS AND THEIR TYPES, IN PROTOCOL ORDER - GPv2Order.Data. THE ORDER IS THE PROTOCOL: a reordered
50
+ // or retyped field still produces a valid signature and is refused by the settlement, so the field list is pinned
51
+ // against the package's COW_ORDER_FIELDS by the check.
52
+ export const ASYNC_ORDER_FIELDS = [
53
+ ["sellToken", "address"],
54
+ ["buyToken", "address"],
55
+ ["receiver", "address"],
56
+ ["sellAmount", "uint256"],
57
+ ["buyAmount", "uint256"],
58
+ ["validTo", "uint32"],
59
+ ["appData", "bytes32"],
60
+ ["feeAmount", "uint256"],
61
+ ["kind", "string"],
62
+ ["partiallyFillable", "bool"],
63
+ ["sellTokenBalance", "string"],
64
+ ["buyTokenBalance", "string"],
65
+ ];
66
+
67
+ // THE STATUS VOCABULARY OF THE ORDER BOOK (OpenAPI: OrderStatus). A status outside this set is not "probably fine":
68
+ // the answer is not the shape we know and it is refused with cow-bad-response.
69
+ export const ASYNC_STATUSES = ["presignaturePending", "open", "fulfilled", "cancelled", "expired"];
70
+ export const ASYNC_FINAL_STATUSES = ["fulfilled", "cancelled", "expired"];
71
+ export const ASYNC_SETTLED_STATUS = "fulfilled";
72
+ export const ASYNC_OPEN_STATUSES = ["presignaturePending", "open"];
73
+
74
+ // THE NAMED REFUSALS. Every one is a VALUE ({ ok: false, code }), never null and never an exception escaping to the
75
+ // caller - the same codes the package's provider uses, so a caller can tell unreachable from refused from not-settled.
76
+ export const ASYNC_REFUSAL_CODES = [
77
+ "cow-unknown-network",
78
+ "cow-bad-order",
79
+ "cow-sign-failed",
80
+ "cow-unreachable",
81
+ "cow-order-unknown",
82
+ "cow-refused",
83
+ "cow-bad-response",
84
+ "cow-not-settled",
85
+ ];
86
+
87
+ const refusal = (code, params = {}) => ({ ok: false, code, params });
88
+
89
+ const isAddress = (v) => /^0x[0-9a-fA-F]{40}$/.test(String(v || ""));
90
+ const isBytes32 = (v) => /^(0x)?[0-9a-fA-F]{64}$/.test(String(v || ""));
91
+ const isUid = (v) => /^0x[0-9a-fA-F]{112}$/.test(String(v || ""));
92
+ const isSignature = (v) => /^0x[0-9a-fA-F]{130}$/.test(String(v || ""));
93
+
94
+ const num = (v) => {
95
+ if (v === null || v === undefined || v === "") return null;
96
+ const s = String(v).trim();
97
+ if (/^0x[0-9a-fA-F]+$/.test(s) || /^[0-9]+$/.test(s)) return s.replace(/^0x/, "");
98
+ return null;
99
+ };
100
+
101
+ // A NAMED REFUSAL IS A VALUE. The caller reads `ok`, and a caller that ignores it would be reading its own lie:
102
+ // so the flow below checks `ok` after EVERY step and stops at the first refusal.
103
+ const joinUrl = (base, path) => String(base || "").replace(/\/+$/, "") + path;
104
+
105
+ /** The order book base URL: the caller's explicit one, else the served network's (evm/asyncRoute.js), else null. */
106
+ export const asyncOrderbookFor = (chainId, orderbook = null, providerId = null) => {
107
+ if (orderbook) return String(orderbook).replace(/\/+$/, "");
108
+ const route = asyncRouteForChain(chainId, providerId);
109
+ return route && route.orderbook ? String(route.orderbook).replace(/\/+$/, "") : null;
110
+ };
111
+
112
+ /** The EIP-712 typed data the wallet signs (eth_signTypedData_v4). The message carries kind/balances as STRINGS,
113
+ * matching the type - the wallet shows them readably and the settlement hashes them (see the package's cow-spec). */
114
+ export const asyncOrderTypedData = ({ order, chainId, settlement = ASYNC_SETTLEMENT }) => ({
115
+ domain: { name: ASYNC_DOMAIN_NAME, version: ASYNC_DOMAIN_VERSION, chainId: Number(chainId), verifyingContract: settlement },
116
+ types: {
117
+ EIP712Domain: [
118
+ { name: "name", type: "string" },
119
+ { name: "version", type: "string" },
120
+ { name: "chainId", type: "uint256" },
121
+ { name: "verifyingContract", type: "address" },
122
+ ],
123
+ Order: ASYNC_ORDER_FIELDS.map(([name, type]) => ({ name, type })),
124
+ },
125
+ primaryType: "Order",
126
+ message: {
127
+ sellToken: order.sellToken,
128
+ buyToken: order.buyToken,
129
+ receiver: order.receiver,
130
+ sellAmount: String(order.sellAmount),
131
+ buyAmount: String(order.buyAmount),
132
+ validTo: Number(order.validTo),
133
+ appData: order.appData,
134
+ feeAmount: String(order.feeAmount),
135
+ kind: String(order.kind),
136
+ partiallyFillable: Boolean(order.partiallyFillable),
137
+ sellTokenBalance: String(order.sellTokenBalance),
138
+ buyTokenBalance: String(order.buyTokenBalance),
139
+ },
140
+ });
141
+
142
+ /**
143
+ * BUILD THE INTENT: from what the person gives, assemble the order and the typed data the wallet will sign.
144
+ * Returns { ok: true, order, typedData } or a named refusal. Pure - no network, no wallet, no DOM.
145
+ *
146
+ * The order book existence is checked FIRST: an unknown network is cow-unknown-network, never a guess at a URL.
147
+ */
148
+ export function asyncIntentVerdict(request = {}) {
149
+ const chainId = Number(request.chainId);
150
+ const orderbook = asyncOrderbookFor(chainId, request.orderbook || null, request.providerId || null);
151
+ if (!orderbook) return refusal("cow-unknown-network", { chainId: Number.isFinite(chainId) ? chainId : null });
152
+ const owner = request.owner;
153
+ const receiver = request.receiver === undefined || request.receiver === null ? owner : request.receiver;
154
+ const order = {
155
+ sellToken: request.sellToken,
156
+ buyToken: request.buyToken,
157
+ receiver,
158
+ sellAmount: num(request.sellAmountWei) === null ? request.sellAmountWei : String(request.sellAmountWei),
159
+ buyAmount: num(request.buyAmountWei) === null ? request.buyAmountWei : String(request.buyAmountWei),
160
+ validTo: request.validTo,
161
+ appData: request.appData === undefined || request.appData === null ? "0x" + "00".repeat(32) : request.appData,
162
+ feeAmount: request.feeAmountWei === undefined || request.feeAmountWei === null ? "0" : String(request.feeAmountWei),
163
+ kind: request.kind === undefined || request.kind === null ? "sell" : request.kind,
164
+ partiallyFillable: request.partiallyFillable === undefined || request.partiallyFillable === null ? false : request.partiallyFillable,
165
+ sellTokenBalance: request.sellTokenBalance === undefined || request.sellTokenBalance === null ? "erc20" : request.sellTokenBalance,
166
+ buyTokenBalance: request.buyTokenBalance === undefined || request.buyTokenBalance === null ? "erc20" : request.buyTokenBalance,
167
+ };
168
+ const missing = [];
169
+ for (const [name] of ASYNC_ORDER_FIELDS) {
170
+ const v = order[name];
171
+ if (v === undefined || v === null || v === "") missing.push(name);
172
+ }
173
+ if (!isAddress(owner)) missing.push("owner");
174
+ if (!isBytes32(order.appData)) missing.push("appData");
175
+ if (!isAddress(order.sellToken) || !isAddress(order.buyToken) || !isAddress(order.receiver)) missing.push("token");
176
+ if (num(order.sellAmount) === null || num(order.buyAmount) === null) missing.push("amount");
177
+ if (num(order.validTo) === null) missing.push("validTo");
178
+ if (!ASYNC_STATUSES.length || !["sell", "buy"].includes(order.kind)) missing.push("kind");
179
+ if (missing.length) return { ...refusal("cow-bad-order", { missing: [...new Set(missing)] }), orderbook };
180
+ return {
181
+ ok: true,
182
+ chainId: Number.isFinite(chainId) ? chainId : null,
183
+ orderbook,
184
+ owner,
185
+ order,
186
+ typedData: asyncOrderTypedData({ order, chainId, settlement: request.settlement || ASYNC_SETTLEMENT }),
187
+ };
188
+ }
189
+
190
+ /**
191
+ * SIGN THE INTENT THROUGH THE WALLET. The adapter's own signTypedData when present, otherwise eth_signTypedData_v4
192
+ * through its EIP-1193 driver (or directly, when the caller hands the provider itself). A wallet that will not sign
193
+ * is cow-sign-failed, not a crash.
194
+ */
195
+ export async function asyncSignVerdict({ typedData, wallet = null, address = null } = {}) {
196
+ if (!typedData || typeof typedData !== "object") return refusal("cow-bad-order", { field: "typedData" });
197
+ try {
198
+ if (wallet && typeof wallet.signTypedData === "function") {
199
+ const signature = await wallet.signTypedData(typedData);
200
+ if (!isSignature(signature)) return refusal("cow-sign-failed", { why: "bad-signature-shape" });
201
+ return { ok: true, signature };
202
+ }
203
+ const driver = wallet && typeof wallet.driver === "function" ? wallet.driver() : wallet;
204
+ if (driver && typeof driver.request === "function") {
205
+ const from = address || (wallet && typeof wallet.address === "function" ? await wallet.address() : null);
206
+ const signature = await driver.request({ method: "eth_signTypedData_v4", params: [from, JSON.stringify(typedData)] });
207
+ if (!isSignature(signature)) return refusal("cow-sign-failed", { why: "bad-signature-shape" });
208
+ return { ok: true, signature };
209
+ }
210
+ return refusal("cow-sign-failed", { why: "no-wallet" });
211
+ } catch (error) {
212
+ return refusal("cow-sign-failed", { why: String((error && error.code) || (error && error.message) || error) });
213
+ }
214
+ }
215
+
216
+ // THE PAYLOAD THE ORDER BOOK EXPECTS (createOrder / OrderCreation). Every field is set EXPLICITLY - no relying on a
217
+ // server default that could contradict the signed order: the signed fields must travel exactly as signed.
218
+ export const asyncOrderSubmission = ({ order, signature, owner, signingScheme = "eip712", from = null }) => ({
219
+ sellToken: order.sellToken,
220
+ buyToken: order.buyToken,
221
+ receiver: order.receiver,
222
+ sellAmount: String(order.sellAmount),
223
+ buyAmount: String(order.buyAmount),
224
+ validTo: Number(order.validTo),
225
+ appData: String(order.appData),
226
+ feeAmount: String(order.feeAmount),
227
+ kind: String(order.kind),
228
+ partiallyFillable: Boolean(order.partiallyFillable),
229
+ sellTokenBalance: String(order.sellTokenBalance),
230
+ buyTokenBalance: String(order.buyTokenBalance),
231
+ signingScheme,
232
+ signature,
233
+ from: from || owner,
234
+ });
235
+
236
+ // ONE JSON REQUEST. The transport is the caller's (the page's fetch, or a stand-in in a check); a thrown fetch is
237
+ // cow-unreachable, a body we cannot read is cow-bad-response - never silence.
238
+ async function ask({ fetchImpl, url, method = "GET", body = null, timeoutMs = 20_000 }) {
239
+ const doFetch = fetchImpl || (typeof fetch === "function" ? fetch : null);
240
+ if (!doFetch) return refusal("cow-unreachable", { why: "no-fetch" });
241
+ let timer = null;
242
+ try {
243
+ const options = { method, headers: body === null ? undefined : { "content-type": "application/json" } };
244
+ if (body !== null) options.body = JSON.stringify(body);
245
+ const signal = typeof AbortController === "function" ? new AbortController() : null;
246
+ if (signal) { options.signal = signal.signal; timer = setTimeout(() => signal.abort(), timeoutMs); }
247
+ const res = await doFetch(url, options);
248
+ let text = null;
249
+ try { text = await res.text(); } catch { text = null; }
250
+ return { ok: true, status: Number(res.status), text };
251
+ } catch (error) {
252
+ return refusal("cow-unreachable", { why: String((error && error.message) || error) });
253
+ } finally {
254
+ if (timer) clearTimeout(timer);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * SUBMIT THE SIGNED ORDER. POST /api/v1/orders -> 201 with the order UID (56 bytes). Every failure is a named
260
+ * refusal: unreachable, refused (the book answered and did not accept), or an answer that is not the shape we know.
261
+ */
262
+ export async function asyncSubmitVerdict({ orderbook, order, signature, owner, fetchImpl = null, timeoutMs = 20_000, signingScheme = "eip712", from = null } = {}) {
263
+ const base = orderbook ? String(orderbook).replace(/\/+$/, "") : null;
264
+ if (!base) return refusal("cow-unknown-network", {});
265
+ if (!isSignature(signature)) return refusal("cow-bad-order", { field: "signature" });
266
+ const res = await ask({ fetchImpl, url: joinUrl(base, ASYNC_SUBMIT_PATH), method: "POST", body: asyncOrderSubmission({ order, signature, owner, signingScheme, from }), timeoutMs });
267
+ if (!res.ok) return res;
268
+ if (res.status >= 400) return refusal("cow-refused", { status: res.status, reason: null });
269
+ if (typeof res.text !== "string" || !isUid(res.text.trim())) return refusal("cow-bad-response", { where: "submit" });
270
+ return { ok: true, uid: res.text.trim() };
271
+ }
272
+
273
+ /**
274
+ * READ ONE ORDER. GET /api/v1/orders/{UID} -> the order, with `status`. A 404 is cow-order-unknown; a body whose
275
+ * status is not a known status word is cow-bad-response - never silence, never null.
276
+ */
277
+ export async function asyncReadVerdict({ orderbook, uid, fetchImpl = null, timeoutMs = 20_000 } = {}) {
278
+ const base = orderbook ? String(orderbook).replace(/\/+$/, "") : null;
279
+ if (!base) return refusal("cow-unknown-network", {});
280
+ if (!isUid(uid)) return refusal("cow-bad-order", { field: "uid" });
281
+ const res = await ask({ fetchImpl, url: joinUrl(base, ASYNC_ORDER_PATH + uid), timeoutMs });
282
+ if (!res.ok) return res;
283
+ if (res.status === 404) return refusal("cow-order-unknown", { uid });
284
+ if (res.status >= 400) return refusal("cow-refused", { status: res.status });
285
+ let body = null;
286
+ try { body = JSON.parse(res.text); } catch { body = null; }
287
+ const status = body && body.status;
288
+ if (typeof status !== "string" || !ASYNC_STATUSES.includes(status)) return refusal("cow-bad-response", { where: "status", got: typeof status === "string" ? status : null });
289
+ return { ok: true, status, order: body };
290
+ }
291
+
292
+ /**
293
+ * WHAT ENDS AN ASYNCHRONOUS EXECUTION - poll the order book until the order is settled or finally dead, with a
294
+ * timeout. Returns:
295
+ * { ok: true, done: true, settled: true, status: "fulfilled" } - the swap settled;
296
+ * { ok: true, done: true, settled: false, status: "cancelled" | "expired" } - the order is finally dead;
297
+ * { ok: false, code: "cow-not-settled" | "cow-unreachable" | ... } - a NAMED refusal.
298
+ * "Not settled in time" is its own code: "we did not wait long enough" is a state of the order, not a breakdown.
299
+ * The clock and the sleep are injectable, so the wait is testable without real time.
300
+ */
301
+ export async function asyncSettledVerdict({ orderbook, uid, fetchImpl = null, pollMs = 5_000, timeoutMs = 300_000, now = null, sleep = null, onTick = null } = {}) {
302
+ const clock = typeof now === "function" ? now : () => Date.now();
303
+ const pause = typeof sleep === "function" ? sleep : (ms) => new Promise((r) => setTimeout(r, ms));
304
+ const step = Number.isFinite(pollMs) && pollMs > 0 ? pollMs : 5_000;
305
+ const limit = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 300_000;
306
+ const startedAt = clock();
307
+ let lastStatus = null;
308
+ for (;;) {
309
+ const read = await asyncReadVerdict({ orderbook, uid, fetchImpl });
310
+ if (!read.ok) return read;
311
+ lastStatus = read.status;
312
+ if (typeof onTick === "function") onTick(lastStatus);
313
+ if (ASYNC_FINAL_STATUSES.includes(lastStatus)) {
314
+ return { ok: true, done: true, settled: lastStatus === ASYNC_SETTLED_STATUS, status: lastStatus, uid };
315
+ }
316
+ if (clock() - startedAt >= limit) return { ...refusal("cow-not-settled", { uid, status: lastStatus, waitedMs: clock() - startedAt }), status: lastStatus, uid };
317
+ await pause(step);
318
+ }
319
+ }
320
+
321
+ // THE WORDS THE SCREEN SHOWS, IN ONE PLACE. English, and they carry the state rather than a promise: the person must
322
+ // see the swap, the wait for settlement and the funding as separate steps, and a refusal must reach the screen with
323
+ // its own name. The screen renders these; the engine never touches the DOM.
324
+ export function asyncStageWords(stage, detail = "") {
325
+ const line = String(detail || "").trim();
326
+ const base = {
327
+ "async-plan": "Building the swap intent",
328
+ "async-sign": "Waiting for the wallet to sign the swap intent",
329
+ "async-submit": "Submitting the swap intent to the order book",
330
+ "async-wait": "Waiting for the swap to settle",
331
+ "async-settled": "Swap settled - now funding the escrow",
332
+ // THE THREE STEPS OF A COMPOSED ROUTE, NAMED AS THREE: the swap (liquidity leg, token -> WETH), the conversion
333
+ // to native (the native leg, WETH -> native coin), and the funding. Each is its own word, so the screen shows a
334
+ // person what they are going through and not a single opaque "processing".
335
+ "async-swap": "Swapping your token for WETH",
336
+ "async-convert": "Converting WETH into native ETH",
337
+ "async-fund": "Funding the escrow with the settled native ETH",
338
+ }[stage] || null;
339
+ return base ? base + (line ? ": " + line : "") : (line || String(stage || ""));
340
+ }
341
+
342
+ // THE REFUSAL WORDS FOR THE SCREEN - by CODE, so a refusal is never an empty toast. Unknown codes are shown as-is.
343
+ export const asyncRefusalWords = (result) => {
344
+ const code = result && result.code ? String(result.code) : null;
345
+ const map = {
346
+ "cow-unknown-network": "no asynchronous provider serves this network",
347
+ "cow-bad-order": "the swap intent is incomplete and was not sent",
348
+ "cow-sign-failed": "the wallet did not sign the swap intent",
349
+ "cow-unreachable": "the order book could not be reached",
350
+ "cow-order-unknown": "the order book does not know this order",
351
+ "cow-refused": "the order book did not accept the order",
352
+ "cow-bad-response": "the order book answered in a shape we do not know",
353
+ "cow-not-settled": "the swap did not settle in time",
354
+ "async-order-dead": "the swap order was cancelled or expired - nothing was funded",
355
+ // THE LIQUIDITY LEG'S OWN REFUSALS (KyberSwap Limit Order), named so a failure there never looks like a failure
356
+ // of the native leg.
357
+ "kyber-unknown-network": "no KyberSwap liquidity provider serves this network",
358
+ "kyber-bad-order": "the liquidity swap intent is incomplete and was not sent",
359
+ "kyber-sign-failed": "the wallet did not sign the liquidity swap intent",
360
+ "kyber-unreachable": "the KyberSwap order book could not be reached",
361
+ "kyber-order-unknown": "the KyberSwap order book does not know this order",
362
+ "kyber-refused": "the KyberSwap order book did not accept the order",
363
+ "kyber-bad-response": "the KyberSwap order book answered in a shape we do not know",
364
+ "kyber-not-settled": "the liquidity swap did not settle in time",
365
+ // THE COMPOSITION'S OWN REFUSALS.
366
+ "async-bad-composition": "this route is not a two-leg composition (a liquidity leg and a native leg are both required)",
367
+ "composition-short": "the swap does not cover the escrow deposit, its reserve and the conversion fee",
368
+ };
369
+ return map[code] || (code ? code : "the asynchronous route stopped");
370
+ };
371
+
372
+ // ------------------------------------------------------------------------------------------------
373
+ // THE COMPOSITION: TWO ASYNCHRONOUS LEGS, IN ORDER, THEN THE ORDINARY FUNDING.
374
+ //
375
+ // WHY TWO LEGS. The escrow is funded with NATIVE coin, and not every asynchronous provider can deliver it. KyberSwap
376
+ // Limit Order settles the WRAPPED native (WETH) only, so it cannot be the last mile; CoWSwap can buy native directly
377
+ // (the BUY_ETH_ADDRESS marker). So when a person needs native and only a wrapped (liquidity) leg is available, the
378
+ // route is COMPOSED: leg one turns the token into WETH (KyberSwap), leg two turns that WETH into native coin
379
+ // (CoWSwap), and only then is the escrow funded. Both legs must SETTLE, in order - a failure in either leg stops the
380
+ // flow with a NAMED refusal and NEVER reaches the funding step.
381
+ //
382
+ // THE COVERAGE IS CHECKED BEFORE ANYTHING IS SIGNED. The conversion is not free: the second leg takes its own fee out
383
+ // of the WETH. So the money the person receives is the first leg's output minus that fee, and it must still cover the
384
+ // escrow deposit and the reserve (mark + refund gas) they must keep. `compositionVerdict` below is that check, and
385
+ // the composition is refused (composition-short) rather than entered when it would leave the person short.
386
+
387
+ // A quantity as BigInt, or null. An unread value is null ("not measured"), never zero.
388
+ const toBig = (v) => {
389
+ if (v === null || v === undefined || v === "") return null;
390
+ try { return BigInt(typeof v === "bigint" ? v : String(v)); } catch { return null; }
391
+ };
392
+
393
+ /**
394
+ * WHETHER A COMPOSED ROUTE COVERS THE DEPOSIT - a pure VALUE, no network and no wallet.
395
+ *
396
+ * The liquidity leg gives `wethOutWei` (gross). The native leg keeps `leg2FeeWei` of it as its own fee, so the person
397
+ * ends with `wethOutWei - leg2FeeWei` native. That must cover the escrow deposit (`depositWei`) PLUS the reserve the
398
+ * person must keep for the mark and the refund (`reserveWei`). Equivalently the gross WETH must cover
399
+ * deposit + reserve + leg2Fee - i.e. the reserve ACCOUNTS FOR the extra fee the second leg costs.
400
+ * { ok: true, wethOutWei, nativeOutWei, needWei, ... } - the composition covers the deposit;
401
+ * { ok: false, code: "composition-short", shortfallWei, ... } - it would leave the person short: refuse.
402
+ * An unmeasured amount is a refusal too (measured: false), never a silent pass.
403
+ */
404
+ export function compositionVerdict({ wethOutWei = null, leg2FeeWei = "0", depositWei = null, reserveWei = "0" } = {}) {
405
+ const gross = toBig(wethOutWei);
406
+ const fee = leg2FeeWei === null || leg2FeeWei === undefined || leg2FeeWei === "" ? 0n : toBig(leg2FeeWei);
407
+ const deposit = toBig(depositWei);
408
+ const reserve = reserveWei === null || reserveWei === undefined || reserveWei === "" ? 0n : toBig(reserveWei);
409
+ if (gross === null || fee === null || fee < 0n || deposit === null || reserve === null || reserve < 0n) {
410
+ return { ok: false, code: "composition-short", measured: false, reason: "the composition's amounts or the deposit were not measured" };
411
+ }
412
+ // THE GROSS WETH THE LIQUIDITY LEG MUST GIVE: the deposit, the reserve the person keeps, and the second leg's fee.
413
+ const needWei = deposit + reserve + fee;
414
+ const nativeOutWei = gross - fee;
415
+ if (gross < needWei) {
416
+ return {
417
+ ok: false, code: "composition-short", measured: true,
418
+ wethOutWei: gross.toString(), leg2FeeWei: fee.toString(), depositWei: deposit.toString(), reserveWei: reserve.toString(),
419
+ nativeOutWei: (nativeOutWei > 0n ? nativeOutWei : 0n).toString(), needWei: needWei.toString(), shortfallWei: (needWei - gross).toString(),
420
+ reason: "the liquidity leg does not cover the escrow deposit, its reserve and the conversion fee",
421
+ };
422
+ }
423
+ return {
424
+ ok: true, measured: true, wethOutWei: gross.toString(), leg2FeeWei: fee.toString(), nativeOutWei: nativeOutWei.toString(),
425
+ needWei: needWei.toString(), depositWei: deposit.toString(), reserveWei: reserve.toString(),
426
+ };
427
+ }
428
+
429
+ /**
430
+ * RUN A COMPOSED ROUTE: the liquidity leg (KyberSwap, token -> WETH), then the native leg (CoWSwap, WETH -> native),
431
+ * then - and only then - the ordinary funding. The caller supplies:
432
+ * route - a composition verdict from evm/asyncRoute.js ({ kind: "composition", chainId, legs: [liquidity, native] });
433
+ * request - { chainId, owner, sellToken, sellAmountWei, wrappedToken, wethOutWei, leg2FeeWei, validTo,
434
+ * expiredAt, depositWei, reserveWei };
435
+ * wallet - the page wallet (adapter or EIP-1193 provider) - it signs BOTH intents;
436
+ * fund - async () => {...} the ordinary escrow funding, called ONCE and only after BOTH legs settled;
437
+ * onStep - (stage, detail, words) -> the screen shows asyncStageWords(stage, detail).
438
+ * Returns { ok: true, kind: "composition", legs: [{provider, id, status}], funded } or { ok: false, stage, code, leg? }.
439
+ */
440
+ export async function runComposedRoute({
441
+ route = {}, request = {}, wallet = null, fund = null,
442
+ fetchImpl = null, pollMs = 5_000, timeoutMs = 300_000, now = null, sleep = null, onStep = null,
443
+ } = {}) {
444
+ const say = (stage, detail) => { if (typeof onStep === "function") onStep(stage, detail, asyncStageWords(stage, detail)); };
445
+ const legs = Array.isArray(route.legs) ? route.legs : [];
446
+ const liquidity = legs.find((l) => l && l.settles === "wrapped") || null;
447
+ const native = legs.find((l) => l && l.settles === "native") || null;
448
+ if (!liquidity || !native) {
449
+ const bad = { ok: false, code: "async-bad-composition", params: { legs: legs.map((l) => l && l.settles) } };
450
+ return { ok: false, stage: "plan", ...bad, words: asyncRefusalWords(bad) };
451
+ }
452
+ const chainId = route.chainId !== undefined && route.chainId !== null ? route.chainId : request.chainId;
453
+ const owner = request.owner;
454
+ const wrapped = request.wrappedToken;
455
+ const leg2FeeWei = request.leg2FeeWei === undefined || request.leg2FeeWei === null ? "0" : request.leg2FeeWei;
456
+
457
+ say("async-plan", "token -> WETH -> native coin");
458
+ // THE COVERAGE CHECK FIRST: never enter a composition that cannot cover the deposit plus the reserve plus the fee.
459
+ const cover = compositionVerdict({ wethOutWei: request.wethOutWei, leg2FeeWei, depositWei: request.depositWei, reserveWei: request.reserveWei });
460
+ if (!cover.ok) return { ok: false, stage: "plan", ...cover, words: asyncRefusalWords(cover) };
461
+ const wethOutWei = cover.wethOutWei;
462
+
463
+ // ---- LEG 1: THE LIQUIDITY LEG (KyberSwap Limit Order) - token -> WETH ------------------------------------
464
+ const book1 = liquidity.orderbook || null;
465
+ const plan1 = kyberLegPlan({
466
+ chainId, maker: owner, receiver: owner,
467
+ makerAsset: request.sellToken, takerAsset: wrapped,
468
+ makingAmount: request.sellAmountWei, takingAmount: wethOutWei, expiredAt: request.expiredAt,
469
+ });
470
+ if (!plan1.ok) return { ok: false, stage: "swap", leg: "liquidity", ...plan1, words: asyncRefusalWords(plan1) };
471
+ say("async-swap", "building the liquidity intent");
472
+ const unsigned1 = await kyberLegUnsigned({ params: plan1.params, chainId, orderbook: book1, fetchImpl });
473
+ if (!unsigned1.ok) return { ok: false, stage: "swap", leg: "liquidity", ...unsigned1, words: asyncRefusalWords(unsigned1) };
474
+ say("async-swap", "waiting for the wallet to sign the swap intent");
475
+ const sig1 = await kyberLegSign({ typedData: unsigned1.typedData, wallet, address: owner });
476
+ if (!sig1.ok) return { ok: false, stage: "swap", leg: "liquidity", ...sig1, words: asyncRefusalWords(sig1) };
477
+ say("async-swap", "submitting the liquidity order");
478
+ const sub1 = await kyberLegSubmit({ params: plan1.params, salt: unsigned1.salt, signature: sig1.signature, orderbook: book1, chainId, fetchImpl });
479
+ if (!sub1.ok) return { ok: false, stage: "swap", leg: "liquidity", ...sub1, words: asyncRefusalWords(sub1) };
480
+ say("async-swap", "order " + sub1.id);
481
+ const settled1 = await kyberLegSettled({ id: sub1.id, chainId, maker: owner, orderbook: book1, fetchImpl, pollMs, timeoutMs, now, sleep,
482
+ onTick: (status) => say("async-swap", "status " + status) });
483
+ if (!settled1.ok) return { ok: false, stage: "swap", leg: "liquidity", ...settled1, words: asyncRefusalWords(settled1) };
484
+ if (!settled1.settled) {
485
+ const dead = { ok: false, code: "async-order-dead", params: { leg: "liquidity", id: sub1.id, status: settled1.status } };
486
+ return { ok: false, stage: "swap", leg: "liquidity", ...dead, words: asyncRefusalWords(dead) };
487
+ }
488
+ const leg1 = { provider: liquidity.id, id: sub1.id, status: settled1.status };
489
+
490
+ // ---- LEG 2: THE NATIVE LEG (CoWSwap) - WETH -> native coin -------------------------------------------------
491
+ say("async-convert", "building the conversion order");
492
+ const intent2 = asyncIntentVerdict({
493
+ chainId, orderbook: native.orderbook || null, owner,
494
+ sellToken: wrapped, buyToken: ASYNC_BUY_ETH_ADDRESS, receiver: owner,
495
+ sellAmountWei: wethOutWei, buyAmountWei: cover.nativeOutWei, validTo: request.validTo,
496
+ feeAmountWei: cover.leg2FeeWei, kind: "sell",
497
+ });
498
+ if (!intent2.ok) return { ok: false, stage: "convert", leg: "native", ...intent2, words: asyncRefusalWords(intent2) };
499
+ say("async-convert", "waiting for the wallet to sign the conversion intent");
500
+ const sig2 = await asyncSignVerdict({ typedData: intent2.typedData, wallet, address: owner });
501
+ if (!sig2.ok) return { ok: false, stage: "convert", leg: "native", ...sig2, words: asyncRefusalWords(sig2) };
502
+ say("async-convert", "submitting the conversion order");
503
+ const sub2 = await asyncSubmitVerdict({ orderbook: intent2.orderbook, order: intent2.order, signature: sig2.signature, owner, fetchImpl });
504
+ if (!sub2.ok) return { ok: false, stage: "convert", leg: "native", ...sub2, words: asyncRefusalWords(sub2) };
505
+ say("async-convert", "order " + String(sub2.uid).slice(0, 12));
506
+ const settled2 = await asyncSettledVerdict({ orderbook: intent2.orderbook, uid: sub2.uid, fetchImpl, pollMs, timeoutMs, now, sleep,
507
+ onTick: (status) => say("async-convert", "status " + status) });
508
+ if (!settled2.ok) return { ok: false, stage: "convert", leg: "native", ...settled2, words: asyncRefusalWords(settled2) };
509
+ if (!settled2.settled) {
510
+ const dead = { ok: false, code: "async-order-dead", params: { leg: "native", uid: sub2.uid, status: settled2.status } };
511
+ return { ok: false, stage: "convert", leg: "native", ...dead, words: asyncRefusalWords(dead) };
512
+ }
513
+ const leg2 = { provider: native.id, uid: sub2.uid, status: settled2.status };
514
+
515
+ // ---- AND ONLY NOW THE ORDINARY FUNDING ---------------------------------------------------------------------
516
+ say("async-fund", "funding the escrow with the converted native coin");
517
+ if (typeof fund !== "function") return { ok: false, stage: "fund", code: "async-no-fund", legs: [leg1, leg2], words: asyncRefusalWords({ code: "async-no-fund" }) };
518
+ const funded = await fund({ kind: "composition", uid: sub2.uid, status: settled2.status, nativeOutWei: cover.nativeOutWei, liquidity: leg1, native: leg2 });
519
+ return { ok: true, kind: "composition", uid: sub2.uid, status: settled2.status, legs: [leg1, leg2], nativeOutWei: cover.nativeOutWei, funded };
520
+ }
521
+
522
+ /**
523
+ * RUN THE ASYNCHRONOUS ROUTE, THEN THE ORDINARY FUNDING - AND ONLY THEN.
524
+ *
525
+ * The order is the whole point: plan the intent, sign it, submit it, WAIT until the order book says the swap
526
+ * SETTLED, and only after a settled result call `fund()`. If anything does not settle, `fund` is NOT called and the
527
+ * caller gets a named refusal: a funded deal with no swap behind it is exactly the failure this ordering forbids.
528
+ *
529
+ * The caller supplies:
530
+ * route - { chainId, orderbook?, providerId? } (the order book from the table or an explicit stand-in);
531
+ * request - the intent fields (owner, sellToken, buyToken, sellAmountWei, buyAmountWei, validTo, ...);
532
+ * wallet - the page wallet (adapter or EIP-1193 provider);
533
+ * fund - async () => {...} the ordinary escrow funding, called ONCE and only after settlement;
534
+ * onStep - (stage, detail) -> the screen shows asyncStageWords(stage, detail).
535
+ * Returns { ok: true, uid, status, funded } on settlement, or { ok: false, stage, code, ... } on a refusal.
536
+ */
537
+ export async function runAsyncRoute({
538
+ route = {}, request = {}, wallet = null, fund = null,
539
+ fetchImpl = null, pollMs = 5_000, timeoutMs = 300_000, now = null, sleep = null, onStep = null,
540
+ } = {}) {
541
+ const say = (stage, detail) => { if (typeof onStep === "function") onStep(stage, detail, asyncStageWords(stage, detail)); };
542
+ // A COMPOSED ROUTE TAKES A DIFFERENT DRIVER: two legs (liquidity + native), each settled in turn, then the funding.
543
+ if (route && (route.kind === "composition" || (Array.isArray(route.legs) && route.legs.length > 1))) {
544
+ return await runComposedRoute({ route, request, wallet, fund, fetchImpl, pollMs, timeoutMs, now, sleep, onStep });
545
+ }
546
+ const chainId = route.chainId !== undefined && route.chainId !== null ? route.chainId : request.chainId;
547
+ const orderbook = asyncOrderbookFor(chainId, route.orderbook || request.orderbook || null, route.providerId || request.providerId || null);
548
+
549
+ say("async-plan", "");
550
+ const intent = asyncIntentVerdict({ ...request, chainId, orderbook });
551
+ if (!intent.ok) return { ok: false, stage: "plan", ...intent, words: asyncRefusalWords(intent) };
552
+
553
+ say("async-sign", "");
554
+ const signed = await asyncSignVerdict({ typedData: intent.typedData, wallet, address: request.owner });
555
+ if (!signed.ok) return { ok: false, stage: "sign", ...signed, words: asyncRefusalWords(signed) };
556
+
557
+ say("async-submit", "");
558
+ const submitted = await asyncSubmitVerdict({ orderbook: intent.orderbook, order: intent.order, signature: signed.signature, owner: request.owner, fetchImpl });
559
+ if (!submitted.ok) return { ok: false, stage: "submit", ...submitted, words: asyncRefusalWords(submitted) };
560
+
561
+ say("async-wait", "order " + String(submitted.uid).slice(0, 12));
562
+ const settled = await asyncSettledVerdict({
563
+ orderbook: intent.orderbook, uid: submitted.uid, fetchImpl, pollMs, timeoutMs, now, sleep,
564
+ onTick: (status) => say("async-wait", "status " + status),
565
+ });
566
+ if (!settled.ok) return { ok: false, stage: "settle", uid: submitted.uid, ...settled, words: asyncRefusalWords(settled) };
567
+ if (!settled.settled) {
568
+ const dead = { ok: false, code: "async-order-dead", params: { uid: submitted.uid, status: settled.status } };
569
+ return { ok: false, stage: "settle", uid: submitted.uid, ...dead, words: asyncRefusalWords(dead) };
570
+ }
571
+
572
+ say("async-settled", "");
573
+ if (typeof fund !== "function") return { ok: false, stage: "fund", code: "async-no-fund", uid: submitted.uid, words: asyncRefusalWords({ code: "async-no-fund" }) };
574
+ say("async-fund", "");
575
+ const funded = await fund({ uid: submitted.uid, status: settled.status, order: intent.order });
576
+ return { ok: true, uid: submitted.uid, status: settled.status, funded };
577
+ }