@ninsei-labs/sdk 0.39.3 → 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 +35 -0
- 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 +8 -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/config.mjs +1 -1
- package/src/engine.mjs +4 -0
- 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 +1 -1
- package/src/swaps.mjs +1 -1
package/src/legs/cow.mjs
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// COWSWAP - THE FIRST CONCRETE ASYNCHRONOUS ROUTE PROVIDER.
|
|
2
|
+
//
|
|
3
|
+
// WHAT IT IS. CoWSwap is an intent auction: the person signs an EIP-712 intent (an "order"), the order goes to a
|
|
4
|
+
// public order book, and solvers settle it LATER as a separate transaction someone else sends. So its shape is
|
|
5
|
+
// "async" - there is no single transaction that swaps and funds the escrow - and its execution ends when the order
|
|
6
|
+
// book reports the order SETTLED (fulfilled), or finally dead (cancelled / expired). `settled(request, deps)` below
|
|
7
|
+
// is that "what ends its execution", and the seam refuses an async provider that cannot name it.
|
|
8
|
+
//
|
|
9
|
+
// EVERY EXTERNAL FACT IS PINNED TO ITS SOURCE:
|
|
10
|
+
// * the order book base URLs (https://api.cow.fi/<slug>) - the order book OpenAPI `servers` list,
|
|
11
|
+
// github.com/cowprotocol/services, crates/orderbook/openapi.yml (and docs.cow.fi "Order book API");
|
|
12
|
+
// * POST /api/v1/orders -> 201 with the order UID (a 56-byte hex string) - the same OpenAPI (createOrder);
|
|
13
|
+
// * GET /api/v1/orders/{UID} -> the order, with `status` one of presignaturePending | open | fulfilled |
|
|
14
|
+
// cancelled | expired - the same OpenAPI (OrderStatus enum, getOrder);
|
|
15
|
+
// * POST body fields (sellToken, buyToken, receiver, sellAmount, buyAmount, validTo, appData, feeAmount, kind,
|
|
16
|
+
// partiallyFillable, sellTokenBalance, buyTokenBalance, signingScheme, signature, from) - createOrder and the
|
|
17
|
+
// OrderCreation schema (docs.cow.fi, API integration; the per-endpoint docs mirror the OpenAPI);
|
|
18
|
+
// * the settlement contract and the domain - cow-spec.mjs (GPv2Settlement / GPv2Signing sources).
|
|
19
|
+
//
|
|
20
|
+
// THE SHARED PRE-SIGNATURE GUARDS STAY ON FOR THIS PROVIDER TOO (cowGate): the price is not the market, the amount
|
|
21
|
+
// is covered, the token allowance - the SAME guard functions the synchronous DEX leg uses. An external, asynchronous
|
|
22
|
+
// route must not become a hole in the guards.
|
|
23
|
+
//
|
|
24
|
+
// NO NEW DEPENDENCY: the typed data is built by cow-spec.mjs (this repository's own EIP-712), signing goes through
|
|
25
|
+
// the wallet adapter / the repository's signer, and the transport is the repository's own createHttp.
|
|
26
|
+
import { ASYNC } from "./shape.mjs";
|
|
27
|
+
import {
|
|
28
|
+
COW_SETTLEMENT, COW_BUY_ETH_ADDRESS, COW_ORDER_FIELDS,
|
|
29
|
+
cowOrderMissingFields, cowOrderDigestHex, cowOrderUid, cowOrderTypedData,
|
|
30
|
+
} from "./cow-spec.mjs";
|
|
31
|
+
import { createHttp } from "../http.mjs";
|
|
32
|
+
import * as engine from "../engine.mjs";
|
|
33
|
+
|
|
34
|
+
// THE NETWORKS WHERE COWSWAP RUNS. Keyed by EVM chainId; the base URL comes from the order book OpenAPI `servers`
|
|
35
|
+
// list. A network absent here is a NAMED refusal ("cow-unknown-network"), never a guess at an endpoint.
|
|
36
|
+
// The settlement contract is the same on every one of them (see cow-spec.mjs).
|
|
37
|
+
export const COW_NETWORKS = Object.freeze({
|
|
38
|
+
1: { slug: "mainnet", orderbook: "https://api.cow.fi/mainnet" },
|
|
39
|
+
100: { slug: "xdai", orderbook: "https://api.cow.fi/xdai" },
|
|
40
|
+
42161: { slug: "arbitrum_one", orderbook: "https://api.cow.fi/arbitrum_one" },
|
|
41
|
+
8453: { slug: "base", orderbook: "https://api.cow.fi/base" },
|
|
42
|
+
56: { slug: "bnb", orderbook: "https://api.cow.fi/bnb" },
|
|
43
|
+
11155111: { slug: "sepolia", orderbook: "https://api.cow.fi/sepolia" },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// THE STATUS VOCABULARY OF THE ORDER BOOK (OpenAPI: OrderStatus). A status outside this set is NOT "probably fine":
|
|
47
|
+
// it means the answer is not the shape we know, and `cowOrderStatus` refuses with "cow-bad-response".
|
|
48
|
+
export const COW_STATUSES = ["presignaturePending", "open", "fulfilled", "cancelled", "expired"];
|
|
49
|
+
export const COW_FINAL_STATUSES = ["fulfilled", "cancelled", "expired"];
|
|
50
|
+
export const COW_SETTLED_STATUS = "fulfilled";
|
|
51
|
+
export const COW_OPEN_STATUSES = ["presignaturePending", "open"];
|
|
52
|
+
|
|
53
|
+
// THE NAMED REFUSALS OF THIS PROVIDER. Every one of them is a VALUE ({ ok: false, code }), never silence and never
|
|
54
|
+
// null: a caller must always be able to tell "unreachable" from "refused" from "not settled yet".
|
|
55
|
+
export const COW_REFUSAL_CODES = [
|
|
56
|
+
"cow-unknown-network", // the chain has no order book here
|
|
57
|
+
"cow-bad-order", // the order is incomplete or malformed - nothing to sign
|
|
58
|
+
"cow-sign-failed", // the wallet did not produce a signature
|
|
59
|
+
"cow-unreachable", // no answer from the order book (connection or timeout)
|
|
60
|
+
"cow-order-unknown", // the order book answered 404: no such order
|
|
61
|
+
"cow-refused", // the order book answered and did not accept (it carries the status and its reason)
|
|
62
|
+
"cow-bad-response", // the answer is not the shape we know
|
|
63
|
+
"cow-not-settled", // the order stayed open until the timeout - "not yet", not "broken"
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const refusal = (code, params = {}) => ({ ok: false, code, params });
|
|
67
|
+
|
|
68
|
+
/** The order book base URL for a chain, or null. */
|
|
69
|
+
export const cowOrderbook = (chainId) => {
|
|
70
|
+
const n = COW_NETWORKS[Number(chainId)];
|
|
71
|
+
return n ? n.orderbook : null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const isAddress = (v) => /^0x[0-9a-fA-F]{40}$/.test(String(v || ""));
|
|
75
|
+
const isBytes32 = (v) => /^(0x)?[0-9a-fA-F]{64}$/.test(String(v || ""));
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* THE INTENT: from what the person gives, build the order, its EIP-712 typed data, its digest and its UID.
|
|
79
|
+
* Returns { ok: true, order, typedData, digest, uid, owner } or a named refusal. Pure - no network, no wallet.
|
|
80
|
+
*/
|
|
81
|
+
export function buildCowIntent(request = {}) {
|
|
82
|
+
const chainId = Number(request.chainId);
|
|
83
|
+
if (cowOrderbook(chainId) === null) return { ...refusal("cow-unknown-network", { chainId: Number.isFinite(chainId) ? chainId : null }), known: Object.keys(COW_NETWORKS).map(Number) };
|
|
84
|
+
const owner = request.owner;
|
|
85
|
+
const receiver = request.receiver === undefined || request.receiver === null ? owner : request.receiver;
|
|
86
|
+
const order = {
|
|
87
|
+
sellToken: request.sellToken,
|
|
88
|
+
buyToken: request.buyToken,
|
|
89
|
+
receiver: receiver || owner,
|
|
90
|
+
sellAmount: request.sellAmountWei,
|
|
91
|
+
buyAmount: request.buyAmountWei,
|
|
92
|
+
validTo: request.validTo,
|
|
93
|
+
appData: request.appData === undefined || request.appData === null ? "0x" + "00".repeat(32) : request.appData,
|
|
94
|
+
feeAmount: request.feeAmountWei === undefined || request.feeAmountWei === null ? "0" : request.feeAmountWei,
|
|
95
|
+
kind: request.kind === undefined || request.kind === null ? "sell" : request.kind,
|
|
96
|
+
partiallyFillable: request.partiallyFillable === undefined || request.partiallyFillable === null ? false : request.partiallyFillable,
|
|
97
|
+
sellTokenBalance: request.sellTokenBalance === undefined || request.sellTokenBalance === null ? "erc20" : request.sellTokenBalance,
|
|
98
|
+
buyTokenBalance: request.buyTokenBalance === undefined || request.buyTokenBalance === null ? "erc20" : request.buyTokenBalance,
|
|
99
|
+
};
|
|
100
|
+
const missing = cowOrderMissingFields(order);
|
|
101
|
+
if (!isAddress(owner)) missing.push("owner");
|
|
102
|
+
if (!isBytes32(order.appData)) missing.push("appData");
|
|
103
|
+
if (!isAddress(order.sellToken) || !isAddress(order.buyToken) || !isAddress(order.receiver)) missing.push("token");
|
|
104
|
+
if (missing.length) return refusal("cow-bad-order", { missing: [...new Set(missing)] });
|
|
105
|
+
try {
|
|
106
|
+
const digest = cowOrderDigestHex({ order, chainId, settlement: COW_SETTLEMENT });
|
|
107
|
+
const uid = cowOrderUid({ digest, owner, validTo: order.validTo });
|
|
108
|
+
const typedData = cowOrderTypedData({ order, chainId, settlement: COW_SETTLEMENT });
|
|
109
|
+
return { ok: true, chainId, owner, order, digest, uid, typedData };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// A malformed number/address inside the encoder: a NAMED refusal, not an exception escaping to the caller.
|
|
112
|
+
return refusal("cow-bad-order", { why: String(error && error.message ? error.message : error) });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// THE PAYLOAD THE ORDER BOOK EXPECTS (createOrder / OrderCreation). Every field is set EXPLICITLY - no relying on a
|
|
117
|
+
// server default that could contradict the signed order: the signed fields must travel exactly as signed.
|
|
118
|
+
export function cowOrderSubmission({ order, signature, owner, signingScheme = "eip712", from = null }) {
|
|
119
|
+
return {
|
|
120
|
+
sellToken: order.sellToken,
|
|
121
|
+
buyToken: order.buyToken,
|
|
122
|
+
receiver: order.receiver,
|
|
123
|
+
sellAmount: String(order.sellAmount),
|
|
124
|
+
buyAmount: String(order.buyAmount),
|
|
125
|
+
validTo: Number(order.validTo),
|
|
126
|
+
appData: String(order.appData),
|
|
127
|
+
feeAmount: String(order.feeAmount),
|
|
128
|
+
kind: String(order.kind),
|
|
129
|
+
partiallyFillable: Boolean(order.partiallyFillable),
|
|
130
|
+
sellTokenBalance: String(order.sellTokenBalance),
|
|
131
|
+
buyTokenBalance: String(order.buyTokenBalance),
|
|
132
|
+
signingScheme,
|
|
133
|
+
signature,
|
|
134
|
+
from: from || owner,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* THE PROVIDER'S PLAN. For an async provider the plan is the INTENT: the signed order that will be submitted, with
|
|
140
|
+
* the typed data, the digest and the UID, plus what ends execution. Returns { ok, ... } - a value.
|
|
141
|
+
*/
|
|
142
|
+
export function cowPlan(request = {}) {
|
|
143
|
+
const intent = buildCowIntent(request);
|
|
144
|
+
if (!intent.ok) return intent;
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
provider: "cowswap",
|
|
148
|
+
venue: "CoWSwap",
|
|
149
|
+
shape: ASYNC,
|
|
150
|
+
route: "cowswap",
|
|
151
|
+
expectedOutWei: String(intent.order.buyAmount),
|
|
152
|
+
order: intent.order,
|
|
153
|
+
typedData: intent.typedData,
|
|
154
|
+
digest: intent.digest,
|
|
155
|
+
uid: intent.uid,
|
|
156
|
+
owner: intent.owner,
|
|
157
|
+
endsWith: { kind: "order-status", settledWhen: COW_SETTLED_STATUS, final: [...COW_FINAL_STATUSES], endpoint: "/api/v1/orders/{uid}" },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* SIGN THE INTENT THROUGH THE WALLET ADAPTER. The adapter's own `signTypedData` is used when present (it wraps
|
|
163
|
+
* eth_signTypedData_v4); otherwise the same call is made through the adapter's EIP-1193 driver, exactly as the page
|
|
164
|
+
* does (www/js/evm/session.js, signTypedData). A wallet that will not sign is a NAMED refusal, not a crash.
|
|
165
|
+
*/
|
|
166
|
+
export async function cowSignOrder({ typedData, wallet, address = null } = {}) {
|
|
167
|
+
if (!typedData || typeof typedData !== "object") return refusal("cow-bad-order", { field: "typedData" });
|
|
168
|
+
try {
|
|
169
|
+
if (wallet && typeof wallet.signTypedData === "function") {
|
|
170
|
+
const signature = await wallet.signTypedData(typedData);
|
|
171
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) return refusal("cow-sign-failed", { why: "bad-signature-shape" });
|
|
172
|
+
return { ok: true, signature };
|
|
173
|
+
}
|
|
174
|
+
const driver = wallet && typeof wallet.driver === "function" ? wallet.driver() : null;
|
|
175
|
+
if (driver && typeof driver.request === "function") {
|
|
176
|
+
const from = address || (typeof wallet.address === "function" ? await wallet.address() : null);
|
|
177
|
+
const signature = await driver.request({ method: "eth_signTypedData_v4", params: [from, JSON.stringify(typedData)] });
|
|
178
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) return refusal("cow-sign-failed", { why: "bad-signature-shape" });
|
|
179
|
+
return { ok: true, signature };
|
|
180
|
+
}
|
|
181
|
+
return refusal("cow-sign-failed", { why: "no-wallet" });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return refusal("cow-sign-failed", { why: String((error && error.code) || (error && error.message) || error) });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* SUBMIT THE SIGNED ORDER. POST /api/v1/orders -> 201 with the order UID. The transport is the repository's own
|
|
189
|
+
* createHttp (one place for the timeout and the status check). Every failure is one of the named refusals above.
|
|
190
|
+
*/
|
|
191
|
+
export async function submitCowOrder({ order, signature, owner, chainId, fetchImpl = null, timeoutMs = 20_000, signingScheme = "eip712", from = null } = {}) {
|
|
192
|
+
const base = cowOrderbook(chainId);
|
|
193
|
+
if (base === null) return refusal("cow-unknown-network", { chainId: Number(chainId) });
|
|
194
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) return refusal("cow-bad-order", { field: "signature" });
|
|
195
|
+
const http = createHttp({ apiBase: base, fetchImpl, timeoutMs });
|
|
196
|
+
const body = JSON.stringify(cowOrderSubmission({ order, signature, owner, signingScheme, from }));
|
|
197
|
+
let res;
|
|
198
|
+
try {
|
|
199
|
+
res = await http.tryJson("/api/v1/orders", { method: "POST", headers: { "content-type": "application/json" }, body });
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return refusal("cow-unreachable", { why: String((error && error.params && error.params.why) || (error && error.code) || error) });
|
|
202
|
+
}
|
|
203
|
+
if (!res || typeof res.ok !== "boolean") return refusal("cow-bad-response", { where: "submit" });
|
|
204
|
+
if (!res.ok) return refusal("cow-refused", { status: res.status, reason: res.body && res.body.errorType ? res.body.errorType : null });
|
|
205
|
+
if (typeof res.body !== "string" || !/^0x[0-9a-fA-F]{112}$/.test(res.body)) return refusal("cow-bad-response", { where: "submit", got: typeof res.body });
|
|
206
|
+
return { ok: true, uid: res.body };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* READ ONE ORDER. GET /api/v1/orders/{UID} -> the order, with `status`. A 404 is "cow-order-unknown"; a body whose
|
|
211
|
+
* status is not a known status is "cow-bad-response" - never silence, never null.
|
|
212
|
+
*/
|
|
213
|
+
export async function cowOrderStatus({ uid, chainId, fetchImpl = null, timeoutMs = 20_000 } = {}) {
|
|
214
|
+
const base = cowOrderbook(chainId);
|
|
215
|
+
if (base === null) return refusal("cow-unknown-network", { chainId: Number(chainId) });
|
|
216
|
+
if (typeof uid !== "string" || !/^0x[0-9a-fA-F]{112}$/.test(uid)) return refusal("cow-bad-order", { field: "uid" });
|
|
217
|
+
const http = createHttp({ apiBase: base, fetchImpl, timeoutMs });
|
|
218
|
+
let res;
|
|
219
|
+
try {
|
|
220
|
+
res = await http.tryJson("/api/v1/orders/" + uid);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return refusal("cow-unreachable", { why: String((error && error.params && error.params.why) || (error && error.code) || error) });
|
|
223
|
+
}
|
|
224
|
+
if (!res || typeof res.ok !== "boolean") return refusal("cow-bad-response", { where: "status" });
|
|
225
|
+
if (res.status === 404) return refusal("cow-order-unknown", { uid });
|
|
226
|
+
if (!res.ok) return refusal("cow-refused", { status: res.status });
|
|
227
|
+
const status = res.body && res.body.status;
|
|
228
|
+
if (typeof status !== "string" || !COW_STATUSES.includes(status)) return refusal("cow-bad-response", { where: "status", got: typeof status === "string" ? status : null });
|
|
229
|
+
return { ok: true, status, order: res.body };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* WHAT ENDS AN ASYNCHRONOUS PROVIDER'S EXECUTION - poll the order book until the order is settled or finally dead,
|
|
234
|
+
* with a timeout. Returns:
|
|
235
|
+
* { ok: true, done: true, settled: true, status: "fulfilled" } - the swap settled;
|
|
236
|
+
* { ok: true, done: true, settled: false, status: "cancelled" | "expired" } - the order is finally dead;
|
|
237
|
+
* { ok: false, code: "cow-not-settled" | "cow-unreachable" | "cow-bad-response" | ... } - a NAMED refusal.
|
|
238
|
+
* "Not settled in time" is its OWN code: "we did not wait long enough" is a state of the order, not a breakdown.
|
|
239
|
+
* The clock and the sleep are injectable so the wait is testable without real time.
|
|
240
|
+
*/
|
|
241
|
+
export async function cowSettled(request = {}, deps = {}) {
|
|
242
|
+
const { uid, chainId } = request;
|
|
243
|
+
if (typeof uid !== "string" || !/^0x[0-9a-fA-F]{112}$/.test(uid)) return refusal("cow-bad-order", { field: "uid" });
|
|
244
|
+
const now = typeof deps.now === "function" ? deps.now : () => Date.now();
|
|
245
|
+
const sleep = typeof deps.sleep === "function" ? deps.sleep : (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
246
|
+
const pollMs = Number.isFinite(deps.pollMs) && deps.pollMs > 0 ? deps.pollMs : 5_000;
|
|
247
|
+
const timeoutMs = Number.isFinite(deps.timeoutMs) && deps.timeoutMs > 0 ? deps.timeoutMs : 300_000;
|
|
248
|
+
const startedAt = now();
|
|
249
|
+
let lastStatus = null;
|
|
250
|
+
for (;;) {
|
|
251
|
+
const read = await cowOrderStatus({ uid, chainId, fetchImpl: deps.fetchImpl, timeoutMs: deps.requestTimeoutMs });
|
|
252
|
+
if (!read.ok) return read; // unreachable / bad-response / unknown: a named refusal, not silence
|
|
253
|
+
lastStatus = read.status;
|
|
254
|
+
if (COW_FINAL_STATUSES.includes(lastStatus)) {
|
|
255
|
+
return { ok: true, done: true, settled: lastStatus === COW_SETTLED_STATUS, status: lastStatus, uid };
|
|
256
|
+
}
|
|
257
|
+
if (now() - startedAt >= timeoutMs) return refusal("cow-not-settled", { uid, status: lastStatus, waitedMs: now() - startedAt });
|
|
258
|
+
await sleep(pollMs);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* THE SHARED PRE-SIGNATURE GUARDS, APPLIED TO THIS PROVIDER. The same guard functions the synchronous DEX leg uses
|
|
264
|
+
* (engine/evm/dex.js priceGateVerdict + dexLegVerdict for "the price is not the market" and "the amount is covered";
|
|
265
|
+
* engine/core/preSignGuards.js allowanceVerdict for "the token is allowed"), composed by the same summary guard
|
|
266
|
+
* (signingGateVerdict). A provider that skipped them would be a hole in the guards. Pure - no network, no wallet.
|
|
267
|
+
*/
|
|
268
|
+
export function cowGate({
|
|
269
|
+
order = null, requiredOutWei = null, tokenUsd = null, referenceNativeUsd = null, tolerated = false, gates = null,
|
|
270
|
+
sellDecimals = null, buyDecimals = null, buySymbol = null, allowanceWei = null, allowanceConsent = false,
|
|
271
|
+
tokenIsNative = false, symbol = null, decimals = null,
|
|
272
|
+
} = {}) {
|
|
273
|
+
if (!order) return { ok: false, blocked: true, reason: "cow-no-order", checks: [] };
|
|
274
|
+
const price = engine.dex.priceGateVerdict({
|
|
275
|
+
amountInWei: order.sellAmount, amountOutWei: order.buyAmount,
|
|
276
|
+
inDecimals: sellDecimals, outDecimals: buyDecimals,
|
|
277
|
+
tokenUsd: Number(tokenUsd) > 0 ? Number(tokenUsd) : null,
|
|
278
|
+
referenceUsd: Number(referenceNativeUsd) > 0 ? Number(referenceNativeUsd) : null,
|
|
279
|
+
stableSteps: [], tolerated: Boolean(tolerated), gates,
|
|
280
|
+
});
|
|
281
|
+
const leg = { ok: true, amountOutWei: String(order.buyAmount), price, wrapped: { symbol: buySymbol, decimals: buyDecimals } };
|
|
282
|
+
const coverage = engine.dex.dexLegVerdict(leg, requiredOutWei);
|
|
283
|
+
const coverageCheck = {
|
|
284
|
+
ok: !coverage.blocked, checked: coverage.kind !== "unchecked" && coverage.kind !== "unquoted",
|
|
285
|
+
blocks: coverage.blocked === true, kind: coverage.kind, reason: coverage.reason,
|
|
286
|
+
};
|
|
287
|
+
const allowance = engine.guards.allowanceVerdict({
|
|
288
|
+
tokenIsNative, allowanceWei, amountWei: order.sellAmount,
|
|
289
|
+
consent: allowanceConsent === true, symbol, decimals: Number.isFinite(Number(decimals)) ? Number(decimals) : 18,
|
|
290
|
+
});
|
|
291
|
+
const gate = engine.guards.signingGateVerdict([coverageCheck, allowance]);
|
|
292
|
+
return { ...gate, price, coverage, allowance };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// THE PROVIDER, AS THE REGISTRY EXPECTS IT: id, kind, shape and, for "async", what ends execution (settled).
|
|
296
|
+
export const cowProvider = Object.freeze({
|
|
297
|
+
id: "cowswap",
|
|
298
|
+
kind: "cow",
|
|
299
|
+
venue: "CoWSwap",
|
|
300
|
+
shape: ASYNC,
|
|
301
|
+
// WHAT IT SETTLES: native coin. A CoW order buys native directly (buyToken = COW_BUY_ETH_ADDRESS), so this
|
|
302
|
+
// provider can carry a native need ALL THE WAY - it is the native leg of a composed route.
|
|
303
|
+
settles: "native",
|
|
304
|
+
networks: COW_NETWORKS,
|
|
305
|
+
settlement: COW_SETTLEMENT,
|
|
306
|
+
buyEthAddress: COW_BUY_ETH_ADDRESS,
|
|
307
|
+
orderFields: COW_ORDER_FIELDS,
|
|
308
|
+
plan: cowPlan,
|
|
309
|
+
settled: cowSettled,
|
|
310
|
+
buildIntent: buildCowIntent,
|
|
311
|
+
sign: cowSignOrder,
|
|
312
|
+
submit: submitCowOrder,
|
|
313
|
+
status: cowOrderStatus,
|
|
314
|
+
gate: cowGate,
|
|
315
|
+
});
|
package/src/legs/evm.mjs
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
// land here in stage 2. Already here: what the network is called for the wallet, how to tell that the wallet is on
|
|
5
5
|
// the right network, how balances are read, and the ROUTE-PROVIDER REGISTRY (our declared routes today, an
|
|
6
6
|
// external aggregator when it arrives).
|
|
7
|
+
// THE SHARED SHAPE VOCABULARY lives in its own module (./shape.mjs) so the registry and the concrete providers can
|
|
8
|
+
// both depend on it without an import cycle. The CoWSwap provider is registered below like any other route provider.
|
|
9
|
+
import { SYNC, ASYNC, providerShapeVerdict } from "./shape.mjs";
|
|
10
|
+
import { cowProvider } from "./cow.mjs";
|
|
11
|
+
import { kyberProvider } from "./kyber.mjs";
|
|
12
|
+
import { uniswapxProvider } from "./uniswapx.mjs";
|
|
13
|
+
import { uniswapRelayProvider } from "./uniswaprelay.mjs";
|
|
14
|
+
|
|
7
15
|
export const vm = "evm";
|
|
8
16
|
|
|
9
17
|
/** What this network is called for the wallet: for EVM - a numeric chainId. */
|
|
@@ -78,8 +86,7 @@ export async function balances({ driver, address, tokens }) {
|
|
|
78
86
|
// REQUIREMENT FOR A PROVIDER: id, kind, shape ("sync" | "async"), plan(...) -> { ok, expectedOutWei | code,
|
|
79
87
|
// route?, calldata?, to? } and, for shape "async", settled(...). Execution (assembling and sending the
|
|
80
88
|
// transaction) is stage 3; for now only our provider is declared, and its plan honestly answers with a code.
|
|
81
|
-
export
|
|
82
|
-
export const ASYNC = "async";
|
|
89
|
+
export { SYNC, ASYNC };
|
|
83
90
|
|
|
84
91
|
const ROUTE_PROVIDERS = {
|
|
85
92
|
declared: {
|
|
@@ -92,6 +99,28 @@ const ROUTE_PROVIDERS = {
|
|
|
92
99
|
return { ok: false, code: "not-implemented", stage: 3 };
|
|
93
100
|
},
|
|
94
101
|
},
|
|
102
|
+
// COWSWAP - AN INTENT AUCTION: "async". The person signs an intent, and the settlement arrives LATER as a separate
|
|
103
|
+
// transaction sent by someone else. Its record therefore carries `settled(...)` - what ends its execution (the
|
|
104
|
+
// order book reporting the order settled or finally dead). The provider itself lives in ./cow.mjs.
|
|
105
|
+
cowswap: cowProvider,
|
|
106
|
+
// KYBERSWAP LIMIT ORDER - A SECOND INTENT AUCTION, ALSO "async". Same role as CoW (the person signs an
|
|
107
|
+
// order, someone else settles it later), a different book and contract. It is the LIQUIDITY leg: its order
|
|
108
|
+
// trades ERC20 against ERC20 and settles WETH, not native coin (see the scope note in ./kyber.mjs). The
|
|
109
|
+
// provider itself lives in ./kyber.mjs.
|
|
110
|
+
kyberswap: kyberProvider,
|
|
111
|
+
// UNISWAPX - A THIRD INTENT AUCTION, ALSO "async". Same role as CoW (the person signs an order, someone else
|
|
112
|
+
// settles it later) and the same native capability (an output may be the NATIVE sentinel), so it is a SECOND
|
|
113
|
+
// native leg as well as a single native route. Unlike CoW, publishing needs the Uniswap Trading API and an API
|
|
114
|
+
// key and the order is cosigned by Uniswap Labs (see the scope note in ./uniswapx.mjs). The provider itself
|
|
115
|
+
// lives in ./uniswapx.mjs.
|
|
116
|
+
uniswapx: uniswapxProvider,
|
|
117
|
+
// THE RELAYED UNISWAP ROUTE - A FOURTH INTENT AUCTION, ALSO "async", AND THE KEY-FREE ONE. Same role as CoW (the
|
|
118
|
+
// person signs an order, someone else settles it later) and the same native capability (the relayed swap unwraps
|
|
119
|
+
// to native), so it is a THIRD native leg as well as a single native route. Its discovery path is the point:
|
|
120
|
+
// Uniswap's own relayer repository documents NO submission endpoint and NO gossip service for a signed relay
|
|
121
|
+
// order, so this provider refuses that step BY NAME ("uniswap-relay-no-discovery") instead of inventing one (see
|
|
122
|
+
// the scope note in ./uniswaprelay.mjs). The provider itself lives in ./uniswaprelay.mjs.
|
|
123
|
+
uniswaprelay: uniswapRelayProvider,
|
|
95
124
|
};
|
|
96
125
|
|
|
97
126
|
/** Who can execute a route at all. The list is needed by the "no such provider" error. */
|
|
@@ -116,3 +145,58 @@ export const requireSyncProvider = (id) => {
|
|
|
116
145
|
}
|
|
117
146
|
return { ok: true, provider };
|
|
118
147
|
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* THE GATE OF A PATH THAT ASSUMES AN ASYNCHRONOUS INTENT. The mirror of requireSyncProvider: it accepts a provider
|
|
151
|
+
* whose swap settles in a LATER, separate action, and refuses by NAME - never silently - a provider that is
|
|
152
|
+
* unknown, that is in fact synchronous, or that declares itself asynchronous without naming what ends execution
|
|
153
|
+
* (`settled`). Refusals are VALUES: { ok: true, provider } or { ok: false, reason, ... }.
|
|
154
|
+
*/
|
|
155
|
+
export const requireAsyncProvider = (id) => {
|
|
156
|
+
const provider = routeProviderFor(id);
|
|
157
|
+
if (!provider) return { ok: false, reason: "provider-unknown", provider: typeof id === "string" ? id : null, known: routeProviders() };
|
|
158
|
+
if (provider.shape !== ASYNC) return { ok: false, reason: "provider-not-asynchronous", provider: provider.id, shape: provider.shape === undefined ? null : provider.shape };
|
|
159
|
+
const verdict = providerShapeVerdict(provider);
|
|
160
|
+
if (!verdict.ok) return { ok: false, reason: verdict.reason, provider: provider.id, shape: provider.shape === undefined ? null : provider.shape, missing: verdict.missing ?? null };
|
|
161
|
+
return { ok: true, provider };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A GENERAL GATE: may a path use this provider at all, whatever the shape, and is the provider complete (an
|
|
166
|
+
* asynchronous one names what ends its execution). The shape itself is NOT required to match a path's assumption
|
|
167
|
+
* here - that is what requireSyncProvider / requireAsyncProvider are for.
|
|
168
|
+
*/
|
|
169
|
+
export const requireProvider = (id) => {
|
|
170
|
+
const provider = routeProviderFor(id);
|
|
171
|
+
if (!provider) return { ok: false, reason: "provider-unknown", provider: typeof id === "string" ? id : null, known: routeProviders() };
|
|
172
|
+
const verdict = providerShapeVerdict(provider);
|
|
173
|
+
if (!verdict.ok) return { ok: false, reason: verdict.reason, provider: provider.id, shape: verdict.shape ?? null, missing: verdict.missing ?? null };
|
|
174
|
+
return { ok: true, provider };
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// --- WHAT A PROVIDER SETTLES: THE COMPOSITION CAPABILITY --------------------------------------------
|
|
178
|
+
//
|
|
179
|
+
// A native need (the escrow is funded with the chain's NATIVE coin) can be carried by a provider only if the
|
|
180
|
+
// provider SETTLES native. A provider that settles the WRAPPED native (KyberSwap Limit Order) can never be the last
|
|
181
|
+
// mile: it is usable only as the LIQUIDITY leg of a COMPOSITION, and the composition needs a native-capable provider
|
|
182
|
+
// on the SAME network to finish (WETH -> native). So the capability is DATA on each provider (`settles`) and the
|
|
183
|
+
// question is answered HERE by a VALUE, never assumed from the provider's name - the same rule the engine's
|
|
184
|
+
// composition follows (www/js/evm/asyncRoute.js, asyncRouteChoice).
|
|
185
|
+
export const SETTLES = Object.freeze({ NATIVE: "native", WRAPPED: "wrapped" });
|
|
186
|
+
|
|
187
|
+
/** WHAT A PROVIDER SETTLES ("native" | "wrapped"), or null when it does not say. */
|
|
188
|
+
export const providerSettles = (provider) => (provider && provider.settles ? provider.settles : null);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* CAN THIS PROVIDER CARRY A NATIVE NEED ALL THE WAY - a VALUE, by name.
|
|
192
|
+
* { ok: true, settles: "native", provider } - it settles native: it can be the native leg;
|
|
193
|
+
* { ok: false, reason: "settles-wrapped", settles, provider } - wrapped native only: a liquidity leg at most;
|
|
194
|
+
* { ok: false, reason: "settles-unstated", settles: null, ... } - it does not say: refused, never assumed native.
|
|
195
|
+
*/
|
|
196
|
+
export const carriesNativeVerdict = (provider) => {
|
|
197
|
+
const settles = providerSettles(provider);
|
|
198
|
+
const id = provider && provider.id ? provider.id : null;
|
|
199
|
+
if (settles === SETTLES.NATIVE) return { ok: true, settles, provider: id };
|
|
200
|
+
if (settles === SETTLES.WRAPPED) return { ok: false, reason: "settles-wrapped", settles, provider: id };
|
|
201
|
+
return { ok: false, reason: "settles-unstated", settles: null, provider: id };
|
|
202
|
+
};
|