@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/engine/ui/dom.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/ui/dom.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
5
|
+
// Tiny DOM helpers. No frameworks: the demo must be readable by eye.
|
|
7
6
|
|
|
8
7
|
export function h(tag, attrs = {}, children = []) {
|
|
9
8
|
const el = document.createElement(tag);
|
|
@@ -12,8 +11,8 @@ export function h(tag, attrs = {}, children = []) {
|
|
|
12
11
|
if (k === "class") el.className = v;
|
|
13
12
|
else if (k === "html") el.innerHTML = v;
|
|
14
13
|
else if (k === "text") el.textContent = v;
|
|
15
|
-
// style
|
|
16
|
-
// (style-src-attr),
|
|
14
|
+
// style is written only through CSSOM: the style="..." attribute is banned by the CSP policy
|
|
15
|
+
// (style-src-attr), while el.style.* and cssText are allowed. So this trap does not return.
|
|
17
16
|
else if (k === "style") {
|
|
18
17
|
if (typeof v === "string") el.style.cssText = v;
|
|
19
18
|
else Object.assign(el.style, v);
|
package/engine/ui/walletModal.js
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/ui/walletModal.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// Почему не модалка Reown (@reown/appkit): она рисует свои стили inline - и атрибутом style="",
|
|
13
|
-
// и целыми <style>-блоками. CSP демки запрещает и то, и другое (ловушка №1 в
|
|
14
|
-
// .hermes/docs/05-frontend-stack.md), ослаблять её ради модалки мы не стали. Поэтому модалку
|
|
15
|
-
// рисуем сами через dom.h() (стили идут через CSSOM), а QR - генератором qrcode из бандла
|
|
16
|
-
// в виде data:-URL (img-src 'self' data: в CSP это разрешает).
|
|
17
|
-
//
|
|
18
|
-
// Никаких секретов здесь нет: в QR лежит открытая ссылка сессии WalletConnect (wc:...),
|
|
19
|
-
// её и показывать пользователю не страшно - такое же показывает любая dapp.
|
|
5
|
+
// A wallet-connect modal: browser-wallet choice and a QR for WalletConnect.
|
|
6
|
+
// The browser-wallet list comes from outside (setChoices); detection is evm/injected.js by EIP-6963. Here is
|
|
7
|
+
// only rendering: the CSP bans inline styles, so everything goes through dom.h(), and wallet icons are data: URLs.
|
|
8
|
+
// NOT the Reown (@reown/appkit) modal: it draws its own inline styles and <style> blocks, both banned by the demo
|
|
9
|
+
// CSP, and we did not weaken it for a modal. So we draw the modal ourselves and the QR with a qrcode generator as a data: URL.
|
|
10
|
+
// No secrets here: the QR holds the public WalletConnect session URI (wc:...), safe to show like any dapp.
|
|
20
11
|
|
|
21
12
|
import { h, copy } from "./dom.js";
|
|
22
13
|
|
|
@@ -30,7 +21,7 @@ export function openWalletModal({ title = "Connect a wallet", subtitle = "", hin
|
|
|
30
21
|
(refs.closeBtn = h("button", { class: "ar-modal-close", type: "button", title: "Close", text: "\u00d7" })),
|
|
31
22
|
]),
|
|
32
23
|
(refs.subtitle = h("div", { class: "ar-modal-sub", text: subtitle })),
|
|
33
|
-
//
|
|
24
|
+
// The browser-wallet list: empty until detection delivers it (then invisible).
|
|
34
25
|
(refs.choices = h("div", { class: "ar-modal-choices" })),
|
|
35
26
|
(refs.qrBox = h("div", { class: "ar-modal-qr", hidden: true }, [(refs.qr = h("img", { alt: "WalletConnect QR code" }))])),
|
|
36
27
|
(refs.uri = h("code", { class: "ar-modal-uri" })),
|
|
@@ -48,12 +39,10 @@ export function openWalletModal({ title = "Connect a wallet", subtitle = "", hin
|
|
|
48
39
|
const closed = { value: false };
|
|
49
40
|
let onCancel = null;
|
|
50
41
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// отмены и удаляла только что созданную сессию (в логе SDK это видно как wc_sessionDelete
|
|
56
|
-
// через 85 мс после wc_sessionSettle).
|
|
42
|
+
// TWO DIFFERENT EXITS FROM THE MODAL, and the difference is fundamental: close() closes it programmatically
|
|
43
|
+
// (e.g. the session settled), while cancel() is the user refusing - and THAT is when we must kill the session
|
|
44
|
+
// we asked for. They must not be confused: before, a successful connect closed the modal, which fired the
|
|
45
|
+
// cancel handler and deleted the just-created session.
|
|
57
46
|
function close() {
|
|
58
47
|
if (closed.value) return;
|
|
59
48
|
closed.value = true;
|
|
@@ -75,9 +64,9 @@ export function openWalletModal({ title = "Connect a wallet", subtitle = "", hin
|
|
|
75
64
|
if (e.target === overlay) cancel();
|
|
76
65
|
});
|
|
77
66
|
|
|
78
|
-
// setChoices([{ key, name, icon, hint, onClick }]) -
|
|
79
|
-
//
|
|
80
|
-
//
|
|
67
|
+
// setChoices([{ key, name, icon, hint, onClick }]) - the browser-wallet list plus WalletConnect. Icons are
|
|
68
|
+
// data: URLs only (the wallet itself provides them): external addresses are not allowed by the demo CSP, and
|
|
69
|
+
// rightly so - a network image must not influence which wallet is chosen.
|
|
81
70
|
function setChoices(list) {
|
|
82
71
|
refs.choices.innerHTML = "";
|
|
83
72
|
refs.choices.hidden = !list || !list.length;
|
|
@@ -91,8 +80,8 @@ export function openWalletModal({ title = "Connect a wallet", subtitle = "", hin
|
|
|
91
80
|
item.hint ? h("span", { class: "ar-modal-choice-hint", text: item.hint }) : null,
|
|
92
81
|
]),
|
|
93
82
|
]);
|
|
94
|
-
//
|
|
95
|
-
//
|
|
83
|
+
// The rest of the modal is closed by whoever handled the choice: a browser wallet shows its own window,
|
|
84
|
+
// while WalletConnect still needs the QR shown.
|
|
96
85
|
btn.addEventListener("click", () => {
|
|
97
86
|
if (closed.value) return;
|
|
98
87
|
item.onClick();
|
|
@@ -114,14 +103,14 @@ export function openWalletModal({ title = "Connect a wallet", subtitle = "", hin
|
|
|
114
103
|
if (closed.value) return;
|
|
115
104
|
if (qrDataUrl) {
|
|
116
105
|
refs.qr.src = qrDataUrl;
|
|
117
|
-
//
|
|
106
|
+
// The frame is shown only when a QR actually exists: before WalletConnect is chosen this is empty.
|
|
118
107
|
refs.qrBox.hidden = false;
|
|
119
108
|
}
|
|
120
109
|
if (uri !== undefined) refs.uri.textContent = uri || "";
|
|
121
110
|
if (subtitle !== undefined) refs.subtitle.textContent = subtitle || "";
|
|
122
111
|
if (hintText !== undefined) refs.hint.textContent = hintText || "";
|
|
123
|
-
// error
|
|
124
|
-
//
|
|
112
|
+
// error is more important than status: otherwise a generic "try again" overwrites the error text, and
|
|
113
|
+
// the refusal reason is visible only in the toast.
|
|
125
114
|
const line = error || status;
|
|
126
115
|
if (line !== undefined) {
|
|
127
116
|
refs.status.className = "ar-modal-status" + (error ? " ar-modal-status-error" : "");
|
package/index.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface NinseiOptions {
|
|
|
47
47
|
* transaction, so the swap and the escrow funding are one transaction (Uniswap, SushiSwap); "async" - the
|
|
48
48
|
* settlement arrives later as a separate action, and then the record must also say what ends execution
|
|
49
49
|
* (CoWSwap). A path that assumes "one transaction" refuses an asynchronous provider BY NAME, not silently.
|
|
50
|
+
* The registered identifiers are `declared` (our own router routes, "sync") and `cowswap` (the CoWSwap intent
|
|
51
|
+
* auction, "async") - see `sdk.config.routing.providers`.
|
|
50
52
|
*/
|
|
51
53
|
routing?: { prefer?: readonly string[] };
|
|
52
54
|
/**
|
|
@@ -348,6 +350,12 @@ export interface WalletAdapter {
|
|
|
348
350
|
* by the caller, not the adapter - otherwise two calls would wait differently.
|
|
349
351
|
*/
|
|
350
352
|
receipt(txHash: string): Promise<unknown>;
|
|
353
|
+
/**
|
|
354
|
+
* SIGN TYPED DATA (EIP-712). Not a transaction: no gas, no money moves, so a person without their own coin can
|
|
355
|
+
* give it. What is signed is decided by the CALLER (ready typed data arrives here). Optional - a wallet that
|
|
356
|
+
* cannot sign typed data leaves it out, and a caller sees `not-implemented`, not silence.
|
|
357
|
+
*/
|
|
358
|
+
signTypedData?(typedData: unknown): Promise<string>;
|
|
351
359
|
/** Switching the network, if this VM can do it from the SDK. No method - `not-implemented`, not silence. */
|
|
352
360
|
switchChain?(chainId: number | string): Promise<void>;
|
|
353
361
|
driver(): unknown;
|
package/package.json
CHANGED
|
@@ -119,6 +119,22 @@ export function evmWallet(provider, { id = "evm" } = {}) {
|
|
|
119
119
|
if (typeof txHash !== "string" || !txHash) throw new SdkError("bad-input", { field: "receipt.txHash" });
|
|
120
120
|
return await provider.request({ method: "eth_getTransactionReceipt", params: [txHash] });
|
|
121
121
|
},
|
|
122
|
+
// SIGNING TYPED DATA (EIP-712) - the same call the page makes (www/js/evm/session.js, signTypedData). This is not
|
|
123
|
+
// a transaction: no gas, no money moves, so a person without their own ETH can give it. What is signed is DECIDED
|
|
124
|
+
// BY THE CALLER (ready typed data arrives here - e.g. a CoWSwap order from src/legs/cow.mjs); the shape is not
|
|
125
|
+
// assembled here, else there would be a second record of the protocol.
|
|
126
|
+
async signTypedData(typedData) {
|
|
127
|
+
if (!typedData || typeof typedData !== "object") throw new SdkError("bad-input", { field: "signTypedData" });
|
|
128
|
+
const accounts = await provider.request({ method: "eth_accounts" });
|
|
129
|
+
const from = Array.isArray(accounts) ? accounts[0] : null;
|
|
130
|
+
if (!from) throw new SdkError("wallet-not-connected", { step: "signTypedData" });
|
|
131
|
+
// The argument order [address, JSON] is as EIP-712 requires and as MetaMask and WalletConnect accept.
|
|
132
|
+
const signature = await provider.request({ method: "eth_signTypedData_v4", params: [from, JSON.stringify(typedData)] });
|
|
133
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) {
|
|
134
|
+
throw new SdkError("wallet-rejected", { step: "signTypedData", why: "bad-signature-shape" });
|
|
135
|
+
}
|
|
136
|
+
return signature;
|
|
137
|
+
},
|
|
122
138
|
driver() {
|
|
123
139
|
return provider;
|
|
124
140
|
},
|
|
@@ -29,7 +29,7 @@ import { config as engineConfig, xmrAddress } from "../engine.mjs";
|
|
|
29
29
|
|
|
30
30
|
const XMR_VM = "monero";
|
|
31
31
|
// The wallet session label - the same as the withdrawal page's. This is not a secret and not an address, but a session name.
|
|
32
|
-
const DEFAULT_PASSWORD = "
|
|
32
|
+
const DEFAULT_PASSWORD = "ninsei-wallet-session";
|
|
33
33
|
|
|
34
34
|
/** Monero network names declared by the core's registry. The adapter keeps no table of its own. */
|
|
35
35
|
const knownNetworks = () => {
|
package/src/adapters/monero.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// leave, BEFORE sending rather than after.
|
|
10
10
|
import { SdkError } from "../errors.mjs";
|
|
11
11
|
|
|
12
|
-
export function moneroWallet({ createWallet, networkType, serverUri, password = "
|
|
12
|
+
export function moneroWallet({ createWallet, networkType, serverUri, password = "ninsei-sweep-session", fields = {} }) {
|
|
13
13
|
const build = typeof createWallet === "function" ? createWallet : null;
|
|
14
14
|
// Refusals - with CODES, as everywhere in the core: the phrase for a person is said by the interface, not the adapter.
|
|
15
15
|
const need = () => { if (!wallet) throw new SdkError("bad-input", { field: "wallet", state: "closed" }); };
|
|
@@ -32,11 +32,11 @@ const keccak256 = (bytes) => new Uint8Array(keccakPkg(Buffer.from(bytes)));
|
|
|
32
32
|
|
|
33
33
|
// THE ENVIRONMENT VARIABLE NAMES - HERE, IN ONE PLACE: the run and the adapter must look at the same name,
|
|
34
34
|
// otherwise the key "is there" but the adapter does not see it.
|
|
35
|
-
export const KEY_ENV = "
|
|
36
|
-
export const KEY_FILE_ENV = "
|
|
37
|
-
export const RPC_ENV = "
|
|
38
|
-
export const CHAIN_ENV = "
|
|
39
|
-
export const CLAIMER_ENV = "
|
|
35
|
+
export const KEY_ENV = "NINSEI_TEST_EVM_KEY";
|
|
36
|
+
export const KEY_FILE_ENV = "NINSEI_TEST_EVM_KEY_FILE";
|
|
37
|
+
export const RPC_ENV = "NINSEI_TEST_EVM_RPC";
|
|
38
|
+
export const CHAIN_ENV = "NINSEI_TEST_EVM_CHAIN";
|
|
39
|
+
export const CLAIMER_ENV = "NINSEI_TEST_EVM_CLAIMER";
|
|
40
40
|
|
|
41
41
|
// AN EXPLICIT WHITELIST OF TEST NETWORKS - HERE, NOT IN THE ENGINE, and WHY. The engine's "production network" rule
|
|
42
42
|
// (sdk/src/swap-flow.mjs: escrow.mode === "live" && !testnet) today does not trigger ON ANY recorded network: for
|
package/src/config.mjs
CHANGED
|
@@ -99,7 +99,7 @@ export const DEFAULT_XMR_NODES = Object.freeze({
|
|
|
99
99
|
{ url: "https://mainnet.xmr.kernal.eu:18089", operator: "kernal.eu" },
|
|
100
100
|
],
|
|
101
101
|
stagenet: [
|
|
102
|
-
{ url: "https://xmr.arrakisswap.trade", operator: "
|
|
102
|
+
{ url: "https://xmr.arrakisswap.trade", operator: "ninseiswap" },
|
|
103
103
|
{ url: "https://stagenet.xmr.kernal.eu:38089", operator: "kernal.eu" },
|
|
104
104
|
],
|
|
105
105
|
});
|
package/src/engine.mjs
CHANGED
|
@@ -31,6 +31,10 @@ export * as order from "../engine/atomic/order.js";
|
|
|
31
31
|
export * as halfenc from "../engine/atomic/halfenc.js";
|
|
32
32
|
// The engine's EVM layer: talking to the chain (order terms, sending, reading) - through this seam.
|
|
33
33
|
export * as evm from "../engine/evm/index.js";
|
|
34
|
+
// THE DEX LEG'S OWN GUARDS - THROUGH THE SAME SEAM. priceGateVerdict ("the price is not the market") and
|
|
35
|
+
// dexLegVerdict (amount coverage + price) live in www/js/evm/dex.js and are re-exported here so a route provider
|
|
36
|
+
// that is NOT the declared Uniswap path (CoWSwap, an aggregator) applies THE SAME guards rather than lookalikes.
|
|
37
|
+
export * as dex from "../engine/evm/dex.js";
|
|
34
38
|
// THE DEAL ENGINE NO LONGER LIVES IN www/js (#32, wave 3): the order flow moved into the package
|
|
35
39
|
// (sdk/src/swap-flow.mjs) and is declared in sdk/index.d.ts. The page reaches it through the bridge, so the
|
|
36
40
|
// mirror no longer carries core/swap-flow.js - what stays in the seam is the ORDER CLIENT (the half worker),
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// EIP-712 TYPED DATA FOR A COW PROTOCOL ORDER - THE SPEC, IN THE SDK'S OWN STYLE.
|
|
2
|
+
//
|
|
3
|
+
// WHY A SEPARATE SPEC AND NOT A NEW LIBRARY. This repository builds EIP-712 typed data itself
|
|
4
|
+
// (src/quoteEip712Spec.mjs is the same pattern for the order quote): the encoding words, the domain separator, the
|
|
5
|
+
// struct hash and the digest are assembled here from the repository's own crypto primitives (src/primitives.mjs),
|
|
6
|
+
// and the SIGNING reuses the existing signer (signQuoteDigest from src/quoteEip712Spec.mjs, which signs ANY 32-byte
|
|
7
|
+
// digest). No dependency is added.
|
|
8
|
+
//
|
|
9
|
+
// EVERY EXTERNAL FACT IS PINNED TO ITS SOURCE, and where a value can be recomputed it is (the check does it):
|
|
10
|
+
// * ORDER type, field types and order - cowprotocol/contracts, lib GPv2Order.sol
|
|
11
|
+
// (github.com/cowprotocol/contracts/blob/main/src/contracts/libraries/GPv2Order.sol): the TYPE_HASH is the
|
|
12
|
+
// keccak256 of the string built from GPv2Order.Data, and the contract HARD-CODES that hash next to the struct;
|
|
13
|
+
// * the TYPE_HASH constant, the kind/balance markers - the same file (TYPE_HASH, KIND_SELL, KIND_BUY,
|
|
14
|
+
// BALANCE_ERC20 are the contract's own constants; keccak256("sell"), keccak256("buy"), keccak256("erc20"));
|
|
15
|
+
// * the DOMAIN name "Gnosis Protocol", version "v2", and the domain separator shape built as
|
|
16
|
+
// keccak256(abi.encode(DOMAIN_TYPE_HASH, DOMAIN_NAME, DOMAIN_VERSION, chainId, address(this)))
|
|
17
|
+
// - cowprotocol/contracts, mixin GPv2Signing.sol
|
|
18
|
+
// (github.com/cowprotocol/contracts/blob/main/src/contracts/mixins/GPv2Signing.sol);
|
|
19
|
+
// * the settlement contract address 0x9008D19f58AAbD9eD0D60971565AA8510560ab41 (deterministic, identical on every
|
|
20
|
+
// supported network) - docs.cow.fi, "Core" contracts (docs.cow.fi/cow-protocol/reference/contracts/core);
|
|
21
|
+
// * the order UID = 56 bytes = digest(32) ++ owner(20) ++ validTo(4), and UID_LENGTH 56 - GPv2Order.sol.
|
|
22
|
+
//
|
|
23
|
+
// THE one fact that is NOT taken from memory is the RESULT: the check recomputes the digest of a REAL settled
|
|
24
|
+
// order and demands it equal the digest half of that order's on-chain UID (fetched from the CoW order book).
|
|
25
|
+
//
|
|
26
|
+
// The domain type string is assembled from parts ON PURPOSE, exactly as in quoteEip712Spec.mjs: a single literal of
|
|
27
|
+
// it is a phrase of several space-separated words, and the SDK reads such literals as user-facing text. Do not
|
|
28
|
+
// "simplify" it. The domain NAME is assembled the same way for the same reason.
|
|
29
|
+
import { keccak256, secp256k1 } from "../primitives.mjs";
|
|
30
|
+
import { signQuoteDigest } from "../quoteEip712Spec.mjs";
|
|
31
|
+
|
|
32
|
+
// The domain name and version, from GPv2Signing.sol (DOMAIN_NAME = keccak256("Gnosis Protocol"),
|
|
33
|
+
// DOMAIN_VERSION = keccak256("v2")). Built from parts so no literal here is a multi-word phrase (see the header).
|
|
34
|
+
export const COW_DOMAIN_NAME = ["Gnosis", "Protocol"].join(" ");
|
|
35
|
+
export const COW_DOMAIN_VERSION = "v2";
|
|
36
|
+
|
|
37
|
+
// The settlement contract that verifies the signature, identical on every network CoW supports (docs.cow.fi, Core).
|
|
38
|
+
export const COW_SETTLEMENT = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41";
|
|
39
|
+
|
|
40
|
+
// The marker for "an order is buying native coin": it has meaning ONLY in buyToken (using it as sellToken makes the
|
|
41
|
+
// settlement revert). Source: cowprotocol contracts TS docs (BUY_ETH_ADDRESS) and docs.cow.fi native-token swaps.
|
|
42
|
+
export const COW_BUY_ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
43
|
+
|
|
44
|
+
// The type strings, assembled from their fields so the order is the single source of truth here (as in GPv2Order).
|
|
45
|
+
const DOMAIN_TYPE_PARTS = ["EIP712Domain(", "string name,", "string version,", "uint256 chainId,", "address verifyingContract)"];
|
|
46
|
+
export const COW_DOMAIN_TYPE = DOMAIN_TYPE_PARTS.join("");
|
|
47
|
+
|
|
48
|
+
// The message, field by field and IN THIS ORDER - the order is the protocol (GPv2Order.Data). The types match the
|
|
49
|
+
// contract's type-hash expression: kind, sellTokenBalance and buyTokenBalance are `string` in the TYPE (their value
|
|
50
|
+
// is the keccak256 of the marker string), while in the struct they are the pre-hashed bytes32 markers.
|
|
51
|
+
export const COW_ORDER_FIELDS = [
|
|
52
|
+
["sellToken", "address"],
|
|
53
|
+
["buyToken", "address"],
|
|
54
|
+
["receiver", "address"],
|
|
55
|
+
["sellAmount", "uint256"],
|
|
56
|
+
["buyAmount", "uint256"],
|
|
57
|
+
["validTo", "uint32"],
|
|
58
|
+
["appData", "bytes32"],
|
|
59
|
+
["feeAmount", "uint256"],
|
|
60
|
+
["kind", "string"],
|
|
61
|
+
["partiallyFillable", "bool"],
|
|
62
|
+
["sellTokenBalance", "string"],
|
|
63
|
+
["buyTokenBalance", "string"],
|
|
64
|
+
];
|
|
65
|
+
export const COW_ORDER_TYPE = "Order(" + COW_ORDER_FIELDS.map((f) => f[1] + " " + f[0]).join(",") + ")";
|
|
66
|
+
|
|
67
|
+
// The contract's OWN type-hash constant (GPv2Order.TYPE_HASH). It is NOT trusted: the check recomputes
|
|
68
|
+
// keccak256(COW_ORDER_TYPE) and demands it equal this - so a typo in the field list above reddens rather than
|
|
69
|
+
// silently signing orders the settlement would reject.
|
|
70
|
+
export const COW_ORDER_TYPE_HASH = "0xd5a25ba2e97094ad7d83dc28a6572da797d6b3e7fc6663bd93efb789fc17e489";
|
|
71
|
+
|
|
72
|
+
// The order-kind and token-balance markers (GPv2Order.KIND_SELL / KIND_BUY / BALANCE_ERC20), derived here from the
|
|
73
|
+
// repository's own keccak256 rather than copied: the check compares them against the contract's constants.
|
|
74
|
+
export const COW_KINDS = ["sell", "buy"];
|
|
75
|
+
export const COW_BALANCES = ["erc20", "external", "internal"];
|
|
76
|
+
export const cowKindMarker = (kind) => bytesToHex(keccak256(String(kind)));
|
|
77
|
+
export const cowBalanceMarker = (balance) => bytesToHex(keccak256(String(balance)));
|
|
78
|
+
|
|
79
|
+
// Error tokens are SINGLE WORDS (the SDK forbids multi-word literals in sdk/src): the spec names a failure by token
|
|
80
|
+
// and the caller maps it to its own code.
|
|
81
|
+
const boom = (token) => { throw new Error(token); };
|
|
82
|
+
|
|
83
|
+
const utf8 = (s) => new TextEncoder().encode(String(s));
|
|
84
|
+
const concat = (...arrays) => {
|
|
85
|
+
const total = arrays.reduce((n, a) => n + a.length, 0);
|
|
86
|
+
const out = new Uint8Array(total);
|
|
87
|
+
let at = 0;
|
|
88
|
+
for (const a of arrays) { out.set(a, at); at += a.length; }
|
|
89
|
+
return out;
|
|
90
|
+
};
|
|
91
|
+
const hexToBytes = (hex) => {
|
|
92
|
+
const h = String(hex).replace(/^0x/, "");
|
|
93
|
+
if (h.length % 2) boom("cow-odd-hex");
|
|
94
|
+
const out = new Uint8Array(h.length / 2);
|
|
95
|
+
for (let i = 0; i < out.length; i++) {
|
|
96
|
+
const byte = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
97
|
+
if (Number.isNaN(byte)) boom("cow-bad-hex");
|
|
98
|
+
out[i] = byte;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
};
|
|
102
|
+
const bytesToHex = (bytes) => {
|
|
103
|
+
let s = "0x";
|
|
104
|
+
for (const b of bytes) s += b.toString(16).padStart(2, "0");
|
|
105
|
+
return s;
|
|
106
|
+
};
|
|
107
|
+
const asBigInt = (v) => {
|
|
108
|
+
if (typeof v === "bigint") return v;
|
|
109
|
+
const s = String(v === undefined || v === null ? "" : v).trim();
|
|
110
|
+
if (/^0x[0-9a-fA-F]+$/.test(s)) return BigInt(s);
|
|
111
|
+
if (/^[0-9]+$/.test(s)) return BigInt(s);
|
|
112
|
+
boom("cow-bad-number");
|
|
113
|
+
};
|
|
114
|
+
const wordUint = (v, bits) => {
|
|
115
|
+
const n = asBigInt(v);
|
|
116
|
+
if (n < 0n) boom("cow-negative");
|
|
117
|
+
if (n >= (1n << BigInt(bits))) boom("cow-overflow");
|
|
118
|
+
const out = new Uint8Array(32);
|
|
119
|
+
let x = n;
|
|
120
|
+
for (let i = 31; i >= 0 && x > 0n; i--) { out[i] = Number(x & 0xffn); x >>= 8n; }
|
|
121
|
+
return out;
|
|
122
|
+
};
|
|
123
|
+
const wordAddress = (v) => {
|
|
124
|
+
const s = String(v === undefined || v === null ? "" : v).trim();
|
|
125
|
+
if (!/^(0x)?[0-9a-fA-F]{40}$/.test(s)) boom("cow-bad-address");
|
|
126
|
+
const out = new Uint8Array(32);
|
|
127
|
+
out.set(hexToBytes(s.replace(/^0x/, "")), 12);
|
|
128
|
+
return out;
|
|
129
|
+
};
|
|
130
|
+
const wordBytes32 = (v) => {
|
|
131
|
+
const s = String(v === undefined || v === null ? "" : v).trim();
|
|
132
|
+
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(s)) boom("cow-bad-bytes32");
|
|
133
|
+
return hexToBytes(s.replace(/^0x/, ""));
|
|
134
|
+
};
|
|
135
|
+
const wordBool = (v) => {
|
|
136
|
+
const out = new Uint8Array(32);
|
|
137
|
+
if (v === true || v === "true" || v === 1 || v === "1") out[31] = 1;
|
|
138
|
+
else if (v === false || v === "false" || v === 0 || v === "0" || v === undefined || v === null) out[31] = 0;
|
|
139
|
+
else boom("cow-bad-bool");
|
|
140
|
+
return out;
|
|
141
|
+
};
|
|
142
|
+
// A `string` field in EIP-712 is encoded as the keccak256 of its UTF-8 bytes - this is why the contract's
|
|
143
|
+
// KIND_SELL equals keccak256("sell").
|
|
144
|
+
const wordString = (v) => keccak256(utf8(v));
|
|
145
|
+
|
|
146
|
+
const encodeField = (type, value) => {
|
|
147
|
+
if (type === "address") return wordAddress(value);
|
|
148
|
+
if (type === "bytes32") return wordBytes32(value);
|
|
149
|
+
if (type === "uint32") return wordUint(value, 32);
|
|
150
|
+
if (type === "uint256") return wordUint(value, 256);
|
|
151
|
+
if (type === "bool") return wordBool(value);
|
|
152
|
+
if (type === "string") return wordString(value);
|
|
153
|
+
boom("cow-bad-type");
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// The fields an order cannot do without. An absent field is not "probably zero": the caller refuses, not signs.
|
|
157
|
+
export const cowOrderMissingFields = (order) => {
|
|
158
|
+
const missing = [];
|
|
159
|
+
for (const [name] of COW_ORDER_FIELDS) {
|
|
160
|
+
const v = order ? order[name] : undefined;
|
|
161
|
+
if (v === undefined || v === null || v === "") missing.push(name);
|
|
162
|
+
}
|
|
163
|
+
return missing;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// The EIP-712 domain. chainId and verifyingContract come LIVE from the order's context, not cached: the digest must
|
|
167
|
+
// change when either does, else an order could move between networks or settlement contracts.
|
|
168
|
+
export const cowDomainSeparator = ({ chainId, settlement = COW_SETTLEMENT }) => keccak256(concat(
|
|
169
|
+
keccak256(utf8(COW_DOMAIN_TYPE)),
|
|
170
|
+
keccak256(utf8(COW_DOMAIN_NAME)),
|
|
171
|
+
keccak256(utf8(COW_DOMAIN_VERSION)),
|
|
172
|
+
wordUint(chainId, 256),
|
|
173
|
+
wordAddress(settlement),
|
|
174
|
+
));
|
|
175
|
+
|
|
176
|
+
export const cowStructHash = (order) => {
|
|
177
|
+
const parts = [keccak256(utf8(COW_ORDER_TYPE))];
|
|
178
|
+
for (const [name, type] of COW_ORDER_FIELDS) parts.push(encodeField(type, order ? order[name] : undefined));
|
|
179
|
+
return keccak256(concat(...parts));
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// The digest the wallet key signs. Anything that changes here changes the recovered address.
|
|
183
|
+
export const cowOrderDigest = ({ order, chainId, settlement = COW_SETTLEMENT }) =>
|
|
184
|
+
keccak256(concat(Uint8Array.from([0x19, 0x01]), cowDomainSeparator({ chainId, settlement }), cowStructHash(order)));
|
|
185
|
+
|
|
186
|
+
export const cowOrderDigestHex = (ctx) => bytesToHex(cowOrderDigest(ctx));
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The order UID: 56 bytes = digest(32) ++ owner(20) ++ validTo(uint32). The order book returns this from
|
|
190
|
+
* POST /api/v1/orders and addresses the order by it. GPv2Order.packOrderUidParams.
|
|
191
|
+
*/
|
|
192
|
+
export const cowOrderUid = ({ digest, owner, validTo }) => {
|
|
193
|
+
const d = hexToBytes(digest); // must be 32 bytes
|
|
194
|
+
if (d.length !== 32) boom("cow-bad-digest");
|
|
195
|
+
const o = hexToBytes(owner); // must be 20 bytes
|
|
196
|
+
if (o.length !== 20) boom("cow-bad-owner");
|
|
197
|
+
const v = wordUint(validTo, 32).slice(28); // the last 4 bytes of the uint32 word, big-endian
|
|
198
|
+
return bytesToHex(concat(d, o, v));
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The typed data a wallet signs (eth_signTypedData_v4). The message carries the kind and balances as STRINGS
|
|
203
|
+
* ("sell", "erc20"), matching the type - the wallet shows them readably, and the digest the settlement computes
|
|
204
|
+
* replaces them with their keccak256 (see wordString).
|
|
205
|
+
*/
|
|
206
|
+
export const cowOrderTypedData = ({ order, chainId, settlement = COW_SETTLEMENT }) => ({
|
|
207
|
+
domain: { name: COW_DOMAIN_NAME, version: COW_DOMAIN_VERSION, chainId: Number(chainId), verifyingContract: settlement },
|
|
208
|
+
types: {
|
|
209
|
+
EIP712Domain: [
|
|
210
|
+
{ name: "name", type: "string" },
|
|
211
|
+
{ name: "version", type: "string" },
|
|
212
|
+
{ name: "chainId", type: "uint256" },
|
|
213
|
+
{ name: "verifyingContract", type: "address" },
|
|
214
|
+
],
|
|
215
|
+
Order: COW_ORDER_FIELDS.map(([name, type]) => ({ name, type })),
|
|
216
|
+
},
|
|
217
|
+
primaryType: "Order",
|
|
218
|
+
message: {
|
|
219
|
+
sellToken: order.sellToken,
|
|
220
|
+
buyToken: order.buyToken,
|
|
221
|
+
receiver: order.receiver,
|
|
222
|
+
sellAmount: String(order.sellAmount),
|
|
223
|
+
buyAmount: String(order.buyAmount),
|
|
224
|
+
validTo: Number(order.validTo),
|
|
225
|
+
appData: order.appData,
|
|
226
|
+
feeAmount: String(order.feeAmount),
|
|
227
|
+
kind: String(order.kind),
|
|
228
|
+
partiallyFillable: Boolean(order.partiallyFillable),
|
|
229
|
+
sellTokenBalance: String(order.sellTokenBalance),
|
|
230
|
+
buyTokenBalance: String(order.buyTokenBalance),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// SIGN THE DIGEST THROUGH THE REPOSITORY'S EXISTING SIGNER: signQuoteDigest signs any 32-byte digest and returns
|
|
235
|
+
// r || s || v with v in {27, 28}. No second signer is written here.
|
|
236
|
+
export const signCowOrderDigest = ({ digest, privateKey }) =>
|
|
237
|
+
signQuoteDigest({ crypto: { secp256k1 }, digest, privateKey });
|
|
238
|
+
|
|
239
|
+
// Recover the signer of a digest from a signature (the same rule the quote check uses): lowercase 0x address, or
|
|
240
|
+
// null when the signature is not usable. The caller turns null into a NAMED refusal, never into consent.
|
|
241
|
+
export const recoverCowOrderSigner = ({ digest, signature }) => {
|
|
242
|
+
try {
|
|
243
|
+
const sig = hexToBytes(String(signature).replace(/^0x/, ""));
|
|
244
|
+
if (sig.length !== 65) return null;
|
|
245
|
+
let v = sig[64];
|
|
246
|
+
if (v >= 27) v -= 27;
|
|
247
|
+
if (v !== 0 && v !== 1) return null;
|
|
248
|
+
const pub = secp256k1.Signature.fromBytes(concat(sig.slice(0, 32), sig.slice(32, 64)))
|
|
249
|
+
.addRecoveryBit(v).recoverPublicKey(digest);
|
|
250
|
+
return bytesToHex(keccak256(pub.toBytes(false).slice(1)).slice(12));
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
};
|