@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,362 @@
1
+
2
+ // UNISWAPX - THE THIRD CONCRETE ASYNCHRONOUS ROUTE PROVIDER, A SIBLING OF cow.mjs AND kyber.mjs.
3
+ //
4
+ // WHAT IT IS. UniswapX is an intent auction: the swapper signs an EIP-712 order (gasless) and fillers compete to
5
+ // settle it on chain LATER as a separate transaction. So its shape is "async" - there is no single transaction that
6
+ // swaps and funds the escrow - and its execution ends when the order service reports the order FILLED, or finally
7
+ // dead (expired / error / cancelled / insufficient-funds). The `settled(request, deps)` below is that "what ends its
8
+ // execution", and the seam refuses an async provider without it.
9
+ //
10
+ // WHY THIS ONE CAN CARRY A ROUTE ALL THE WAY TO NATIVE. Its output token may be address(0), the protocol's NATIVE
11
+ // sentinel (uniswapx-spec.mjs, UNISWAPX_NATIVE, from Uniswap/UniswapX CurrencyLibrary). So a route to a native need
12
+ // can end HERE, unlike the KyberSwap leg (which settles the wrapped native only). `settles: "native"`.
13
+ //
14
+ // THE FLOW, AND WHERE THE HOSTED SERVICE IS.
15
+ // 1) BUILD the swapper's own part of the order LOCALLY (uniswapxPlan below): every field of a V2 Dutch order that
16
+ // the swapper signs is known here, and the digest is computed from this repository's own EIP-712
17
+ // (uniswapx-spec.mjs). This part needs no service.
18
+ // 2) SIGN it through the wallet adapter (eth_signTypedData_v4 over Permit2's domain, the order as the witness).
19
+ // 3) CO-SIGN AND PUBLISH. A V2 Dutch order is COSIGNED: `cosignerData` (the decay window and any overrides) and a
20
+ // `cosignature` are produced by the cosigner AFTER the swapper signs. Uniswap's own documentation states that
21
+ // the Uniswap Interface and the Uniswap API set the cosigner to Uniswap Labs ("Auction Types"), and orders
22
+ // reach fillers through the Uniswap API: "Order submission is handled by the Uniswap Trading API" (the UniswapX
23
+ // orders OpenAPI). That endpoint needs an API key, so PUBLISHING IS A HOSTED, KEYED STEP - and THIS PACKAGE
24
+ // HOLDS NO KEY. It posts the signed order to OUR SUBMITTER, a URL taken from configuration (the caller's
25
+ // `submitter`), and the submitter adds the key SERVER-SIDE. With no submitter it refuses by name
26
+ // ("uniswapx-no-submitter") instead of inventing an endpoint and instead of asking the browser for a key.
27
+ // 4) READ THE OUTCOME from the UniswapX orders service (GET /orders on https://api.uniswap.org/v2 - the endpoint
28
+ // Uniswap's own filler documentation tells fillers to poll), by the order hash, and read `orderStatus`.
29
+ //
30
+ // EVERY EXTERNAL FACT IS PINNED TO ITS SOURCE:
31
+ // * the order type, the field names/order, the Permit2 domain and the native sentinel - uniswapx-spec.mjs, with
32
+ // its own citations (Uniswap/UniswapX src/lib/V2DutchOrderLib.sol, DutchOrderLib.sol, OrderInfoLib.sol,
33
+ // CurrencyLibrary.sol; Uniswap/permit2 src/EIP712.sol; Uniswap/uniswapx-sdk src/order/V2DutchOrder.ts);
34
+ // * the reactor addresses per chain - developers.uniswap.org/deployments.json (the feed the UniswapX "Deployment"
35
+ // page points to): Ethereum mainnet V2DutchOrderReactor 0x00000011F84B9aa48e5f8aA8B9897600006289Be. Other chains
36
+ // run the Dutch V3 reactor, whose order struct is a different type and is NOT built here;
37
+ // * the publishing endpoint and its API key requirement - developers.uniswap.org, "Swapping via the Uniswap API"
38
+ // (POST /order for a UniswapX order) and the Trading API OpenAPI (trade-api.gateway.uniswap.org/v1/api.json:
39
+ // path /order, operation "Create a gasless order", security apiKey in header `x-api-key`);
40
+ // * the READ endpoint and status vocabulary - api.uniswap.org/v2/uniswapx/docs.json (GET /orders; OrderStatus
41
+ // enum open | expired | error | cancelled | filled | insufficient-funds) and developers.uniswap.org,
42
+ // "Fillers Overview" (poll the orders endpoint, at most 4 RPS).
43
+ //
44
+ // THE SHARED PRE-SIGNATURE GUARDS STAY ON FOR THIS PROVIDER TOO (uniswapxGate): the price is not the market, the
45
+ // amount is covered, the token allowance - the SAME guard functions the synchronous DEX leg and the other two
46
+ // providers use. An external, asynchronous route must not become a hole in the guards.
47
+ //
48
+ // NO NEW DEPENDENCY: the typed data is built by uniswapx-spec.mjs (this repository's own EIP-712), signing goes
49
+ // through the wallet adapter / the repository's signer, and the transport is the repository's own createHttp.
50
+ import { ASYNC } from "./shape.mjs";
51
+ import {
52
+ UNISWAPX_PERMIT2, UNISWAPX_NATIVE, UNISWAPX_ORDER_FIELDS,
53
+ uniswapxOrderMissingFields, uniswapxOrderHashHex, uniswapxOrderTypedData,
54
+ } from "./uniswapx-spec.mjs";
55
+ import { createHttp } from "../http.mjs";
56
+ import * as engine from "../engine.mjs";
57
+
58
+ // THE ORDER SERVICE (read side): the UniswapX orders API, one base URL for every chain (api.uniswap.org/v2).
59
+ export const UNISWAPX_API = "https://api.uniswap.org/v2";
60
+ export const UNISWAPX_ORDER_PATH = "/orders";
61
+ // The venue name, in one word (no assembly needed: it is a single token).
62
+ export const UNISWAPX_VENUE = "UniswapX";
63
+
64
+ // THE COSIGNER. A V2 Dutch order is cosigned, and Uniswap's documentation states the Uniswap Interface and the
65
+ // Uniswap API set the cosigner to Uniswap Labs ("Auction Types", concepts). The address below is the one every real
66
+ // mainnet Dutch_V2 order we read carries; it is a DEFAULT for building a well-formed order, and the cosignature
67
+ // itself is the cosigner's - this provider cannot produce it.
68
+ export const UNISWAPX_COSIGNER = "0x4449Cd34d1eb1FEDCF02A1Be3834FfDe8E6A6180";
69
+
70
+ // ZERO - used for the absent additional-validation contract.
71
+ export const UNISWAPX_ZERO = "0x0000000000000000000000000000000000000000";
72
+
73
+ // THE NETWORKS WHERE THIS PROVIDER CAN BUILD AN ORDER. Keyed by EVM chainId. Only the chains whose reactor accepts
74
+ // the V2 Dutch order this spec implements: Ethereum mainnet (V2DutchOrderReactor). A network absent here is a NAMED
75
+ // refusal ("uniswapx-unknown-network"), never a guess at an endpoint or a reactor.
76
+ export const UNISWAPX_NETWORKS = Object.freeze({
77
+ 1: { orderbook: UNISWAPX_API, reactor: "0x00000011F84B9aa48e5f8aA8B9897600006289Be" },
78
+ });
79
+
80
+ // THE STATUS VOCABULARY OF THE ORDER SERVICE (api.uniswap.org/v2/uniswapx/docs.json: OrderStatus). A status outside
81
+ // this set is NOT "probably fine": the answer is not the shape we know and it is refused with "uniswapx-bad-response".
82
+ export const UNISWAPX_STATUSES = ["open", "expired", "error", "cancelled", "filled", "insufficient-funds"];
83
+ export const UNISWAPX_FINAL_STATUSES = ["expired", "error", "cancelled", "filled", "insufficient-funds"];
84
+ export const UNISWAPX_SETTLED_STATUS = "filled";
85
+ export const UNISWAPX_OPEN_STATUSES = ["open"];
86
+
87
+ // THE NAMED REFUSALS OF THIS PROVIDER. Every one is a VALUE ({ ok: false, code }), never silence and never null: a
88
+ // caller must always be able to tell "unreachable" from "refused" from "not settled yet", and a missing submitter
89
+ // ("uniswapx-no-submitter") from every other refusal - publishing is blocked one way here, and it is named.
90
+ export const UNISWAPX_REFUSAL_CODES = [
91
+ "uniswapx-unknown-network", // the chain has no V2 Dutch reactor here
92
+ "uniswapx-bad-order", // the order is incomplete or malformed - nothing to sign
93
+ "uniswapx-sign-failed", // the wallet did not produce a signature
94
+ "uniswapx-no-submitter", // no submitter URL was configured - publishing is refused, never guessed
95
+ "uniswapx-unreachable", // no answer from the order service (connection or timeout)
96
+ "uniswapx-order-unknown", // the service does not know this order hash
97
+ "uniswapx-refused", // the service answered and did not accept (it carries the status and its reason)
98
+ "uniswapx-bad-response", // the answer is not the shape we know
99
+ "uniswapx-not-settled", // the order stayed open until the timeout - "not yet", not "broken"
100
+ ];
101
+
102
+ const refusal = (code, params = {}) => ({ ok: false, code, params });
103
+
104
+ /** The reactor for a chain, or null. */
105
+ export const uniswapxReactorFor = (chainId) => {
106
+ const n = UNISWAPX_NETWORKS[Number(chainId)];
107
+ return n ? n.reactor : null;
108
+ };
109
+ /** The order service base URL for a chain, or null. */
110
+ export const uniswapxOrderbook = (chainId) => {
111
+ const n = UNISWAPX_NETWORKS[Number(chainId)];
112
+ return n ? n.orderbook : null;
113
+ };
114
+
115
+ const isAddress = (v) => /^0x[0-9a-fA-F]{40}$/.test(String(v || ""));
116
+ const isPositive = (v) => { try { return BigInt(String(v)) > 0n; } catch { return false; } };
117
+ const isHash = (v) => /^0x[0-9a-fA-F]{64}$/.test(String(v || ""));
118
+
119
+ /**
120
+ * THE INTENT: from what the person gives, build the swapper's own V2 Dutch order, its EIP-712 typed data and its
121
+ * order hash. Returns { ok: true, order, typedData, orderHash, chainId, owner, reactor } or a named refusal.
122
+ * Pure - no network, no wallet. The cosignerData / cosignature are NOT here: the cosigner adds them after the
123
+ * swapper signs (see the header) and this provider cannot produce them.
124
+ */
125
+ export function buildUniswapxIntent(request = {}) {
126
+ const chainId = Number(request.chainId);
127
+ const net = UNISWAPX_NETWORKS[chainId];
128
+ if (!net) return { ...refusal("uniswapx-unknown-network", { chainId: Number.isFinite(chainId) ? chainId : null }), known: Object.keys(UNISWAPX_NETWORKS).map(Number) };
129
+ const swapper = request.swapper;
130
+ const recipient = request.recipient === undefined || request.recipient === null ? swapper : request.recipient;
131
+ const order = {
132
+ reactor: request.reactor === undefined || request.reactor === null ? net.reactor : request.reactor,
133
+ swapper,
134
+ nonce: request.nonce,
135
+ deadline: request.deadline,
136
+ additionalValidationContract: request.additionalValidationContract === undefined || request.additionalValidationContract === null ? UNISWAPX_ZERO : request.additionalValidationContract,
137
+ additionalValidationData: request.additionalValidationData === undefined || request.additionalValidationData === null ? "0x" : request.additionalValidationData,
138
+ cosigner: request.cosigner === undefined || request.cosigner === null ? UNISWAPX_COSIGNER : request.cosigner,
139
+ baseInputToken: request.sellToken,
140
+ baseInputStartAmount: request.sellAmountWei,
141
+ baseInputEndAmount: request.sellAmountEndWei === undefined || request.sellAmountEndWei === null ? request.sellAmountWei : request.sellAmountEndWei,
142
+ baseOutputs: [{
143
+ token: request.buyToken,
144
+ startAmount: request.buyAmountWei,
145
+ endAmount: request.buyAmountEndWei === undefined || request.buyAmountEndWei === null ? request.buyAmountWei : request.buyAmountEndWei,
146
+ recipient,
147
+ }],
148
+ };
149
+ const missing = uniswapxOrderMissingFields(order);
150
+ if (!isAddress(swapper)) missing.push("swapper");
151
+ if (!isAddress(order.reactor)) missing.push("reactor");
152
+ if (!isAddress(order.cosigner)) missing.push("cosigner");
153
+ if (!isAddress(order.baseInputToken)) missing.push("sellToken");
154
+ if (!isPositive(order.baseInputStartAmount) || !isPositive(order.baseInputEndAmount)) missing.push("sellAmountWei");
155
+ if (!isPositive(order.baseOutputs[0].startAmount) || !isPositive(order.baseOutputs[0].endAmount)) missing.push("buyAmountWei");
156
+ if (!isAddress(order.baseOutputs[0].token) || !isAddress(order.baseOutputs[0].recipient)) missing.push("buyToken");
157
+ if (!Number.isInteger(Number(order.deadline))) missing.push("deadline");
158
+ if (missing.length) return refusal("uniswapx-bad-order", { missing: [...new Set(missing)] });
159
+ try {
160
+ const typedData = uniswapxOrderTypedData({ order, chainId });
161
+ const orderHash = uniswapxOrderHashHex(order);
162
+ return { ok: true, chainId, owner: swapper, reactor: order.reactor, order, orderHash, typedData };
163
+ } catch (error) {
164
+ // A malformed number/address inside the encoder: a NAMED refusal, not an exception escaping to the caller.
165
+ return refusal("uniswapx-bad-order", { why: String(error && error.message ? error.message : error) });
166
+ }
167
+ }
168
+
169
+ /** The provider's plan, in the shape the registry and the paths expect. Returns a VALUE with a named refusal. */
170
+ export function uniswapxPlan(request = {}) {
171
+ const intent = buildUniswapxIntent(request);
172
+ if (!intent.ok) return intent;
173
+ const output = intent.order.baseOutputs[0];
174
+ return {
175
+ ok: true,
176
+ provider: "uniswapx",
177
+ venue: UNISWAPX_VENUE,
178
+ shape: ASYNC,
179
+ route: "uniswapx",
180
+ expectedOutWei: String(output.endAmount),
181
+ order: intent.order,
182
+ orderHash: intent.orderHash,
183
+ typedData: intent.typedData,
184
+ owner: intent.owner,
185
+ reactor: intent.reactor,
186
+ nativeOutput: String(output.token).toLowerCase() === UNISWAPX_NATIVE,
187
+ endsWith: { kind: "order-status", settledWhen: UNISWAPX_SETTLED_STATUS, final: [...UNISWAPX_FINAL_STATUSES], endpoint: UNISWAPX_ORDER_PATH },
188
+ };
189
+ }
190
+
191
+ /**
192
+ * SIGN THE ORDER THROUGH THE WALLET ADAPTER. The adapter's own `signTypedData` is used when present (it wraps
193
+ * eth_signTypedData_v4); otherwise the same call is made through the adapter's EIP-1193 driver. A wallet that will
194
+ * not sign is a NAMED refusal, not a crash.
195
+ */
196
+ export async function uniswapxSignOrder({ typedData, wallet, address = null } = {}) {
197
+ if (!typedData || typeof typedData !== "object") return refusal("uniswapx-bad-order", { field: "typedData" });
198
+ try {
199
+ if (wallet && typeof wallet.signTypedData === "function") {
200
+ const signature = await wallet.signTypedData(typedData);
201
+ if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) return refusal("uniswapx-sign-failed", { why: "bad-signature-shape" });
202
+ return { ok: true, signature };
203
+ }
204
+ const driver = wallet && typeof wallet.driver === "function" ? wallet.driver() : null;
205
+ if (driver && typeof driver.request === "function") {
206
+ const from = address || (typeof wallet.address === "function" ? await wallet.address() : null);
207
+ const signature = await driver.request({ method: "eth_signTypedData_v4", params: [from, JSON.stringify(typedData)] });
208
+ if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) return refusal("uniswapx-sign-failed", { why: "bad-signature-shape" });
209
+ return { ok: true, signature };
210
+ }
211
+ return refusal("uniswapx-sign-failed", { why: "no-wallet" });
212
+ } catch (error) {
213
+ return refusal("uniswapx-sign-failed", { why: String((error && error.code) || (error && error.message) || error) });
214
+ }
215
+ }
216
+
217
+ /**
218
+ * PUBLISH THE SIGNED ORDER TO OUR SUBMITTER. The submitter is OUR proxy - a URL from configuration (the caller's
219
+ * `submitter`), NEVER a host baked into this package. It holds the Uniswap Trading API key SERVER-SIDE and forwards
220
+ * the order on, so the browser never sees a key and this function NEVER sends one: it posts `{signature, quote,
221
+ * routing}` with a JSON content-type and nothing else. The `quote` is the object the host returned from its own
222
+ * quote call - this provider does not fabricate it, and the cosigner is Uniswap Labs' (see the header). With no
223
+ * submitter URL it refuses by name ("uniswapx-no-submitter"). The endpoint is NEVER guessed.
224
+ */
225
+ export async function submitUniswapxOrder({ quote = null, signature = null, routing = "DUTCH_V2", submitter = null, fetchImpl = null, timeoutMs = 20_000 } = {}) {
226
+ if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) return refusal("uniswapx-bad-order", { field: "signature" });
227
+ if (!quote || typeof quote !== "object") return refusal("uniswapx-bad-order", { field: "quote" });
228
+ const target = submitter === undefined || submitter === null || submitter === "" ? null : String(submitter);
229
+ if (!target) return refusal("uniswapx-no-submitter", { field: "submitter" });
230
+ const http = createHttp({ apiBase: target, fetchImpl, timeoutMs });
231
+ // NO `x-api-key` HERE, EVER: the submitter adds it server-side, so the package and the browser stay key-free.
232
+ const body = JSON.stringify({ signature, quote, routing });
233
+ let res;
234
+ try {
235
+ res = await http.tryJson("", { method: "POST", headers: { "content-type": "application/json" }, body });
236
+ } catch (error) {
237
+ return refusal("uniswapx-unreachable", { why: String((error && error.params && error.params.why) || (error && error.code) || error) });
238
+ }
239
+ if (!res || typeof res.ok !== "boolean") return refusal("uniswapx-bad-response", { where: "submit" });
240
+ if (!res.ok) return refusal("uniswapx-refused", { status: res.status, stage: "submit" });
241
+ const data = res.body && typeof res.body === "object" ? res.body : null;
242
+ if (!data || typeof data.orderId !== "string" || data.orderId === "") return refusal("uniswapx-bad-response", { where: "submit", field: "orderId" });
243
+ const status = data.orderStatus;
244
+ if (typeof status !== "string" || !UNISWAPX_STATUSES.includes(status)) return refusal("uniswapx-bad-response", { where: "submit", field: "orderStatus" });
245
+ return { ok: true, orderId: data.orderId, orderStatus: status, requestId: typeof data.requestId === "string" ? data.requestId : null };
246
+ }
247
+
248
+ /**
249
+ * READ ONE ORDER by its hash. GET /orders?orderHash=<hash> -> { orders: [ ... ] }. The entry's `orderStatus` must be
250
+ * a known status word (else "uniswapx-bad-response"); an empty list means the service does not know the order
251
+ * ("uniswapx-order-unknown") - never silence, never null.
252
+ */
253
+ export async function uniswapxOrderStatus({ orderHash, chainId = null, fetchImpl = null, timeoutMs = 20_000, api = null } = {}) {
254
+ const chain = Number(chainId);
255
+ const base = api ? String(api) : (Number.isFinite(chain) ? uniswapxOrderbook(chain) : null);
256
+ if (!base) return refusal("uniswapx-unknown-network", { chainId: Number.isFinite(chain) ? chain : null });
257
+ if (!isHash(orderHash)) return refusal("uniswapx-bad-order", { field: "orderHash" });
258
+ const http = createHttp({ apiBase: base, fetchImpl, timeoutMs });
259
+ let res;
260
+ try {
261
+ res = await http.tryJson(UNISWAPX_ORDER_PATH + "?orderHash=" + orderHash);
262
+ } catch (error) {
263
+ return refusal("uniswapx-unreachable", { why: String((error && error.params && error.params.why) || (error && error.code) || error) });
264
+ }
265
+ if (!res || typeof res.ok !== "boolean") return refusal("uniswapx-bad-response", { where: "orders" });
266
+ if (!res.ok) return refusal("uniswapx-refused", { status: res.status, stage: "orders" });
267
+ const orders = res.body && res.body.orders;
268
+ if (!Array.isArray(orders)) return refusal("uniswapx-bad-response", { where: "orders" });
269
+ const found = orders.find((o) => o && String(o.orderHash || "").toLowerCase() === String(orderHash).toLowerCase()) || orders[0] || null;
270
+ if (!found) return refusal("uniswapx-order-unknown", { orderHash });
271
+ const status = found.orderStatus;
272
+ if (typeof status !== "string" || !UNISWAPX_STATUSES.includes(status)) return refusal("uniswapx-bad-response", { where: "orderStatus", got: typeof status === "string" ? status : null });
273
+ return { ok: true, status, order: found };
274
+ }
275
+
276
+ /**
277
+ * WHAT ENDS AN ASYNCHRONOUS PROVIDER'S EXECUTION - poll the order service until the order is settled or finally
278
+ * dead, with a timeout. Returns:
279
+ * { ok: true, done: true, settled: true, status: "filled" } - the swap settled;
280
+ * { ok: true, done: true, settled: false, status: "expired" | "error" | "cancelled" | ... } - finally dead;
281
+ * { ok: false, code: "uniswapx-not-settled" | "uniswapx-unreachable" | ... } - a NAMED refusal.
282
+ * "Not settled in time" is its OWN code: "we did not wait long enough" is a state of the order, not a breakdown.
283
+ * The clock and the sleep are injectable so the wait is testable without real time.
284
+ */
285
+ export async function uniswapxSettled(request = {}, deps = {}) {
286
+ const { orderHash, chainId = null } = request;
287
+ if (!isHash(orderHash)) return refusal("uniswapx-bad-order", { field: "orderHash" });
288
+ const now = typeof deps.now === "function" ? deps.now : () => Date.now();
289
+ const sleep = typeof deps.sleep === "function" ? deps.sleep : (ms) => new Promise((resolve) => setTimeout(resolve, ms));
290
+ const pollMs = Number.isFinite(deps.pollMs) && deps.pollMs > 0 ? deps.pollMs : 5_000;
291
+ const timeoutMs = Number.isFinite(deps.timeoutMs) && deps.timeoutMs > 0 ? deps.timeoutMs : 300_000;
292
+ const startedAt = now();
293
+ let lastStatus = null;
294
+ for (;;) {
295
+ const read = await uniswapxOrderStatus({ orderHash, chainId, fetchImpl: deps.fetchImpl, timeoutMs: deps.requestTimeoutMs, api: deps.api });
296
+ if (!read.ok) return read; // unreachable / bad-response / unknown: a named refusal, not silence
297
+ lastStatus = read.status;
298
+ if (UNISWAPX_FINAL_STATUSES.includes(lastStatus)) {
299
+ return { ok: true, done: true, settled: lastStatus === UNISWAPX_SETTLED_STATUS, status: lastStatus, orderHash };
300
+ }
301
+ if (now() - startedAt >= timeoutMs) return refusal("uniswapx-not-settled", { orderHash, status: lastStatus, waitedMs: now() - startedAt });
302
+ await sleep(pollMs);
303
+ }
304
+ }
305
+
306
+ /**
307
+ * THE SHARED PRE-SIGNATURE GUARDS, APPLIED TO THIS PROVIDER. The same guard functions the synchronous DEX leg and the
308
+ * other two providers use (engine.dex.priceGateVerdict, engine.dex.dexLegVerdict, engine.guards.allowanceVerdict),
309
+ * composed by the same summary guard (engine.guards.signingGateVerdict). Pure - no network, no wallet.
310
+ */
311
+ export function uniswapxGate({
312
+ order = null, requiredOutWei = null, tokenUsd = null, referenceNativeUsd = null, tolerated = false, gates = null,
313
+ sellDecimals = null, buyDecimals = null, buySymbol = null, allowanceWei = null, allowanceConsent = false,
314
+ tokenIsNative = false, symbol = null, decimals = null,
315
+ } = {}) {
316
+ if (!order) return { ok: false, blocked: true, reason: "uniswapx-no-order", checks: [] };
317
+ const output = Array.isArray(order.baseOutputs) && order.baseOutputs.length ? order.baseOutputs[0] : null;
318
+ const amountOut = output ? output.endAmount : null;
319
+ const price = engine.dex.priceGateVerdict({
320
+ amountInWei: order.baseInputEndAmount, amountOutWei: amountOut,
321
+ inDecimals: sellDecimals, outDecimals: buyDecimals,
322
+ tokenUsd: Number(tokenUsd) > 0 ? Number(tokenUsd) : null,
323
+ referenceUsd: Number(referenceNativeUsd) > 0 ? Number(referenceNativeUsd) : null,
324
+ stableSteps: [], tolerated: Boolean(tolerated), gates,
325
+ });
326
+ const leg = { ok: true, amountOutWei: amountOut === null ? null : String(amountOut), price, wrapped: { symbol: buySymbol, decimals: buyDecimals } };
327
+ const coverage = engine.dex.dexLegVerdict(leg, requiredOutWei);
328
+ const coverageCheck = {
329
+ ok: !coverage.blocked, checked: coverage.kind !== "unchecked" && coverage.kind !== "unquoted",
330
+ blocks: coverage.blocked === true, kind: coverage.kind, reason: coverage.reason,
331
+ };
332
+ const allowance = engine.guards.allowanceVerdict({
333
+ tokenIsNative, allowanceWei, amountWei: order.baseInputEndAmount,
334
+ consent: allowanceConsent === true, symbol, decimals: Number.isFinite(Number(decimals)) ? Number(decimals) : 18,
335
+ });
336
+ const gate = engine.guards.signingGateVerdict([coverageCheck, allowance]);
337
+ return { ...gate, price, coverage, allowance };
338
+ }
339
+
340
+ // THE PROVIDER, AS THE REGISTRY EXPECTS IT: id, kind, shape and, for "async", what ends execution (settled).
341
+ export const uniswapxProvider = Object.freeze({
342
+ id: "uniswapx",
343
+ kind: "uniswapx",
344
+ venue: UNISWAPX_VENUE,
345
+ shape: ASYNC,
346
+ // WHAT IT SETTLES: native coin. An UniswapX output token may be the NATIVE sentinel address(0)
347
+ // (uniswapx-spec.mjs), so this provider can carry a native need ALL THE WAY - it is a native leg of a composed
348
+ // route, or a single route on its own.
349
+ settles: "native",
350
+ networks: UNISWAPX_NETWORKS,
351
+ settlement: UNISWAPX_NETWORKS[1].reactor,
352
+ permit2: UNISWAPX_PERMIT2,
353
+ nativeToken: UNISWAPX_NATIVE,
354
+ orderFields: UNISWAPX_ORDER_FIELDS,
355
+ plan: uniswapxPlan,
356
+ settled: uniswapxSettled,
357
+ buildIntent: buildUniswapxIntent,
358
+ sign: uniswapxSignOrder,
359
+ submit: submitUniswapxOrder,
360
+ status: uniswapxOrderStatus,
361
+ gate: uniswapxGate,
362
+ });
package/src/quotes.mjs CHANGED
@@ -208,7 +208,7 @@ const compactOffer = (o) => ({
208
208
  // the price of the failed row to tell the person WHY there is no offer (and not just "none").
209
209
  const compactRefusal = (r) => ({ ...compactOffer(r), code: r.code, why: r.why });
210
210
 
211
- export function createQuotes({ config, http, now = () => Date.now(), evmCall = null, evmCode = null, providerAllowed = null, knownFactoryCodes = null, knownImplementationCodes = null }) {
211
+ export function createQuotes({ config, http, now = () => Date.now(), evmCall = null, evmCode = null, providerAllowed = null, knownFactoryCodes = null, knownImplementationCodes = null, denylist = null }) {
212
212
  // THE INTERFACE'S FACTORY POLICY (#103): the code hashes of factories it is willing to trade through
213
213
  // (extcodehash semantics = keccak256 of the runtime code). An EMPTY list is "no policy" (like
214
214
  // allowedProviders); a NON-EMPTY one refuses any factory whose code is not on it. Lower-cased once.
@@ -340,9 +340,19 @@ export function createQuotes({ config, http, now = () => Date.now(), evmCall = n
340
340
  if (!request || typeof request !== "object") fail("bad-input", { field: "request" });
341
341
  if (typeof request.providerId !== "string" || !request.providerId) fail("bad-input", { field: "providerId" });
342
342
  if (!request.order || typeof request.order !== "object") fail("bad-input", { field: "order" });
343
- let body = null;
343
+ // THE INTERFACE'S OWN ADDRESS DENYLIST (issue #16): POLICY, not proof - the interface refuses to ask for a
344
+ // quote for an address on its own list, and does it BEFORE any request is made. `order.locker` is the
345
+ // address the funds come from on a purchase (the claimer there is the node's own address, so it is
346
+ // deliberately not checked). An empty/absent list is "no policy"; the comparison is WITHOUT CASE.
347
+ if (denylist && typeof request.order.locker === "string" && denylist.has(request.order.locker.toLowerCase())) {
348
+ fail("address-denied", { step: "order-quote" });
349
+ }
350
+ let res = null;
344
351
  try {
345
- body = await http.json(config.route("orderQuote"), {
352
+ // THE ANSWER IS READ AS A VALUE (tryJson), NOT AS AN EXCEPTION: "did not answer" and "answered and
353
+ // refused" must stay different, and a refusal's own body carries the reason. `json` throws
354
+ // server-unavailable and LOSES that body.
355
+ res = await http.tryJson(config.route("orderQuote"), {
346
356
  method: "POST",
347
357
  headers: { "content-type": "application/json" },
348
358
  body: JSON.stringify({ providerId: request.providerId, order: request.order }),
@@ -351,7 +361,15 @@ export function createQuotes({ config, http, now = () => Date.now(), evmCall = n
351
361
  if (error && error.code === "bad-input") throw error;
352
362
  fail("server-unavailable", { step: "order-quote" });
353
363
  }
354
- if (!body || body.ok !== true || !body.quote) fail("quote-refused", { step: "order-quote" });
364
+ // A NODE'S DENYLIST REFUSAL REACHES THE APP UNDER ITS OWN NEUTRAL SDK CODE, WITHOUT the `why` text and
365
+ // WITHOUT the address: the node names both (the maker's denylist and the address), and the interface shows
366
+ // the person its own neutral message. Every other refusal keeps its existing code (quote-refused etc.).
367
+ const refusedCode = res && res.body && typeof res.body.code === "string" ? res.body.code : null;
368
+ if (refusedCode === "denied-address") fail("address-denied", { step: "order-quote" });
369
+ if (!res || res.ok !== true || !res.body || res.body.ok !== true || !res.body.quote) {
370
+ fail("quote-refused", { step: "order-quote" });
371
+ }
372
+ const body = res.body;
355
373
  const quote = body.quote;
356
374
  // A MISSING FIELD OF THE SIGNED SET IS NOT "PROBABLY ZERO": the digest cannot be built, so the quote is
357
375
  // not verifiable and is refused (its own code, so the interface can say which field is unbound).
@@ -460,7 +478,7 @@ export function createQuotes({ config, http, now = () => Date.now(), evmCall = n
460
478
  }
461
479
  // BINDING THE QUOTE TO OUR TERMS. The signature confirms the provider signed THIS quote, but not that it is
462
480
  // about our swap: we are responsible for the binding. The context string is computed by OUR engine module
463
- // (format arrakis-order-v3; the field order is part of the protocol and must not be rearranged). There is
481
+ // (format ninsei-order-v3; the field order is part of the protocol and must not be rearranged). There is
464
482
  // deliberately no copy of the format here: a copy would silently drift from the protocol.
465
483
  //
466
484
  // THE ORDER OF THE GATES: signature first, then binding. So a refusal names the nearest reason, not the first
package/src/swaps.mjs CHANGED
@@ -247,7 +247,7 @@ const recordInputOf = (request) => {
247
247
  // WHAT THE PERSON SAW: the maker, its spread, the fee, the rate and the quote signature. Screen data, but it must
248
248
  // live in the record - otherwise the progress screen shows an empty maker instead of whoever gave the price.
249
249
  makerId: pass(quote.makerId), makerName: pass(quote.makerName), makerSpread: pass(quote.makerSpread),
250
- quoteSignature: pass(quote.signature), fee: pass(quote.arrakisFee),
250
+ quoteSignature: pass(quote.signature), fee: pass(quote.ninseiFee),
251
251
  dexSpread: pass(quote.dexSpread), gasUsd: pass(quote.gasUsd), rate: pass(quote.rate),
252
252
  };
253
253
  };
@@ -264,6 +264,10 @@ export function createSwaps({ preflight, http, createRecord, mode = null, xmrNet
264
264
  // adapter (send/receipt): windows and message signatures remain the interface's. A READY pass arrives here (as
265
265
  // www/js/evm/auth.js, signIn obtains it today), and the core merely presents it.
266
266
  auth = null,
267
+ // THE INTERFACE'S ADDRESS DENYLIST (issue #16), a Set of lower-cased addresses already assembled by the facade.
268
+ // The check on the ACTUAL sender is done in start() below, ONE place, right before the send - the wallet adapter
269
+ // only HANDS OUT the current account (`address()`), it does not carry the policy.
270
+ denylist = null,
267
271
  watch: watchOptions = null } = {}) {
268
272
  // WHAT READS THE CHAIN AT THE LOCK IS A SEAM, as in actions.mjs: by default the engine's read, from outside you can
269
273
  // substitute your own. Otherwise the lock step could not be checked beyond predicting the address - and "the address
@@ -757,6 +761,19 @@ export function createSwaps({ preflight, http, createRecord, mode = null, xmrNet
757
761
  if (!wallet || typeof wallet.send !== "function" || typeof wallet.receipt !== "function") {
758
762
  throw new SdkError("bad-input", { field: "wallet", missing: ["send", "receipt"], surface: "swaps.start", step: "lock" });
759
763
  }
764
+ // THE INTERFACE'S ADDRESS DENYLIST, ON THE ACTUAL SENDER (issue #16). The person may have switched accounts
765
+ // in the wallet, so what matters is the address the funding transaction will really be sent from AT THIS
766
+ // MOMENT, not `order.locker` checked earlier. The wallet adapter hands out the current account (the adapter's
767
+ // `address()` reads `eth_accounts[0]`); the comparison lives HERE, one place, right before the send - not
768
+ // inside the adapter. The refusal happens BEFORE any request: no transaction is signed or sent.
769
+ if (denylist) {
770
+ const sender = typeof wallet.address === "function" ? await wallet.address() : null;
771
+ if (typeof sender !== "string" || !sender) {
772
+ // A HALF-ADAPTER CANNOT BE APPLIED THE POLICY TO: silently skipping would make the list skippable.
773
+ throw new SdkError("bad-input", { field: "wallet", missing: ["address"], surface: "swaps.start", step: "lock" });
774
+ }
775
+ if (denylist.has(sender.toLowerCase())) throw new SdkError("address-denied", { step: "lock" });
776
+ }
760
777
  // THE LOCK TERMS ARE THE SAME AS THE ORDER'S, and are taken from the caller: exactly these fields enter the
761
778
  // context the DLEQ proof is bound to (orderContext.js, FIELDS). The deadlines and composition are checked by the
762
779
  // engine (lock.send -> escrow.orderTerms) BEFORE sending, not by our own arithmetic: the contract sets the rules.