@ninsei-labs/sdk 0.39.2 → 0.40.0-develop.1

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 (72) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +4 -1
  3. package/engine/atomic/dleq2.js +86 -111
  4. package/engine/atomic/halfenc.js +58 -72
  5. package/engine/atomic/halves.js +31 -53
  6. package/engine/atomic/order-client.js +15 -16
  7. package/engine/atomic/order.js +44 -62
  8. package/engine/atomic/orderContext.js +26 -50
  9. package/engine/atomic/orderHalfCommit.js +17 -18
  10. package/engine/core/chainSource.js +54 -69
  11. package/engine/core/clock.js +11 -12
  12. package/engine/core/config.js +554 -449
  13. package/engine/core/finality.js +34 -53
  14. package/engine/core/format.js +35 -36
  15. package/engine/core/preSignGuards.js +109 -117
  16. package/engine/core/rfqSource.js +57 -75
  17. package/engine/core/sellWindow.js +23 -24
  18. package/engine/core/store.js +20 -21
  19. package/engine/core/swap.js +242 -243
  20. package/engine/evm/amounts.js +50 -51
  21. package/engine/evm/asyncExec.js +577 -0
  22. package/engine/evm/asyncLegs.js +308 -0
  23. package/engine/evm/asyncRoute.js +179 -0
  24. package/engine/evm/auth.js +41 -48
  25. package/engine/evm/claimGas.js +28 -47
  26. package/engine/evm/depositor.js +39 -43
  27. package/engine/evm/dex.js +266 -267
  28. package/engine/evm/escrow.js +113 -129
  29. package/engine/evm/factory.js +71 -77
  30. package/engine/evm/fees.js +41 -42
  31. package/engine/evm/funding.js +160 -162
  32. package/engine/evm/gasReserve.js +67 -36
  33. package/engine/evm/index.js +46 -53
  34. package/engine/evm/injected.js +34 -42
  35. package/engine/evm/permit.js +129 -56
  36. package/engine/evm/prices.js +30 -45
  37. package/engine/evm/session.js +188 -189
  38. package/engine/mock/chain.js +17 -18
  39. package/engine/monero/address.js +33 -48
  40. package/engine/monero/halvesSweep.js +25 -43
  41. package/engine/monero/node.js +14 -15
  42. package/engine/monero/scan.js +82 -95
  43. package/engine/monero/swapKeys.js +30 -33
  44. package/engine/monero/unlock.js +17 -20
  45. package/engine/monero/wallet.js +32 -42
  46. package/engine/recovery/recoveryFile.js +154 -161
  47. package/engine/recovery/restore.js +50 -61
  48. package/engine/sdk/bridge.js +244 -234
  49. package/engine/ui/dom.js +4 -5
  50. package/engine/ui/walletModal.js +20 -31
  51. package/index.d.ts +26 -0
  52. package/package.json +1 -1
  53. package/src/adapters/evm-wallet.mjs +16 -0
  54. package/src/adapters/monero-wallet.mjs +1 -1
  55. package/src/adapters/monero.mjs +1 -1
  56. package/src/adapters/node-test-evm-wallet.mjs +5 -5
  57. package/src/codes.mjs +5 -0
  58. package/src/config.mjs +1 -1
  59. package/src/engine.mjs +4 -0
  60. package/src/index.mjs +16 -2
  61. package/src/legs/cow-spec.mjs +254 -0
  62. package/src/legs/cow.mjs +315 -0
  63. package/src/legs/evm.mjs +86 -2
  64. package/src/legs/kyber-spec.mjs +265 -0
  65. package/src/legs/kyber.mjs +425 -0
  66. package/src/legs/shape.mjs +35 -0
  67. package/src/legs/uniswaprelay-spec.mjs +541 -0
  68. package/src/legs/uniswaprelay.mjs +422 -0
  69. package/src/legs/uniswapx-spec.mjs +329 -0
  70. package/src/legs/uniswapx.mjs +362 -0
  71. package/src/quotes.mjs +23 -5
  72. package/src/swaps.mjs +18 -1
