@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/engine/atomic/dleq2.js +86 -111
  3. package/engine/atomic/halfenc.js +58 -72
  4. package/engine/atomic/halves.js +31 -53
  5. package/engine/atomic/order-client.js +15 -16
  6. package/engine/atomic/order.js +44 -62
  7. package/engine/atomic/orderContext.js +26 -50
  8. package/engine/atomic/orderHalfCommit.js +17 -18
  9. package/engine/core/chainSource.js +54 -69
  10. package/engine/core/clock.js +11 -12
  11. package/engine/core/config.js +554 -449
  12. package/engine/core/finality.js +34 -53
  13. package/engine/core/format.js +35 -36
  14. package/engine/core/preSignGuards.js +109 -117
  15. package/engine/core/rfqSource.js +57 -75
  16. package/engine/core/sellWindow.js +23 -24
  17. package/engine/core/store.js +20 -21
  18. package/engine/core/swap.js +242 -243
  19. package/engine/evm/amounts.js +50 -51
  20. package/engine/evm/asyncExec.js +577 -0
  21. package/engine/evm/asyncLegs.js +308 -0
  22. package/engine/evm/asyncRoute.js +179 -0
  23. package/engine/evm/auth.js +41 -48
  24. package/engine/evm/claimGas.js +28 -47
  25. package/engine/evm/depositor.js +39 -43
  26. package/engine/evm/dex.js +266 -267
  27. package/engine/evm/escrow.js +113 -129
  28. package/engine/evm/factory.js +71 -77
  29. package/engine/evm/fees.js +41 -42
  30. package/engine/evm/funding.js +160 -162
  31. package/engine/evm/gasReserve.js +67 -36
  32. package/engine/evm/index.js +46 -53
  33. package/engine/evm/injected.js +34 -42
  34. package/engine/evm/permit.js +129 -56
  35. package/engine/evm/prices.js +30 -45
  36. package/engine/evm/session.js +188 -189
  37. package/engine/mock/chain.js +17 -18
  38. package/engine/monero/address.js +33 -48
  39. package/engine/monero/halvesSweep.js +25 -43
  40. package/engine/monero/node.js +14 -15
  41. package/engine/monero/scan.js +82 -95
  42. package/engine/monero/swapKeys.js +30 -33
  43. package/engine/monero/unlock.js +17 -20
  44. package/engine/monero/wallet.js +32 -42
  45. package/engine/recovery/recoveryFile.js +154 -161
  46. package/engine/recovery/restore.js +50 -61
  47. package/engine/sdk/bridge.js +244 -234
  48. package/engine/ui/dom.js +4 -5
  49. package/engine/ui/walletModal.js +20 -31
  50. package/index.d.ts +8 -0
  51. package/package.json +1 -1
  52. package/src/adapters/evm-wallet.mjs +16 -0
  53. package/src/adapters/monero-wallet.mjs +1 -1
  54. package/src/adapters/monero.mjs +1 -1
  55. package/src/adapters/node-test-evm-wallet.mjs +5 -5
  56. package/src/config.mjs +1 -1
  57. package/src/engine.mjs +4 -0
  58. package/src/legs/cow-spec.mjs +254 -0
  59. package/src/legs/cow.mjs +315 -0
  60. package/src/legs/evm.mjs +86 -2
  61. package/src/legs/kyber-spec.mjs +265 -0
  62. package/src/legs/kyber.mjs +425 -0
  63. package/src/legs/shape.mjs +35 -0
  64. package/src/legs/uniswaprelay-spec.mjs +541 -0
  65. package/src/legs/uniswaprelay.mjs +422 -0
  66. package/src/legs/uniswapx-spec.mjs +329 -0
  67. package/src/legs/uniswapx.mjs +362 -0
  68. package/src/quotes.mjs +1 -1
  69. 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, then run: node tools/build-sdk-engine.mjs
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
- // Крошечные DOM-хелперы. Никаких фреймворков: демка должна читаться глазами.
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 пишем только через CSSOM: атрибут style="..." запрещён политикой CSP
16
- // (style-src-attr), а el.style.* и cssText - разрешены. Так эта ловушка не вернётся.
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);
@@ -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, then run: node tools/build-sdk-engine.mjs
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
- // Своя модалка подключения кошелька: выбор браузерного кошелька и QR для WalletConnect.
7
- //
8
- // Список браузерных кошельков приходит снаружи (setChoices) - обнаружением занимается
9
- // www/js/evm/injected.js по EIP-6963. Здесь только отрисовка: CSP запрещает inline-стили, поэтому
10
- // всё идёт через dom.h(), а иконки кошельков - data:-URL, что разрешено img-src 'self' data:.
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
- // close() - закрывает программно (например, сессия успешно установлена);
53
- // cancel() - пользователь отказался: вот тут-то и надо гасить сессию, которую мы просили.
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 }]) - список браузерных кошельков и WalletConnect.
79
- // Иконки только из data:-URL (их отдаёт сам кошелёк по EIP-6963): внешние адреса в CSP демки не
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
- // кошелёк, а у WalletConnect нужно ещё показать QR.
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
- // Рамку показываем только когда QR реально есть: до выбора WalletConnect это пустое место.
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 важнее status: иначе переданное «попробуйте ещё раз» перетирает сам текст ошибки, и
124
- // причина отказа видна только в тосте (раньше error влиял лишь на цвет - мёртвый параметр).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninsei-labs/sdk",
3
- "version": "0.39.3",
3
+ "version": "0.40.0-develop.1",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -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 = "arrakis-wallet-session";
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 = () => {
@@ -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 = "arrakis-sweep-session", fields = {} }) {
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 = "ARRAKIS_TEST_EVM_KEY";
36
- export const KEY_FILE_ENV = "ARRAKIS_TEST_EVM_KEY_FILE";
37
- export const RPC_ENV = "ARRAKIS_TEST_EVM_RPC";
38
- export const CHAIN_ENV = "ARRAKIS_TEST_EVM_CHAIN";
39
- export const CLAIMER_ENV = "ARRAKIS_TEST_EVM_CLAIMER";
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: "arrakisswap" },
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
+ };