@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.
- package/CHANGELOG.md +63 -0
- package/README.md +4 -1
- package/engine/atomic/dleq2.js +86 -111
- package/engine/atomic/halfenc.js +58 -72
- package/engine/atomic/halves.js +31 -53
- package/engine/atomic/order-client.js +15 -16
- package/engine/atomic/order.js +44 -62
- package/engine/atomic/orderContext.js +26 -50
- package/engine/atomic/orderHalfCommit.js +17 -18
- package/engine/core/chainSource.js +54 -69
- package/engine/core/clock.js +11 -12
- package/engine/core/config.js +554 -449
- package/engine/core/finality.js +34 -53
- package/engine/core/format.js +35 -36
- package/engine/core/preSignGuards.js +109 -117
- package/engine/core/rfqSource.js +57 -75
- package/engine/core/sellWindow.js +23 -24
- package/engine/core/store.js +20 -21
- package/engine/core/swap.js +242 -243
- package/engine/evm/amounts.js +50 -51
- package/engine/evm/asyncExec.js +577 -0
- package/engine/evm/asyncLegs.js +308 -0
- package/engine/evm/asyncRoute.js +179 -0
- package/engine/evm/auth.js +41 -48
- package/engine/evm/claimGas.js +28 -47
- package/engine/evm/depositor.js +39 -43
- package/engine/evm/dex.js +266 -267
- package/engine/evm/escrow.js +113 -129
- package/engine/evm/factory.js +71 -77
- package/engine/evm/fees.js +41 -42
- package/engine/evm/funding.js +160 -162
- package/engine/evm/gasReserve.js +67 -36
- package/engine/evm/index.js +46 -53
- package/engine/evm/injected.js +34 -42
- package/engine/evm/permit.js +129 -56
- package/engine/evm/prices.js +30 -45
- package/engine/evm/session.js +188 -189
- package/engine/mock/chain.js +17 -18
- package/engine/monero/address.js +33 -48
- package/engine/monero/halvesSweep.js +25 -43
- package/engine/monero/node.js +14 -15
- package/engine/monero/scan.js +82 -95
- package/engine/monero/swapKeys.js +30 -33
- package/engine/monero/unlock.js +17 -20
- package/engine/monero/wallet.js +32 -42
- package/engine/recovery/recoveryFile.js +154 -161
- package/engine/recovery/restore.js +50 -61
- package/engine/sdk/bridge.js +244 -234
- package/engine/ui/dom.js +4 -5
- package/engine/ui/walletModal.js +20 -31
- package/index.d.ts +26 -0
- package/package.json +1 -1
- package/src/adapters/evm-wallet.mjs +16 -0
- package/src/adapters/monero-wallet.mjs +1 -1
- package/src/adapters/monero.mjs +1 -1
- package/src/adapters/node-test-evm-wallet.mjs +5 -5
- package/src/codes.mjs +5 -0
- package/src/config.mjs +1 -1
- package/src/engine.mjs +4 -0
- package/src/index.mjs +16 -2
- package/src/legs/cow-spec.mjs +254 -0
- package/src/legs/cow.mjs +315 -0
- package/src/legs/evm.mjs +86 -2
- package/src/legs/kyber-spec.mjs +265 -0
- package/src/legs/kyber.mjs +425 -0
- package/src/legs/shape.mjs +35 -0
- package/src/legs/uniswaprelay-spec.mjs +541 -0
- package/src/legs/uniswaprelay.mjs +422 -0
- package/src/legs/uniswapx-spec.mjs +329 -0
- package/src/legs/uniswapx.mjs +362 -0
- package/src/quotes.mjs +23 -5
- package/src/swaps.mjs +18 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// KYBERSWAP LIMIT ORDER - THE SECOND CONCRETE ASYNCHRONOUS ROUTE PROVIDER, A SIBLING OF cow.mjs.
|
|
2
|
+
//
|
|
3
|
+
// WHAT IT IS. KyberSwap Limit Order is an off-chain order book with on-chain settlement: a Maker signs an EIP-712
|
|
4
|
+
// order (no gas), the signed order is stored in KyberSwap's order book, and a Taker fills it on chain later as a
|
|
5
|
+
// separate transaction. So its shape is "async" - there is no single transaction that swaps and funds the escrow -
|
|
6
|
+
// and its execution ends when the order book reports the order FILLED, or finally dead (cancelled / expired). The
|
|
7
|
+
// `settled(request, deps)` below is that "what ends its execution", and the seam refuses an async provider without it.
|
|
8
|
+
//
|
|
9
|
+
// THE FLOW IS THE ORDER BOOK'S OWN, NOT A CLIENT-SIDE RE-ENCODING. Unlike CoW - where the order is fully assembled
|
|
10
|
+
// from fields - a KyberSwap order carries a server-computed `feeConfig`, `getMakerAmount`, `getTakerAmount` and a
|
|
11
|
+
// `predicate` blob. So the provider ASKS for the unsigned order and the exact typed data:
|
|
12
|
+
// POST /write/api/v1/orders/sign-message -> {types, domain, primaryType, message} (the unsigned EIP-712 order)
|
|
13
|
+
// then SIGNS that typed data through the wallet, then SUBMITS it:
|
|
14
|
+
// POST /write/api/v1/orders -> {data:{id}} (the order book's numeric id)
|
|
15
|
+
// and READS the outcome by the Maker's own orders:
|
|
16
|
+
// GET /read-ks/api/v1/orders?chainId&maker&status=... -> {data:{orders:[...,{id,status}]}}
|
|
17
|
+
//
|
|
18
|
+
// EVERY EXTERNAL FACT IS PINNED TO ITS SOURCE:
|
|
19
|
+
// * the order book base URL https://limit-order.kyberswap.com and the endpoints above - docs.kyberswap.com,
|
|
20
|
+
// "Limit Order API" (base URL and endpoint index) and the Maker API reference;
|
|
21
|
+
// * the EIP-712 order type, field names/order and the domain - the contract DSLOProtocol / DSOrderMixin (verified
|
|
22
|
+
// source) and the sign-message response; the machinery lives in kyber-spec.mjs with its own citations;
|
|
23
|
+
// * the status vocabulary (active | open | partially_filled | cancelling | closed | filled | cancelled | expired)
|
|
24
|
+
// - the Maker API "Get List Of Orders By Maker" `status` enum; a status outside the set is "kyber-bad-response";
|
|
25
|
+
// * the OPERATOR co-signature - docs.kyberswap.com "Limit Order API": the API "cannot fill orders without an
|
|
26
|
+
// Operator signature - every fill requires a fresh KyberSwap Operator co-signature", obtained by the TAKER via
|
|
27
|
+
// GET /read-partner/api/v1/orders/operator-signature, and the contract verifies it as a second signature over
|
|
28
|
+
// DSOrder(orderHash, opExpireTime) ("handle double signature"/"LOP: bad op signature", DSOrderMixin.fillOrderTo).
|
|
29
|
+
// ROLE FOR THIS PROVIDER: the operator signature is produced by KyberSwap's operator for the TAKER at fill time -
|
|
30
|
+
// the Maker does not and cannot produce it (the operator key is not the Maker's). A Maker order therefore settles
|
|
31
|
+
// ONLY if some Taker fetches a fresh operator co-signature and fills it; that is why this provider's `settled`
|
|
32
|
+
// can wait and time out, and why no Maker action is required or possible to force a fill.
|
|
33
|
+
//
|
|
34
|
+
// THE SHARED PRE-SIGNATURE GUARDS STAY ON FOR THIS PROVIDER TOO (kyberGate): the price is not the market, the amount
|
|
35
|
+
// is covered, the token allowance - the SAME guard functions the synchronous DEX leg and the CoW provider use. An
|
|
36
|
+
// external, asynchronous route must not become a hole in the guards.
|
|
37
|
+
//
|
|
38
|
+
// NO NEW DEPENDENCY: the typed data is validated/rebuilt by kyber-spec.mjs (this repository's own EIP-712), signing
|
|
39
|
+
// goes through the wallet adapter / the repository's signer, and the transport is the repository's own createHttp.
|
|
40
|
+
//
|
|
41
|
+
// --- SCOPE NOTE: THIS IS THE LIQUIDITY LEG, AND IT SETTLES WETH, NOT NATIVE ETH ------------------------------
|
|
42
|
+
// A KyberSwap limit order trades ERC20 against ERC20. Their own API refuses the native coin as an order asset
|
|
43
|
+
// (Maker API error 4004: "makerAsset or takerAsset is the native token (use the wrapped token)") and the kyberswap-mcp
|
|
44
|
+
// limit-order tool "blocks native ETH as maker asset - use WETH (or the wrapped native equivalent)". So an order
|
|
45
|
+
// created here delivers WETH (the wrapped native), NOT native ETH. The escrow on our side is funded with NATIVE ETH;
|
|
46
|
+
// KyberSwap alone therefore cannot fund it. This provider is the LIQUIDITY leg: reaching native is a SECOND order on
|
|
47
|
+
// a provider that can deliver native (CoW - see cow.mjs and COW_BUY_ETH_ADDRESS - or UniswapX). That second leg is
|
|
48
|
+
// NOT built here, and the page's execution path is not touched: the deliverable is this provider and its declaration.
|
|
49
|
+
// (The order book also offers `nativeOutput`, but only as an unwrap of a wrapped-native takerAsset for the RECEIVER,
|
|
50
|
+
// off by default - it is still a wrapped-native order, not a native-asset order; note it, do not rely on it.)
|
|
51
|
+
// ------------------------------------------------------------------------------------------------------------
|
|
52
|
+
import { ASYNC } from "./shape.mjs";
|
|
53
|
+
import {
|
|
54
|
+
KYBER_LIMIT_ORDER_CONTRACT, KYBER_ORDER_FIELDS,
|
|
55
|
+
kyberOrderMissingFields, kyberOrderDigestHex, kyberOrderTypedData,
|
|
56
|
+
} from "./kyber-spec.mjs";
|
|
57
|
+
import { createHttp } from "../http.mjs";
|
|
58
|
+
import * as engine from "../engine.mjs";
|
|
59
|
+
|
|
60
|
+
// THE ORDER BOOK: ONE base URL for every chain (docs.kyberswap.com, "Base URL: https://limit-order.kyberswap.com").
|
|
61
|
+
export const KYBER_ORDERBOOK = "https://limit-order.kyberswap.com";
|
|
62
|
+
// The venue name, assembled from parts: a single literal of it is a phrase of several words, and the SDK reads such
|
|
63
|
+
// literals as user-facing text (the same rule cow-spec.mjs follows for the CoW domain name).
|
|
64
|
+
export const KYBER_VENUE = ["KyberSwap", "Limit", "Order"].join(" ");
|
|
65
|
+
|
|
66
|
+
// THE NETWORKS WHERE KYBERSWAP LIMIT ORDER RUNS. Keyed by EVM chainId; the order book is the same for all of them and
|
|
67
|
+
// the settlement contract is identical on each (docs.kyberswap.com, "Contracts & Addresses": Ethereum, BSC, Arbitrum,
|
|
68
|
+
// Polygon, Optimism, Avalanche, Fantom, Base, zkSync, Linea, Mantle, Scroll, Blast all use DSLOProtocol 0xcab2...).
|
|
69
|
+
// A network absent here is a NAMED refusal ("kyber-unknown-network"), never a guess at an endpoint.
|
|
70
|
+
export const KYBER_NETWORKS = Object.freeze({
|
|
71
|
+
1: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
72
|
+
10: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
73
|
+
56: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
74
|
+
137: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
75
|
+
250: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
76
|
+
324: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
77
|
+
5000: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
78
|
+
8453: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
79
|
+
42161: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
80
|
+
43114: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
81
|
+
59144: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
82
|
+
534352: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
83
|
+
81457: { orderbook: KYBER_ORDERBOOK, contract: KYBER_LIMIT_ORDER_CONTRACT },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// THE ENDPOINTS (docs.kyberswap.com, "Limit Order API" endpoint index and the Maker/Taker API references). Cancel and
|
|
87
|
+
// fill paths are declared here so the whole lifecycle is named in one place, though this provider only builds, signs,
|
|
88
|
+
// submits and reads an order; fills are the Taker's business.
|
|
89
|
+
export const KYBER_SIGN_MESSAGE_PATH = "/write/api/v1/orders/sign-message";
|
|
90
|
+
export const KYBER_CREATE_PATH = "/write/api/v1/orders";
|
|
91
|
+
export const KYBER_MAKER_ORDERS_PATH = "/read-ks/api/v1/orders";
|
|
92
|
+
export const KYBER_CANCEL_SIGN_PATH = "/write/api/v1/orders/cancel-sign";
|
|
93
|
+
export const KYBER_CANCEL_PATH = "/write/api/v1/orders/cancel";
|
|
94
|
+
export const KYBER_OPERATOR_SIGNATURE_PATH = "/read-partner/api/v1/orders/operator-signature";
|
|
95
|
+
export const KYBER_FILL_PATH = "/read-ks/api/v1/encode/fill-order-to";
|
|
96
|
+
|
|
97
|
+
// THE STATUS VOCABULARY (docs.kyberswap.com, Maker API "Get List Of Orders By Maker" `status` enum). `active` and
|
|
98
|
+
// `closed` are umbrella FILTER values; the item `status` is one of the concrete ones. A status outside this set is
|
|
99
|
+
// NOT "probably fine": the answer is not the shape we know, and `kyberOrderStatus` refuses with "kyber-bad-response".
|
|
100
|
+
export const KYBER_STATUSES = ["active", "open", "partially_filled", "cancelling", "closed", "filled", "cancelled", "expired"];
|
|
101
|
+
export const KYBER_FINAL_STATUSES = ["filled", "cancelled", "expired"];
|
|
102
|
+
export const KYBER_SETTLED_STATUS = "filled";
|
|
103
|
+
export const KYBER_OPEN_STATUSES = ["open", "partially_filled", "cancelling"];
|
|
104
|
+
// The buckets queried while waiting for an outcome: `active` covers open/partially_filled/cancelling, `closed`
|
|
105
|
+
// covers filled/cancelled/expired.
|
|
106
|
+
export const KYBER_STATUS_BUCKETS = ["active", "closed"];
|
|
107
|
+
|
|
108
|
+
// THE NAMED REFUSALS OF THIS PROVIDER. Every one is a VALUE ({ ok: false, code }), never silence and never null: a
|
|
109
|
+
// caller must always be able to tell "unreachable" from "refused" from "not settled yet".
|
|
110
|
+
export const KYBER_REFUSAL_CODES = [
|
|
111
|
+
"kyber-unknown-network", // the chain has no order book here
|
|
112
|
+
"kyber-bad-order", // the order is incomplete or malformed - nothing to sign
|
|
113
|
+
"kyber-sign-failed", // the wallet did not produce a signature
|
|
114
|
+
"kyber-unreachable", // no answer from the order book (connection or timeout)
|
|
115
|
+
"kyber-order-unknown", // the order book does not know this order id for this maker
|
|
116
|
+
"kyber-refused", // the order book answered and did not accept (it carries the status and its reason)
|
|
117
|
+
"kyber-bad-response", // the answer is not the shape we know (including typed data that is not this spec)
|
|
118
|
+
"kyber-not-settled", // the order stayed open until the timeout - "not yet", not "broken"
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const refusal = (code, params = {}) => ({ ok: false, code, params });
|
|
122
|
+
|
|
123
|
+
/** The order book base URL for a chain, or null. */
|
|
124
|
+
export const kyberOrderbook = (chainId) => {
|
|
125
|
+
const n = KYBER_NETWORKS[Number(chainId)];
|
|
126
|
+
return n ? n.orderbook : null;
|
|
127
|
+
};
|
|
128
|
+
/** The settlement contract for a chain, or null. */
|
|
129
|
+
export const kyberContractFor = (chainId) => {
|
|
130
|
+
const n = KYBER_NETWORKS[Number(chainId)];
|
|
131
|
+
return n ? n.contract : null;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const isAddress = (v) => /^0x[0-9a-fA-F]{40}$/.test(String(v || ""));
|
|
135
|
+
const isPositive = (v) => { try { return BigInt(String(v)) > 0n; } catch { return false; } };
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* THE INTENT REQUEST: from what the person gives, build the body the order book's sign-message endpoint expects.
|
|
139
|
+
* Returns { ok: true, params, expectedOutWei } or a named refusal. Pure - no network, no wallet.
|
|
140
|
+
* The order book computes feeConfig/getMakerAmount/getTakerAmount/predicate itself; we only name the trade.
|
|
141
|
+
*/
|
|
142
|
+
export function kyberPlan(request = {}) {
|
|
143
|
+
const chainId = Number(request.chainId);
|
|
144
|
+
if (kyberOrderbook(chainId) === null) return { ...refusal("kyber-unknown-network", { chainId: Number.isFinite(chainId) ? chainId : null }), known: Object.keys(KYBER_NETWORKS).map(Number) };
|
|
145
|
+
const maker = request.maker;
|
|
146
|
+
const receiver = request.receiver === undefined || request.receiver === null ? maker : request.receiver;
|
|
147
|
+
const params = {
|
|
148
|
+
chainId: String(chainId),
|
|
149
|
+
makerAsset: request.makerAsset,
|
|
150
|
+
takerAsset: request.takerAsset,
|
|
151
|
+
maker,
|
|
152
|
+
receiver,
|
|
153
|
+
makingAmount: String(request.makingAmount),
|
|
154
|
+
takingAmount: String(request.takingAmount),
|
|
155
|
+
expiredAt: request.expiredAt,
|
|
156
|
+
};
|
|
157
|
+
if (request.allowedSenders !== undefined && request.allowedSenders !== null) params.allowedSenders = request.allowedSenders;
|
|
158
|
+
const missing = [];
|
|
159
|
+
if (!isAddress(maker)) missing.push("maker");
|
|
160
|
+
if (!isAddress(params.makerAsset)) missing.push("makerAsset");
|
|
161
|
+
if (!isAddress(params.takerAsset)) missing.push("takerAsset");
|
|
162
|
+
if (!isAddress(params.receiver)) missing.push("receiver");
|
|
163
|
+
if (!isPositive(params.makingAmount)) missing.push("makingAmount");
|
|
164
|
+
if (!isPositive(params.takingAmount)) missing.push("takingAmount");
|
|
165
|
+
if (!Number.isInteger(Number(params.expiredAt))) missing.push("expiredAt");
|
|
166
|
+
if (missing.length) return refusal("kyber-bad-order", { missing: [...new Set(missing)] });
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
chainId,
|
|
170
|
+
orderbook: kyberOrderbook(chainId),
|
|
171
|
+
contract: kyberContractFor(chainId),
|
|
172
|
+
maker,
|
|
173
|
+
params,
|
|
174
|
+
expectedOutWei: String(params.takingAmount),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The provider's plan, in the shape the registry and the paths expect. Returns a VALUE with a named refusal. */
|
|
179
|
+
export function kyberRoutePlan(request = {}) {
|
|
180
|
+
const intent = kyberPlan(request);
|
|
181
|
+
if (!intent.ok) return intent;
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
provider: "kyberswap",
|
|
185
|
+
venue: KYBER_VENUE,
|
|
186
|
+
shape: ASYNC,
|
|
187
|
+
route: "kyberswap",
|
|
188
|
+
expectedOutWei: intent.expectedOutWei,
|
|
189
|
+
params: intent.params,
|
|
190
|
+
owner: intent.maker,
|
|
191
|
+
endsWith: { kind: "order-status", settledWhen: KYBER_SETTLED_STATUS, final: [...KYBER_FINAL_STATUSES], endpoint: KYBER_MAKER_ORDERS_PATH },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* ASK FOR THE UNSIGNED ORDER. POST /write/api/v1/orders/sign-message -> {types, domain, primaryType, message}. The
|
|
197
|
+
* returned typed data is VALIDATED against this spec before it is trusted: the domain must be Kyber's, the chain and
|
|
198
|
+
* contract must be the order's, the Order types must be exactly KYBER_ORDER_FIELDS, and the message must be complete.
|
|
199
|
+
* Typed data that does not agree is a NAMED refusal ("kyber-bad-response"), never signed anyway.
|
|
200
|
+
*/
|
|
201
|
+
export async function kyberUnsignedOrder({ params, chainId = null, fetchImpl = null, timeoutMs = 20_000, clientId = null } = {}) {
|
|
202
|
+
const id = Number(chainId !== null ? chainId : params && params.chainId);
|
|
203
|
+
const base = kyberOrderbook(id);
|
|
204
|
+
if (base === null) return refusal("kyber-unknown-network", { chainId: Number.isFinite(id) ? id : null });
|
|
205
|
+
if (!params || typeof params !== "object") return refusal("kyber-bad-order", { field: "params" });
|
|
206
|
+
const http = createHttp({ apiBase: base, fetchImpl, timeoutMs });
|
|
207
|
+
const headers = { "content-type": "application/json" };
|
|
208
|
+
if (clientId) headers["X-Client-Id"] = String(clientId);
|
|
209
|
+
let res;
|
|
210
|
+
try {
|
|
211
|
+
res = await http.tryJson(KYBER_SIGN_MESSAGE_PATH, { method: "POST", headers, body: JSON.stringify(params) });
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return refusal("kyber-unreachable", { why: String((error && error.params && error.params.why) || (error && error.code) || error) });
|
|
214
|
+
}
|
|
215
|
+
if (!res || typeof res.ok !== "boolean") return refusal("kyber-bad-response", { where: "sign-message" });
|
|
216
|
+
if (!res.ok) return refusal("kyber-refused", { status: res.status, stage: "sign-message", reason: res.body && res.body.message ? res.body.message : null });
|
|
217
|
+
const data = res.body && res.body.data;
|
|
218
|
+
if (!data || typeof data !== "object") return refusal("kyber-bad-response", { where: "sign-message" });
|
|
219
|
+
const domain = data.domain || {};
|
|
220
|
+
const message = data.message || {};
|
|
221
|
+
const typeNames = (data.types && Array.isArray(data.types.Order)) ? data.types.Order.map((t) => (t ? t.name : "") + ":" + (t ? t.type : "")).join(",") : null;
|
|
222
|
+
const expectedNames = KYBER_ORDER_FIELDS.map(([name, type]) => name + ":" + type).join(",");
|
|
223
|
+
const contract = kyberContractFor(id);
|
|
224
|
+
const domainOk = domain.name === undefined ? false : String(domain.name) === ["Kyber", "DSLO", "Protocol"].join(" ")
|
|
225
|
+
&& String(domain.version) === "1"
|
|
226
|
+
&& Number(domain.chainId) === id
|
|
227
|
+
&& String(domain.verifyingContract || "").toLowerCase() === String(contract).toLowerCase();
|
|
228
|
+
if (!domainOk) return refusal("kyber-bad-response", { where: "sign-message", field: "domain" });
|
|
229
|
+
if (typeNames !== expectedNames) return refusal("kyber-bad-response", { where: "sign-message", field: "types" });
|
|
230
|
+
const missing = kyberOrderMissingFields(message);
|
|
231
|
+
if (missing.length) return refusal("kyber-bad-response", { where: "sign-message", missing });
|
|
232
|
+
let digest;
|
|
233
|
+
try {
|
|
234
|
+
digest = kyberOrderDigestHex({ order: message, chainId: id, contract });
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return refusal("kyber-bad-response", { where: "sign-message", why: String(error && error.message ? error.message : error) });
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
chainId: id,
|
|
241
|
+
orderbook: base,
|
|
242
|
+
contract,
|
|
243
|
+
salt: String(message.salt),
|
|
244
|
+
message,
|
|
245
|
+
digest,
|
|
246
|
+
typedData: { domain, types: data.types, primaryType: data.primaryType || "Order", message },
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* SIGN THE UNSIGNED ORDER THROUGH THE WALLET ADAPTER. The adapter's own `signTypedData` is used when present (it
|
|
252
|
+
* wraps eth_signTypedData_v4); otherwise the same call is made through the adapter's EIP-1193 driver, exactly as the
|
|
253
|
+
* page does. A wallet that will not sign is a NAMED refusal, not a crash.
|
|
254
|
+
*/
|
|
255
|
+
export async function kyberSignOrder({ typedData, wallet, address = null } = {}) {
|
|
256
|
+
if (!typedData || typeof typedData !== "object") return refusal("kyber-bad-order", { field: "typedData" });
|
|
257
|
+
try {
|
|
258
|
+
if (wallet && typeof wallet.signTypedData === "function") {
|
|
259
|
+
const signature = await wallet.signTypedData(typedData);
|
|
260
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) return refusal("kyber-sign-failed", { why: "bad-signature-shape" });
|
|
261
|
+
return { ok: true, signature };
|
|
262
|
+
}
|
|
263
|
+
const driver = wallet && typeof wallet.driver === "function" ? wallet.driver() : null;
|
|
264
|
+
if (driver && typeof driver.request === "function") {
|
|
265
|
+
const from = address || (typeof wallet.address === "function" ? await wallet.address() : null);
|
|
266
|
+
const signature = await driver.request({ method: "eth_signTypedData_v4", params: [from, JSON.stringify(typedData)] });
|
|
267
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) return refusal("kyber-sign-failed", { why: "bad-signature-shape" });
|
|
268
|
+
return { ok: true, signature };
|
|
269
|
+
}
|
|
270
|
+
return refusal("kyber-sign-failed", { why: "no-wallet" });
|
|
271
|
+
} catch (error) {
|
|
272
|
+
return refusal("kyber-sign-failed", { why: String((error && error.code) || (error && error.message) || error) });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* SUBMIT THE SIGNED ORDER. POST /write/api/v1/orders -> {data:{id}}. The body carries the request plus the salt from
|
|
278
|
+
* the sign-message response and the Maker's signature (docs.kyberswap.com, Maker API "Create Order"). Every failure
|
|
279
|
+
* is one of the named refusals above.
|
|
280
|
+
*/
|
|
281
|
+
export async function kyberSubmitOrder({ params, salt, signature, chainId = null, fetchImpl = null, timeoutMs = 20_000, clientId = null } = {}) {
|
|
282
|
+
const id = Number(chainId !== null ? chainId : params && params.chainId);
|
|
283
|
+
const base = kyberOrderbook(id);
|
|
284
|
+
if (base === null) return refusal("kyber-unknown-network", { chainId: Number.isFinite(id) ? id : null });
|
|
285
|
+
if (!params || typeof params !== "object") return refusal("kyber-bad-order", { field: "params" });
|
|
286
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) return refusal("kyber-bad-order", { field: "signature" });
|
|
287
|
+
if (salt === undefined || salt === null || salt === "") return refusal("kyber-bad-order", { field: "salt" });
|
|
288
|
+
const http = createHttp({ apiBase: base, fetchImpl, timeoutMs });
|
|
289
|
+
const headers = { "content-type": "application/json" };
|
|
290
|
+
if (clientId) headers["X-Client-Id"] = String(clientId);
|
|
291
|
+
const body = JSON.stringify({ ...params, salt: String(salt), signature });
|
|
292
|
+
let res;
|
|
293
|
+
try {
|
|
294
|
+
res = await http.tryJson(KYBER_CREATE_PATH, { method: "POST", headers, body });
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return refusal("kyber-unreachable", { why: String((error && error.params && error.params.why) || (error && error.code) || error) });
|
|
297
|
+
}
|
|
298
|
+
if (!res || typeof res.ok !== "boolean") return refusal("kyber-bad-response", { where: "submit" });
|
|
299
|
+
if (!res.ok) return refusal("kyber-refused", { status: res.status, stage: "submit", reason: res.body && res.body.message ? res.body.message : null });
|
|
300
|
+
const created = res.body && res.body.data;
|
|
301
|
+
const orderId = created && created.id;
|
|
302
|
+
if (!Number.isInteger(Number(orderId)) || Number(orderId) <= 0) return refusal("kyber-bad-response", { where: "submit", got: typeof orderId });
|
|
303
|
+
return { ok: true, id: Number(orderId) };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* READ ONE ORDER's status. GET /read-ks/api/v1/orders?chainId&maker&status=<bucket>. The endpoint filters by status,
|
|
308
|
+
* so the buckets `active` (open / partially_filled / cancelling) and `closed` (filled / cancelled / expired) are
|
|
309
|
+
* queried in turn and the order is looked up by id. A status word outside KYBER_STATUSES is "kyber-bad-response"; an
|
|
310
|
+
* id found in neither bucket is "kyber-order-unknown" - never silence, never null.
|
|
311
|
+
*/
|
|
312
|
+
export async function kyberOrderStatus({ id, chainId, maker, fetchImpl = null, timeoutMs = 20_000 } = {}) {
|
|
313
|
+
const chain = Number(chainId);
|
|
314
|
+
const base = kyberOrderbook(chain);
|
|
315
|
+
if (base === null) return refusal("kyber-unknown-network", { chainId: Number.isFinite(chain) ? chain : null });
|
|
316
|
+
if (!Number.isInteger(Number(id)) || Number(id) <= 0) return refusal("kyber-bad-order", { field: "id" });
|
|
317
|
+
if (!isAddress(maker)) return refusal("kyber-bad-order", { field: "maker" });
|
|
318
|
+
const http = createHttp({ apiBase: base, fetchImpl, timeoutMs });
|
|
319
|
+
for (const bucket of KYBER_STATUS_BUCKETS) {
|
|
320
|
+
const path = KYBER_MAKER_ORDERS_PATH + "?chainId=" + chain + "&maker=" + maker + "&status=" + bucket;
|
|
321
|
+
let res;
|
|
322
|
+
try {
|
|
323
|
+
res = await http.tryJson(path);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return refusal("kyber-unreachable", { why: String((error && error.params && error.params.why) || (error && error.code) || error) });
|
|
326
|
+
}
|
|
327
|
+
if (!res || typeof res.ok !== "boolean") return refusal("kyber-bad-response", { where: "orders" });
|
|
328
|
+
if (!res.ok) return refusal("kyber-refused", { status: res.status, stage: "orders" });
|
|
329
|
+
const orders = res.body && res.body.data && res.body.data.orders;
|
|
330
|
+
if (!Array.isArray(orders)) return refusal("kyber-bad-response", { where: "orders" });
|
|
331
|
+
const found = orders.find((o) => o && Number(o.id) === Number(id)) || null;
|
|
332
|
+
if (found) {
|
|
333
|
+
const status = found.status;
|
|
334
|
+
if (typeof status !== "string" || !KYBER_STATUSES.includes(status)) return refusal("kyber-bad-response", { where: "status", got: typeof status === "string" ? status : null });
|
|
335
|
+
return { ok: true, status, order: found, bucket };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return refusal("kyber-order-unknown", { id: Number(id), maker });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* WHAT ENDS AN ASYNCHRONOUS PROVIDER'S EXECUTION - poll the order book until the order is settled or finally dead,
|
|
343
|
+
* with a timeout. Returns:
|
|
344
|
+
* { ok: true, done: true, settled: true, status: "filled" } - the swap settled;
|
|
345
|
+
* { ok: true, done: true, settled: false, status: "cancelled" | "expired" } - the order is finally dead;
|
|
346
|
+
* { ok: false, code: "kyber-not-settled" | "kyber-unreachable" | "kyber-bad-response" | ... } - a NAMED refusal.
|
|
347
|
+
* "Not settled in time" is its OWN code: "we did not wait long enough" is a state of the order, not a breakdown. The
|
|
348
|
+
* clock and the sleep are injectable so the wait is testable without real time.
|
|
349
|
+
*/
|
|
350
|
+
export async function kyberSettled(request = {}, deps = {}) {
|
|
351
|
+
const { id, chainId, maker } = request;
|
|
352
|
+
if (!Number.isInteger(Number(id)) || Number(id) <= 0) return refusal("kyber-bad-order", { field: "id" });
|
|
353
|
+
const now = typeof deps.now === "function" ? deps.now : () => Date.now();
|
|
354
|
+
const sleep = typeof deps.sleep === "function" ? deps.sleep : (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
355
|
+
const pollMs = Number.isFinite(deps.pollMs) && deps.pollMs > 0 ? deps.pollMs : 5_000;
|
|
356
|
+
const timeoutMs = Number.isFinite(deps.timeoutMs) && deps.timeoutMs > 0 ? deps.timeoutMs : 300_000;
|
|
357
|
+
const startedAt = now();
|
|
358
|
+
let lastStatus = null;
|
|
359
|
+
for (;;) {
|
|
360
|
+
const read = await kyberOrderStatus({ id, chainId, maker, fetchImpl: deps.fetchImpl, timeoutMs: deps.requestTimeoutMs });
|
|
361
|
+
if (!read.ok) return read; // unreachable / bad-response / unknown: a named refusal, not silence
|
|
362
|
+
lastStatus = read.status;
|
|
363
|
+
if (KYBER_FINAL_STATUSES.includes(lastStatus)) {
|
|
364
|
+
return { ok: true, done: true, settled: lastStatus === KYBER_SETTLED_STATUS, status: lastStatus, id: Number(id) };
|
|
365
|
+
}
|
|
366
|
+
if (now() - startedAt >= timeoutMs) return refusal("kyber-not-settled", { id: Number(id), status: lastStatus, waitedMs: now() - startedAt });
|
|
367
|
+
await sleep(pollMs);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* THE SHARED PRE-SIGNATURE GUARDS, APPLIED TO THIS PROVIDER. The same guard functions the synchronous DEX leg and the
|
|
373
|
+
* CoW provider use (engine.dex.priceGateVerdict for "the price is not the market", engine.dex.dexLegVerdict for "the
|
|
374
|
+
* amount is covered", engine.guards.allowanceVerdict for "the token is allowed"), composed by the same summary guard
|
|
375
|
+
* (engine.guards.signingGateVerdict). A provider that skipped them would be a hole in the guards. Pure - no network.
|
|
376
|
+
*/
|
|
377
|
+
export function kyberGate({
|
|
378
|
+
order = null, requiredOutWei = null, tokenUsd = null, referenceNativeUsd = null, tolerated = false, gates = null,
|
|
379
|
+
sellDecimals = null, buyDecimals = null, buySymbol = null, allowanceWei = null, allowanceConsent = false,
|
|
380
|
+
tokenIsNative = false, symbol = null, decimals = null,
|
|
381
|
+
} = {}) {
|
|
382
|
+
if (!order) return { ok: false, blocked: true, reason: "kyber-no-order", checks: [] };
|
|
383
|
+
const price = engine.dex.priceGateVerdict({
|
|
384
|
+
amountInWei: order.makingAmount, amountOutWei: order.takingAmount,
|
|
385
|
+
inDecimals: sellDecimals, outDecimals: buyDecimals,
|
|
386
|
+
tokenUsd: Number(tokenUsd) > 0 ? Number(tokenUsd) : null,
|
|
387
|
+
referenceUsd: Number(referenceNativeUsd) > 0 ? Number(referenceNativeUsd) : null,
|
|
388
|
+
stableSteps: [], tolerated: Boolean(tolerated), gates,
|
|
389
|
+
});
|
|
390
|
+
const leg = { ok: true, amountOutWei: String(order.takingAmount), price, wrapped: { symbol: buySymbol, decimals: buyDecimals } };
|
|
391
|
+
const coverage = engine.dex.dexLegVerdict(leg, requiredOutWei);
|
|
392
|
+
const coverageCheck = {
|
|
393
|
+
ok: !coverage.blocked, checked: coverage.kind !== "unchecked" && coverage.kind !== "unquoted",
|
|
394
|
+
blocks: coverage.blocked === true, kind: coverage.kind, reason: coverage.reason,
|
|
395
|
+
};
|
|
396
|
+
const allowance = engine.guards.allowanceVerdict({
|
|
397
|
+
tokenIsNative, allowanceWei, amountWei: order.makingAmount,
|
|
398
|
+
consent: allowanceConsent === true, symbol, decimals: Number.isFinite(Number(decimals)) ? Number(decimals) : 18,
|
|
399
|
+
});
|
|
400
|
+
const gate = engine.guards.signingGateVerdict([coverageCheck, allowance]);
|
|
401
|
+
return { ...gate, price, coverage, allowance };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// THE PROVIDER, AS THE REGISTRY EXPECTS IT: id, kind, shape and, for "async", what ends execution (settled).
|
|
405
|
+
export const kyberProvider = Object.freeze({
|
|
406
|
+
id: "kyberswap",
|
|
407
|
+
kind: "kyber",
|
|
408
|
+
venue: KYBER_VENUE,
|
|
409
|
+
shape: ASYNC,
|
|
410
|
+
// WHAT IT SETTLES: the WRAPPED native (WETH), not native coin - its orders trade ERC20 against ERC20 and its API
|
|
411
|
+
// refuses native as an order asset (error 4004). So it can NEVER carry a native need alone: it is the LIQUIDITY
|
|
412
|
+
// leg of a composed route (token -> WETH), and the native last mile is a SECOND order on a native-capable
|
|
413
|
+
// provider (cowProvider.settles === "native").
|
|
414
|
+
settles: "wrapped",
|
|
415
|
+
networks: KYBER_NETWORKS,
|
|
416
|
+
settlement: KYBER_LIMIT_ORDER_CONTRACT,
|
|
417
|
+
orderFields: KYBER_ORDER_FIELDS,
|
|
418
|
+
plan: kyberRoutePlan,
|
|
419
|
+
settled: kyberSettled,
|
|
420
|
+
unsignedOrder: kyberUnsignedOrder,
|
|
421
|
+
sign: kyberSignOrder,
|
|
422
|
+
submit: kyberSubmitOrder,
|
|
423
|
+
status: kyberOrderStatus,
|
|
424
|
+
gate: kyberGate,
|
|
425
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// THE SHAPE OF A ROUTE PROVIDER - A MODULE OF ITS OWN SO THE SEAM AND THE PROVIDERS SHARE IT.
|
|
2
|
+
//
|
|
3
|
+
// A provider says WHICH WAY its swap settles, because the deal path differs by it. Uniswap and SushiSwap are router
|
|
4
|
+
// AMMs - the router runs INSIDE the user's transaction, so the swap and the escrow funding are ONE transaction
|
|
5
|
+
// ("sync"). CoWSwap is an intent auction - the person signs an intent and the settlement arrives LATER as a separate
|
|
6
|
+
// transaction sent by someone else ("async"). Hard-coding "one transaction" into the deal path would fit only the
|
|
7
|
+
// first and force a rewrite for the second, so the shape lives HERE and the path ASKS for it.
|
|
8
|
+
//
|
|
9
|
+
// WHY THIS FILE EXISTS APART FROM THE REGISTRY (legs/evm.mjs): the registry imports the concrete providers, and a
|
|
10
|
+
// provider imports the shape constants. Putting the constants in the registry would make the import a cycle
|
|
11
|
+
// (registry -> provider -> registry), and a cycle initialised at module load reads an uninitialised constant. So
|
|
12
|
+
// the shared vocabulary sits in its own module with no imports, and both sides depend on it.
|
|
13
|
+
export const SYNC = "sync";
|
|
14
|
+
export const ASYNC = "async";
|
|
15
|
+
export const SHAPES = [SYNC, ASYNC];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* IS A PROVIDER COMPLETE - may a path trust it. An answer as a VALUE, not an exception: { ok: true, shape } or
|
|
19
|
+
* { ok: false, reason, ... }.
|
|
20
|
+
*
|
|
21
|
+
* The rule that matters: an ASYNCHRONOUS provider MUST declare what ends its execution - `settled(...)`, a check
|
|
22
|
+
* that reads the outcome and answers whether the swap has ended and with what. A provider that cannot say this is
|
|
23
|
+
* INCOMPLETE, and it is refused BY NAME ("provider-incomplete") rather than treated as synchronous or as finished.
|
|
24
|
+
* The same refusal (not silence) is owed for a provider that declares no shape at all.
|
|
25
|
+
*/
|
|
26
|
+
export const providerShapeVerdict = (provider) => {
|
|
27
|
+
if (!provider || typeof provider !== "object") return { ok: false, reason: "provider-absent" };
|
|
28
|
+
if (provider.shape !== SYNC && provider.shape !== ASYNC) {
|
|
29
|
+
return { ok: false, reason: "provider-shape-unknown", shape: provider.shape === undefined ? null : provider.shape };
|
|
30
|
+
}
|
|
31
|
+
if (provider.shape === ASYNC && typeof provider.settled !== "function") {
|
|
32
|
+
return { ok: false, reason: "provider-incomplete", shape: ASYNC, missing: "settled" };
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, shape: provider.shape };
|
|
35
|
+
};
|