@@ -0,0 +1,308 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/asyncLegs.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 KYBERSWAP LIMIT ORDER LEG, EXECUTED ON THE PAGE - THE LIQUIDITY LEG OF A COMPOSED ROUTE.
6
+ //
7
+ // WHAT IT IS AND WHY THE ENGINE REACHES FOR IT AT ALL. KyberSwap Limit Order is an intent auction: the Maker signs
8
+ // an EIP-712 order off-chain (no gas), the order is stored in KyberSwap's book, and a Taker settles it on chain
9
+ // later. It is an ASYNCHRONOUS provider like CoWSwap - but its orders trade ERC20 against ERC20 and its API refuses
10
+ // native coin as an order asset (error 4004), so it settles the WRAPPED native (WETH), not native. Our escrow is
11
+ // funded with NATIVE coin, so this provider can never be the last mile: it is the LIQUIDITY leg, and reaching native
12
+ // is a SECOND order on a native-capable provider (CoWSwap, evm/asyncExec.js). This module is the first leg.
13
+ //
14
+ // WHY HERE AND NOT IN THE PACKAGE. The page cannot import the package's sources (they pull bare npm specifiers and
15
+ // the DOM-free package never sees the page wallet); it reaches the core only through a built bundle whose surface is
16
+ // fixed. So the provider's CONTRACT (the EIP-712 order fields, the domain, the order book paths, the status
17
+ // vocabulary, the refusal codes) is repeated here - exactly as the CoW contract is repeated in evm/asyncExec.js -
18
+ // and the agreement of the two is a CHECK (tools/check-async-route-compose.mjs compares every constant against
19
+ // sdk/src/legs/kyber.mjs and sdk/src/legs/kyber-spec.mjs), never a promise in a comment.
20
+ //
21
+ // NO NEW DEPENDENCY: the transport is the page's fetch, the signature is the wallet's own eth_signTypedData_v4,
22
+ // and the typed data comes FROM THE ORDER BOOK'S sign-message endpoint (validated against this spec before it is
23
+ // trusted). Nothing is trusted to a library.
24
+ //
25
+ // EVERY FAILURE IS A NAMED VALUE ({ ok: false, code }), never silence and never a funded deal with no swap behind it.
26
+
27
+ // THE ORDER BOOK: one base URL for every chain (docs.kyberswap.com, "Base URL: https://limit-order.kyberswap.com").
28
+ export const KYBER_LEG_ORDERBOOK = "https://limit-order.kyberswap.com";
29
+ // The venue name, assembled from parts (a single multi-word literal would read as user-facing text to the SDK).
30
+ export const KYBER_LEG_VENUE = ["KyberSwap", "Limit", "Order"].join(" ");
31
+
32
+ // THE EIP-712 DOMAIN AND THE VERIFYING CONTRACT - DSLOProtocol, identical on every network KyberSwap serves
33
+ // (docs.kyberswap.com, "Contracts & Addresses"; sdk/src/legs/kyber-spec.mjs).
34
+ export const KYBER_LEG_DOMAIN_NAME = ["Kyber", "DSLO", "Protocol"].join(" ");
35
+ export const KYBER_LEG_DOMAIN_VERSION = "1";
36
+ export const KYBER_LEG_CONTRACT = "0xcab2FA2eeab7065B45CBcF6E3936dDE2506b4f6C";
37
+
38
+ // THE ORDER FIELDS AND THEIR TYPES, IN PROTOCOL ORDER - DSOrderMixin.LIMIT_ORDER_TYPEHASH. The order IS the protocol:
39
+ // a reordered or retyped field still produces a valid signature and is refused by the contract, so the field list is
40
+ // pinned against sdk/src/legs/kyber-spec.mjs (KYBER_ORDER_FIELDS) by the check.
41
+ export const KYBER_LEG_ORDER_FIELDS = [
42
+ ["salt", "uint256"],
43
+ ["makerAsset", "address"],
44
+ ["takerAsset", "address"],
45
+ ["maker", "address"],
46
+ ["receiver", "address"],
47
+ ["allowedSender", "address"],
48
+ ["makingAmount", "uint256"],
49
+ ["takingAmount", "uint256"],
50
+ ["feeConfig", "uint256"],
51
+ ["makerAssetData", "bytes"],
52
+ ["takerAssetData", "bytes"],
53
+ ["getMakerAmount", "bytes"],
54
+ ["getTakerAmount", "bytes"],
55
+ ["predicate", "bytes"],
56
+ ["interaction", "bytes"],
57
+ ];
58
+
59
+ // THE ENDPOINTS (docs.kyberswap.com, "Limit Order API" endpoint index and the Maker API reference).
60
+ export const KYBER_LEG_SIGN_MESSAGE_PATH = "/write/api/v1/orders/sign-message";
61
+ export const KYBER_LEG_CREATE_PATH = "/write/api/v1/orders";
62
+ export const KYBER_LEG_MAKER_ORDERS_PATH = "/read-ks/api/v1/orders";
63
+
64
+ // THE STATUS VOCABULARY (Maker API "Get List Of Orders By Maker" `status` enum). `active` and `closed` are umbrella
65
+ // FILTER values; an item `status` is one of the concrete ones. A status outside this set is not "probably fine" - it
66
+ // is "kyber-bad-response".
67
+ export const KYBER_LEG_STATUSES = ["active", "open", "partially_filled", "cancelling", "closed", "filled", "cancelled", "expired"];
68
+ export const KYBER_LEG_FINAL_STATUSES = ["filled", "cancelled", "expired"];
69
+ export const KYBER_LEG_SETTLED_STATUS = "filled";
70
+ export const KYBER_LEG_OPEN_STATUSES = ["open", "partially_filled", "cancelling"];
71
+ // The buckets queried while waiting for an outcome: `active` covers open/partially_filled/cancelling, `closed`
72
+ // covers filled/cancelled/expired.
73
+ export const KYBER_LEG_STATUS_BUCKETS = ["active", "closed"];
74
+
75
+ // THE NAMED REFUSALS. Every one is a VALUE ({ ok: false, code }), never null: a caller can always tell unreachable
76
+ // from refused from not-settled-yet.
77
+ export const KYBER_LEG_REFUSAL_CODES = [
78
+ "kyber-unknown-network", // the chain has no order book here
79
+ "kyber-bad-order", // the order is incomplete or malformed - nothing to sign
80
+ "kyber-sign-failed", // the wallet did not produce a signature
81
+ "kyber-unreachable", // no answer from the order book (connection or timeout)
82
+ "kyber-order-unknown", // the order book does not know this order id for this maker
83
+ "kyber-refused", // the order book answered and did not accept (carries the status and its reason)
84
+ "kyber-bad-response", // the answer is not the shape we know (including typed data that is not this spec)
85
+ "kyber-not-settled", // the order stayed open until the timeout - "not yet", not "broken"
86
+ ];
87
+
88
+ const refusal = (code, params = {}) => ({ ok: false, code, params });
89
+
90
+ const isAddress = (v) => /^0x[0-9a-fA-F]{40}$/.test(String(v || ""));
91
+ const isPositive = (v) => { try { return BigInt(String(v)) > 0n; } catch { return false; } };
92
+ const isSignature = (v) => /^0x[0-9a-fA-F]{130}$/.test(String(v || ""));
93
+
94
+ const joinUrl = (base, path) => String(base || "").replace(/\/+$/, "") + path;
95
+
96
+ // ONE JSON REQUEST. A thrown fetch is kyber-unreachable, an unreadable body is kyber-bad-response - never silence.
97
+ async function ask({ fetchImpl, url, method = "GET", body = null, timeoutMs = 20_000 }) {
98
+ const doFetch = fetchImpl || (typeof fetch === "function" ? fetch : null);
99
+ if (!doFetch) return refusal("kyber-unreachable", { why: "no-fetch" });
100
+ let timer = null;
101
+ try {
102
+ const options = { method, headers: body === null ? undefined : { "content-type": "application/json" } };
103
+ if (body !== null) options.body = JSON.stringify(body);
104
+ const signal = typeof AbortController === "function" ? new AbortController() : null;
105
+ if (signal) { options.signal = signal.signal; timer = setTimeout(() => signal.abort(), timeoutMs); }
106
+ const res = await doFetch(url, options);
107
+ let text = null;
108
+ try { text = await res.text(); } catch { text = null; }
109
+ return { ok: true, status: Number(res.status), text };
110
+ } catch (error) {
111
+ return refusal("kyber-unreachable", { why: String((error && error.message) || error) });
112
+ } finally {
113
+ if (timer) clearTimeout(timer);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * THE INTENT REQUEST: from what the person gives, build the body the order book's sign-message endpoint expects.
119
+ * Returns { ok: true, params, expectedOutWei } or a named refusal. Pure - no network, no wallet. The order book
120
+ * computes feeConfig/getMakerAmount/getTakerAmount/predicate itself; we only name the trade (token -> wrapped native).
121
+ */
122
+ export function kyberLegPlan(request = {}) {
123
+ const maker = request.maker;
124
+ const receiver = request.receiver === undefined || request.receiver === null ? maker : request.receiver;
125
+ const params = {
126
+ chainId: String(request.chainId),
127
+ makerAsset: request.makerAsset,
128
+ takerAsset: request.takerAsset,
129
+ maker,
130
+ receiver,
131
+ makingAmount: String(request.makingAmount),
132
+ takingAmount: String(request.takingAmount),
133
+ expiredAt: request.expiredAt,
134
+ };
135
+ if (request.allowedSenders !== undefined && request.allowedSenders !== null) params.allowedSenders = request.allowedSenders;
136
+ const missing = [];
137
+ if (!Number.isInteger(Number(params.chainId))) missing.push("chainId");
138
+ if (!isAddress(maker)) missing.push("maker");
139
+ if (!isAddress(params.makerAsset)) missing.push("makerAsset");
140
+ if (!isAddress(params.takerAsset)) missing.push("takerAsset");
141
+ if (!isAddress(params.receiver)) missing.push("receiver");
142
+ if (!isPositive(params.makingAmount)) missing.push("makingAmount");
143
+ if (!isPositive(params.takingAmount)) missing.push("takingAmount");
144
+ if (!Number.isInteger(Number(params.expiredAt))) missing.push("expiredAt");
145
+ if (missing.length) return refusal("kyber-bad-order", { missing: [...new Set(missing)] });
146
+ return { ok: true, chainId: Number(params.chainId), params, expectedOutWei: String(params.takingAmount) };
147
+ }
148
+
149
+ /**
150
+ * ASK FOR THE UNSIGNED ORDER. POST sign-message -> { types, domain, primaryType, message }. The returned typed data
151
+ * is VALIDATED against this spec before it is trusted: the domain must be Kyber's and this contract's, the chain must
152
+ * be the order's, the Order types must be EXACTLY KYBER_LEG_ORDER_FIELDS, and the message must be complete. Typed
153
+ * data that does not agree is a NAMED refusal (kyber-bad-response), never signed anyway.
154
+ */
155
+ export async function kyberLegUnsigned({ params, chainId = null, orderbook = null, fetchImpl = null, timeoutMs = 20_000 } = {}) {
156
+ const id = Number(chainId !== null ? chainId : params && params.chainId);
157
+ const base = orderbook ? String(orderbook).replace(/\/+$/, "") : KYBER_LEG_ORDERBOOK;
158
+ if (!Number.isFinite(id)) return refusal("kyber-unknown-network", { chainId: null });
159
+ if (!params || typeof params !== "object") return refusal("kyber-bad-order", { field: "params" });
160
+ const res = await ask({ fetchImpl, url: joinUrl(base, KYBER_LEG_SIGN_MESSAGE_PATH), method: "POST", body: params, timeoutMs });
161
+ if (!res.ok) return res;
162
+ if (res.status >= 400) return refusal("kyber-refused", { status: res.status, stage: "sign-message", reason: null });
163
+ let body = null;
164
+ try { body = JSON.parse(res.text); } catch { body = null; }
165
+ const data = body && body.data;
166
+ if (!data || typeof data !== "object") return refusal("kyber-bad-response", { where: "sign-message" });
167
+ const domain = data.domain || {};
168
+ const message = data.message || {};
169
+ const typeNames = (data.types && Array.isArray(data.types.Order)) ? data.types.Order.map((t) => (t ? t.name : "") + ":" + (t ? t.type : "")).join(",") : null;
170
+ const expectedNames = KYBER_LEG_ORDER_FIELDS.map(([name, type]) => name + ":" + type).join(",");
171
+ const domainOk = String(domain.name) === KYBER_LEG_DOMAIN_NAME
172
+ && String(domain.version) === KYBER_LEG_DOMAIN_VERSION
173
+ && Number(domain.chainId) === id
174
+ && String(domain.verifyingContract || "").toLowerCase() === KYBER_LEG_CONTRACT.toLowerCase();
175
+ if (!domainOk) return refusal("kyber-bad-response", { where: "sign-message", field: "domain" });
176
+ if (typeNames !== expectedNames) return refusal("kyber-bad-response", { where: "sign-message", field: "types" });
177
+ const missing = KYBER_LEG_ORDER_FIELDS.filter(([name]) => message[name] === undefined || message[name] === null || message[name] === "");
178
+ if (missing.length) return refusal("kyber-bad-response", { where: "sign-message", missing: missing.map((m) => m[0]) });
179
+ return {
180
+ ok: true,
181
+ chainId: id,
182
+ orderbook: base,
183
+ contract: KYBER_LEG_CONTRACT,
184
+ salt: String(message.salt),
185
+ message,
186
+ typedData: { domain, types: data.types, primaryType: data.primaryType || "Order", message },
187
+ };
188
+ }
189
+
190
+ /**
191
+ * SIGN THE UNSIGNED ORDER THROUGH THE WALLET. The adapter's own signTypedData when present (it wraps
192
+ * eth_signTypedData_v4), otherwise the same call through the wallet's EIP-1193 driver. A wallet that will not sign
193
+ * is kyber-sign-failed, not a crash.
194
+ */
195
+ export async function kyberLegSign({ typedData, wallet = null, address = null } = {}) {
196
+ if (!typedData || typeof typedData !== "object") return refusal("kyber-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("kyber-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("kyber-sign-failed", { why: "bad-signature-shape" });
208
+ return { ok: true, signature };
209
+ }
210
+ return refusal("kyber-sign-failed", { why: "no-wallet" });
211
+ } catch (error) {
212
+ return refusal("kyber-sign-failed", { why: String((error && error.code) || (error && error.message) || error) });
213
+ }
214
+ }
215
+
216
+ /**
217
+ * SUBMIT THE SIGNED ORDER. POST write path -> { data: { id } }. The body carries the request plus the salt from the
218
+ * sign-message response and the Maker's signature. Every failure is one of the named refusals above.
219
+ */
220
+ export async function kyberLegSubmit({ params, salt, signature, orderbook = null, chainId = null, fetchImpl = null, timeoutMs = 20_000 } = {}) {
221
+ const base = orderbook ? String(orderbook).replace(/\/+$/, "") : KYBER_LEG_ORDERBOOK;
222
+ if (!params || typeof params !== "object") return refusal("kyber-bad-order", { field: "params" });
223
+ if (!isSignature(signature)) return refusal("kyber-bad-order", { field: "signature" });
224
+ if (salt === undefined || salt === null || salt === "") return refusal("kyber-bad-order", { field: "salt" });
225
+ const body = { ...params, salt: String(salt), signature };
226
+ const res = await ask({ fetchImpl, url: joinUrl(base, KYBER_LEG_CREATE_PATH), method: "POST", body, timeoutMs });
227
+ if (!res.ok) return res;
228
+ if (res.status >= 400) return refusal("kyber-refused", { status: res.status, stage: "submit", reason: null });
229
+ let parsed = null;
230
+ try { parsed = JSON.parse(res.text); } catch { parsed = null; }
231
+ const created = parsed && parsed.data;
232
+ const id = created && created.id;
233
+ if (!Number.isInteger(Number(id)) || Number(id) <= 0) return refusal("kyber-bad-response", { where: "submit", got: typeof id });
234
+ return { ok: true, id: Number(id), chainId: Number.isFinite(Number(chainId)) ? Number(chainId) : null };
235
+ }
236
+
237
+ /**
238
+ * READ ONE ORDER's status. GET maker orders by bucket; the order is looked up by id. A status word outside
239
+ * KYBER_LEG_STATUSES is kyber-bad-response; an id found in neither bucket is kyber-order-unknown - never silence.
240
+ */
241
+ export async function kyberLegRead({ id, chainId, maker, orderbook = null, fetchImpl = null, timeoutMs = 20_000 } = {}) {
242
+ const base = orderbook ? String(orderbook).replace(/\/+$/, "") : KYBER_LEG_ORDERBOOK;
243
+ const chain = Number(chainId);
244
+ if (!Number.isInteger(Number(id)) || Number(id) <= 0) return refusal("kyber-bad-order", { field: "id" });
245
+ if (!isAddress(maker)) return refusal("kyber-bad-order", { field: "maker" });
246
+ for (const bucket of KYBER_LEG_STATUS_BUCKETS) {
247
+ const path = KYBER_LEG_MAKER_ORDERS_PATH + "?chainId=" + chain + "&maker=" + maker + "&status=" + bucket;
248
+ const res = await ask({ fetchImpl, url: joinUrl(base, path), timeoutMs });
249
+ if (!res.ok) return res;
250
+ if (res.status >= 400) return refusal("kyber-refused", { status: res.status, stage: "orders" });
251
+ let parsed = null;
252
+ try { parsed = JSON.parse(res.text); } catch { parsed = null; }
253
+ const orders = parsed && parsed.data && parsed.data.orders;
254
+ if (!Array.isArray(orders)) return refusal("kyber-bad-response", { where: "orders" });
255
+ const found = orders.find((o) => o && Number(o.id) === Number(id)) || null;
256
+ if (found) {
257
+ const status = found.status;
258
+ if (typeof status !== "string" || !KYBER_LEG_STATUSES.includes(status)) return refusal("kyber-bad-response", { where: "status", got: typeof status === "string" ? status : null });
259
+ return { ok: true, status, order: found, bucket };
260
+ }
261
+ }
262
+ return refusal("kyber-order-unknown", { id: Number(id), maker });
263
+ }
264
+
265
+ /**
266
+ * WHAT ENDS THIS LEG'S EXECUTION - poll the order book until the order is settled or finally dead, with a timeout.
267
+ * { ok: true, done: true, settled: true, status: "filled" } - the leg settled (WETH delivered);
268
+ * { ok: true, done: true, settled: false, status: "cancelled" | "expired" } - the order is finally dead;
269
+ * { ok: false, code: "kyber-not-settled" | "kyber-unreachable" | ... } - a NAMED refusal.
270
+ * "Not settled in time" is its own code: "we did not wait long enough" is a state of the order, not a breakdown. The
271
+ * clock and the sleep are injectable, so the wait is testable without real time.
272
+ */
273
+ export async function kyberLegSettled({ id, chainId, maker, orderbook = null, fetchImpl = null, pollMs = 5_000, timeoutMs = 300_000, now = null, sleep = null, onTick = null } = {}) {
274
+ if (!Number.isInteger(Number(id)) || Number(id) <= 0) return refusal("kyber-bad-order", { field: "id" });
275
+ const clock = typeof now === "function" ? now : () => Date.now();
276
+ const pause = typeof sleep === "function" ? sleep : (ms) => new Promise((r) => setTimeout(r, ms));
277
+ const step = Number.isFinite(pollMs) && pollMs > 0 ? pollMs : 5_000;
278
+ const limit = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 300_000;
279
+ const startedAt = clock();
280
+ let lastStatus = null;
281
+ for (;;) {
282
+ const read = await kyberLegRead({ id, chainId, maker, orderbook, fetchImpl });
283
+ if (!read.ok) return read;
284
+ lastStatus = read.status;
285
+ if (typeof onTick === "function") onTick(lastStatus);
286
+ if (KYBER_LEG_FINAL_STATUSES.includes(lastStatus)) {
287
+ return { ok: true, done: true, settled: lastStatus === KYBER_LEG_SETTLED_STATUS, status: lastStatus, id: Number(id) };
288
+ }
289
+ if (clock() - startedAt >= limit) return { ...refusal("kyber-not-settled", { id: Number(id), status: lastStatus, waitedMs: clock() - startedAt }), status: lastStatus, id: Number(id) };
290
+ await pause(step);
291
+ }
292
+ }
293
+
294
+ // THE REFUSAL WORDS FOR THE SCREEN - by CODE, so a refusal is never an empty toast. Unknown codes are shown as-is.
295
+ export const kyberLegRefusalWords = (result) => {
296
+ const code = result && result.code ? String(result.code) : null;
297
+ const map = {
298
+ "kyber-unknown-network": "no KyberSwap liquidity provider serves this network",
299
+ "kyber-bad-order": "the swap intent is incomplete and was not sent",
300
+ "kyber-sign-failed": "the wallet did not sign the swap intent",
301
+ "kyber-unreachable": "the KyberSwap order book could not be reached",
302
+ "kyber-order-unknown": "the KyberSwap order book does not know this order",
303
+ "kyber-refused": "the KyberSwap order book did not accept the order",
304
+ "kyber-bad-response": "the KyberSwap order book answered in a shape we do not know",
305
+ "kyber-not-settled": "the swap did not settle in time",
306
+ };
307
+ return map[code] || (code ? code : "the liquidity leg stopped");
308
+ };
@@ -0,0 +1,179 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/asyncRoute.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
+ // ASYNCHRONOUS ROUTE PROVIDERS BY NETWORK - "IS THERE A PATH FOR A PERSON WITH NO OWN NATIVE COIN".
6
+ //
7
+ // WHY THIS MODULE EXISTS. The declared routes in the registry (www/js/core/config.js: DEX_ROUTES) are ROUTER
8
+ // swaps: they run INSIDE the person's own transaction (shape "sync"), so the person must hold native coin to pay
9
+ // for that transaction. A person coming from USDC who holds NONE cannot sign it at all. The way out is an INTENT
10
+ // AUCTION (shape "async"): sign an intent, let solvers settle it later, and only THEN pay for the escrow deposit -
11
+ // so the deposit's gas is paid AFTER the swap, not before it. Whether such a provider serves a network is DATA
12
+ // (core/config.js: ASYNC_ROUTE_PROVIDERS), and this module is the ONE engine place that answers it - as a value,
13
+ // with a NAMED refusal, never by silence and never as a silent "yes".
14
+ //
15
+ // STEP 5: THE CHOICE CONSIDERS BOTH PROVIDERS AND WHAT A PROVIDER SETTLES. The escrow is funded with NATIVE coin,
16
+ // so the choice is asked "can you carry the route ALL THE WAY to native?". A provider declares what it ends at
17
+ // (`settles`: "native" for CoW, "wrapped" for KyberSwap - whose orders trade ERC20 against ERC20 and whose API
18
+ // refuses native as an order asset). So a wrapped provider is NEVER chosen as the sole provider for a native need:
19
+ // it is a LIQUIDITY leg, and the route to native is a COMPOSITION of two orders - the liquidity leg (token -> WETH)
20
+ // and a native leg on a native-capable provider (WETH -> native coin, CoW). The choice returns a route record or a
21
+ // composition; the composition is EXECUTED in evm/asyncExec.js, which pays the second leg's own fee out of the
22
+ // reserve so the person is not sent into a composition that leaves them short of the deposit.
23
+ //
24
+ // IT MIRRORS THE PACKAGE'S PROVIDER LAYER ON PURPOSE. The move to async lives in the SDK package
25
+ // (sdk/src/legs/{shape,cow,kyber}.mjs): there `cowOrderbook(chainId)` / `kyberOrderbook(chainId)` answer whether a
26
+ // provider covers a network and `requireAsyncProvider(id)` answers whether a registered provider is complete. The
27
+ // engine cannot import the package at runtime (the page loads www/js; the package is a separate bundle), so the
28
+ // network table is repeated HERE and the agreement of the two is a CHECK - tools/check-buy-async-route.mjs compares
29
+ // this table against sdk/src/legs/cow.mjs and sdk/src/legs/kyber.mjs (including `settles`), so a repeated truth
30
+ // cannot drift silently.
31
+ import { ASYNC_ROUTE_PROVIDERS } from "../core/config.js";
32
+
33
+ // WHAT THE PERSON NEEDS TO END WITH. The escrow is funded with the network's NATIVE coin, so the default is native.
34
+ export const ASYNC_NEEDS = ["native", "wrapped"];
35
+ export const ASYNC_DEFAULT_NEED = "native";
36
+ // WHAT A PROVIDER SETTLES: the two values the table's `settles` field may take.
37
+ export const ASYNC_SETTLES = Object.freeze({ NATIVE: "native", WRAPPED: "wrapped" });
38
+
39
+ // A NAMED SEAM FOR CHECKS: route record(s) injected for a network the table does not serve, so the asynchronous path
40
+ // can be exercised end to end against a local stand-in order book. PRODUCTION NEVER SETS IT - the page reads the
41
+ // table, and the table is what the guard cross-checks against the package. The injection is explicit, named at the
42
+ // call site and cleared by the same function, so "the path was taken against a stand-in" is never mistaken for
43
+ // "a real provider serves this network". Step 5 needs MORE THAN ONE provider at once (a composition), so the seam
44
+ // takes a single record OR a list; the verdict stays a VALUE either way.
45
+ let injectedRoutes = [];
46
+ const injectionValue = () => (injectedRoutes.length === 0 ? null : injectedRoutes.length === 1 ? injectedRoutes[0] : injectedRoutes);
47
+ export const setAsyncRouteInjection = (route = null) => {
48
+ if (Array.isArray(route)) injectedRoutes = route.filter(Boolean);
49
+ else if (route && typeof route === "object") injectedRoutes = [route];
50
+ else injectedRoutes = [];
51
+ return injectionValue();
52
+ };
53
+ export const asyncRouteInjection = () => injectionValue();
54
+ const injectedFor = (chainId) => injectedRoutes.filter((r) => r && Number(r.chainId) === Number(chainId));
55
+
56
+ /** The provider record by id, or null. */
57
+ export const asyncRouteProvider = (id) => (id && ASYNC_ROUTE_PROVIDERS[id]) || null;
58
+
59
+ /** Every declared asynchronous provider id, sorted. */
60
+ export const asyncRouteProviderIds = () => Object.keys(ASYNC_ROUTE_PROVIDERS).sort();
61
+
62
+ /** The networks (numeric chainIds, ascending) a provider serves, or an empty list. */
63
+ export const asyncRouteChains = (id) => Object.keys((asyncRouteProvider(id) || {}).networks || {})
64
+ .map(Number).filter(Number.isFinite).sort((a, b) => a - b);
65
+
66
+ /** WHAT A PROVIDER SETTLES ("native" | "wrapped"), or null. The table declares it; an injection may override it. */
67
+ export const asyncRouteProviderSettles = (id) => {
68
+ const provider = asyncRouteProvider(id);
69
+ return provider ? (provider.settles === undefined ? null : provider.settles) : null;
70
+ };
71
+
72
+ // A route record: { id, venue, shape, chainId, slug, orderbook, contract, settles }. `settles` resolves the
73
+ // injection's own value first, then the table's for the same id (so an injection that names a real provider inherits
74
+ // its capability and does not have to repeat it).
75
+ const settlesOf = (id, extra = {}) => (extra.settles ? extra.settles : asyncRouteProviderSettles(id));
76
+ const recordFor = (id, chainId, net, extra = {}) => {
77
+ const provider = asyncRouteProvider(id) || {};
78
+ return {
79
+ id,
80
+ venue: extra.venue || provider.venue || id,
81
+ shape: extra.shape || provider.shape || "async",
82
+ chainId: Number(chainId),
83
+ slug: net.slug === undefined ? null : net.slug,
84
+ orderbook: net.orderbook === undefined ? null : net.orderbook,
85
+ contract: net.contract === undefined ? null : net.contract,
86
+ settles: settlesOf(id, extra),
87
+ };
88
+ };
89
+
90
+ /**
91
+ * EVERY provider that serves a network, as route records (sorted by id). An injection for the same id WINS over the
92
+ * table (checks drive the path against a stand-in book). An empty list means no asynchronous provider serves the
93
+ * network - the caller must refuse BY NAME, never guess.
94
+ */
95
+ export const asyncRouteProvidersForChain = (chainId) => {
96
+ const n = Number(chainId);
97
+ const byId = new Map();
98
+ for (const id of asyncRouteProviderIds()) {
99
+ const provider = asyncRouteProvider(id);
100
+ const net = provider.networks && provider.networks[n];
101
+ if (net) byId.set(id, recordFor(id, n, net));
102
+ }
103
+ for (const inj of injectedFor(n)) {
104
+ byId.set(inj.id, recordFor(inj.id, n, { slug: inj.slug, orderbook: inj.orderbook, contract: inj.contract },
105
+ { settles: inj.settles, venue: inj.venue, shape: inj.shape }));
106
+ }
107
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
108
+ };
109
+
110
+ /**
111
+ * The provider record for a network, or null. `providerId` names the provider; absent, the first by id (the
112
+ * historical default, kept so a caller that passes only a chain sees the same record as before). The injection is
113
+ * consulted first (checks only).
114
+ */
115
+ export const asyncRouteForChain = (chainId, providerId = null) => {
116
+ const n = Number(chainId);
117
+ if (providerId) {
118
+ const inj = injectedFor(n).find((r) => r.id === providerId);
119
+ if (inj) return recordFor(inj.id, n, { slug: inj.slug, orderbook: inj.orderbook, contract: inj.contract },
120
+ { settles: inj.settles, venue: inj.venue, shape: inj.shape });
121
+ return asyncRouteProvidersForChain(n).find((r) => r.id === providerId) || null;
122
+ }
123
+ const served = asyncRouteProvidersForChain(n);
124
+ return served.length ? served[0] : null;
125
+ };
126
+
127
+ // The chains a named provider (or the first, when unnamed) serves - the `known` list of a refusal, unchanged in
128
+ // meaning from before the step.
129
+ const knownChainsFor = (providerId) => asyncRouteChains(providerId || asyncRouteProviderIds()[0] || null);
130
+
131
+ /**
132
+ * WHICH WAY AN ASYNCHRONOUS NEED GOES FOR A NETWORK - as a VALUE, with a NAMED refusal.
133
+ *
134
+ * { ok: true, kind: "single", ...route, need } - one provider carries the need all the way;
135
+ * { ok: true, kind: "composition", legs: [liquidity, native], ... } - no single provider carries a NATIVE need,
136
+ * but a liquidity (wrapped) leg and a
137
+ * native-capable leg both serve the network;
138
+ * { ok: false, reason: "no-async-provider" | "no-native-async-provider", ... } - a named refusal.
139
+ *
140
+ * The rule that matters: a provider that does not settle what the person needs is NEVER chosen as the sole provider
141
+ * for that need (a route that ends in WETH must not pass as native). It may only be a leg of a composition, and a
142
+ * composition is possible only when a native-capable provider serves the SAME network to finish it.
143
+ */
144
+ export const asyncRouteChoice = ({ chainId = null, providerId = null, need = ASYNC_DEFAULT_NEED } = {}) => {
145
+ const n = Number(chainId);
146
+ const finite = Number.isFinite(n);
147
+ const want = ASYNC_NEEDS.includes(need) ? need : ASYNC_DEFAULT_NEED;
148
+ const served = finite ? asyncRouteProvidersForChain(n) : [];
149
+ if (!served.length) {
150
+ return { ok: false, reason: "no-async-provider", chainId: finite ? n : null, need: want, provider: providerId || null, known: knownChainsFor(providerId) };
151
+ }
152
+ let named = null;
153
+ if (providerId) {
154
+ named = served.find((r) => r.id === providerId) || null;
155
+ if (!named) return { ok: false, reason: "no-async-provider", chainId: n, need: want, provider: providerId, known: served.map((r) => r.id) };
156
+ }
157
+ const considered = named ? [named] : served;
158
+ const direct = considered.filter((r) => r.settles === want);
159
+ if (direct.length) {
160
+ const first = direct[0];
161
+ return { ok: true, kind: "single", ...first, need: want, provider: first.id, legs: [first] };
162
+ }
163
+ // No single provider carries the need. Compose when the need is native and BOTH legs exist on this network.
164
+ const liquidity = considered.filter((r) => r.settles === ASYNC_SETTLES.WRAPPED);
165
+ const native = served.filter((r) => r.settles === ASYNC_SETTLES.NATIVE);
166
+ if (want === ASYNC_SETTLES.NATIVE && liquidity.length && native.length) {
167
+ const leg1 = liquidity[0];
168
+ const leg2 = native[0];
169
+ return { ok: true, kind: "composition", need: want, chainId: n, provider: leg1.id, venue: leg1.venue, shape: "async",
170
+ orderbook: leg1.orderbook, legs: [leg1, leg2] };
171
+ }
172
+ // The truthful reason: the network DOES serve asynchronous providers, but none can finish at what the person needs.
173
+ const reason = want === ASYNC_SETTLES.NATIVE && liquidity.length && !native.length
174
+ ? "no-native-async-provider" : "no-async-provider";
175
+ return { ok: false, reason, chainId: n, need: want, missing: want, provider: (named || liquidity[0] || served[0]).id, known: served.map((r) => r.id) };
176
+ };
177
+
178
+ /** The historical name of the same verdict, kept so existing callers (evm/permit.js) see one implementation. */
179
+ export const asyncRouteVerdict = asyncRouteChoice;