@ninsei-labs/sdk 0.39.2 → 0.40.0-develop.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +4 -1
  3. package/engine/atomic/dleq2.js +86 -111
  4. package/engine/atomic/halfenc.js +58 -72
  5. package/engine/atomic/halves.js +31 -53
  6. package/engine/atomic/order-client.js +15 -16
  7. package/engine/atomic/order.js +44 -62
  8. package/engine/atomic/orderContext.js +26 -50
  9. package/engine/atomic/orderHalfCommit.js +17 -18
  10. package/engine/core/chainSource.js +54 -69
  11. package/engine/core/clock.js +11 -12
  12. package/engine/core/config.js +554 -449
  13. package/engine/core/finality.js +34 -53
  14. package/engine/core/format.js +35 -36
  15. package/engine/core/preSignGuards.js +109 -117
  16. package/engine/core/rfqSource.js +57 -75
  17. package/engine/core/sellWindow.js +23 -24
  18. package/engine/core/store.js +20 -21
  19. package/engine/core/swap.js +242 -243
  20. package/engine/evm/amounts.js +50 -51
  21. package/engine/evm/asyncExec.js +577 -0
  22. package/engine/evm/asyncLegs.js +308 -0
  23. package/engine/evm/asyncRoute.js +179 -0
  24. package/engine/evm/auth.js +41 -48
  25. package/engine/evm/claimGas.js +28 -47
  26. package/engine/evm/depositor.js +39 -43
  27. package/engine/evm/dex.js +266 -267
  28. package/engine/evm/escrow.js +113 -129
  29. package/engine/evm/factory.js +71 -77
  30. package/engine/evm/fees.js +41 -42
  31. package/engine/evm/funding.js +160 -162
  32. package/engine/evm/gasReserve.js +67 -36
  33. package/engine/evm/index.js +46 -53
  34. package/engine/evm/injected.js +34 -42
  35. package/engine/evm/permit.js +129 -56
  36. package/engine/evm/prices.js +30 -45
  37. package/engine/evm/session.js +188 -189
  38. package/engine/mock/chain.js +17 -18
  39. package/engine/monero/address.js +33 -48
  40. package/engine/monero/halvesSweep.js +25 -43
  41. package/engine/monero/node.js +14 -15
  42. package/engine/monero/scan.js +82 -95
  43. package/engine/monero/swapKeys.js +30 -33
  44. package/engine/monero/unlock.js +17 -20
  45. package/engine/monero/wallet.js +32 -42
  46. package/engine/recovery/recoveryFile.js +154 -161
  47. package/engine/recovery/restore.js +50 -61
  48. package/engine/sdk/bridge.js +244 -234
  49. package/engine/ui/dom.js +4 -5
  50. package/engine/ui/walletModal.js +20 -31
  51. package/index.d.ts +26 -0
  52. package/package.json +1 -1
  53. package/src/adapters/evm-wallet.mjs +16 -0
  54. package/src/adapters/monero-wallet.mjs +1 -1
  55. package/src/adapters/monero.mjs +1 -1
  56. package/src/adapters/node-test-evm-wallet.mjs +5 -5
  57. package/src/codes.mjs +5 -0
  58. package/src/config.mjs +1 -1
  59. package/src/engine.mjs +4 -0
  60. package/src/index.mjs +16 -2
  61. package/src/legs/cow-spec.mjs +254 -0
  62. package/src/legs/cow.mjs +315 -0
  63. package/src/legs/evm.mjs +86 -2
  64. package/src/legs/kyber-spec.mjs +265 -0
  65. package/src/legs/kyber.mjs +425 -0
  66. package/src/legs/shape.mjs +35 -0
  67. package/src/legs/uniswaprelay-spec.mjs +541 -0
  68. package/src/legs/uniswaprelay.mjs +422 -0
  69. package/src/legs/uniswapx-spec.mjs +329 -0
  70. package/src/legs/uniswapx.mjs +362 -0
  71. package/src/quotes.mjs +23 -5
  72. package/src/swaps.mjs +18 -1
@@ -1,33 +1,28 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/auth.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
- // ВХОД ПО ПОДПИСИ КОШЕЛЬКА И СПИСОК СДЕЛОК НА СЕРВЕРЕ.
5
+ // WALLET-SIGNATURE LOGIN AND THE SERVER-SIDE SWAP LIST.
6
+ // Why: while swap records live only in localStorage, "my list" is tied to the browser. A server-side list
7
+ // fixes that; binding it to the wallet address keeps other people's swaps hidden.
7
8
  //
8
- // Зачем. Пока записи о сделках живут только в localStorage, "мой список" привязан к браузеру: очистили
9
- // данные сайта, открыли с другого устройства - список пуст, хотя деньги в эскроу на месте. Серверный
10
- // список снимает это ограничение, а привязка к адресу кошелька не даёт видеть чужое.
9
+ // A SIGNATURE, NOT JUST A CONNECTION. Connecting is only a channel. Proving you own the address needs a
10
+ // message signature, re-done every time: the pass lives in sessionStorage and dies with the tab.
11
11
  //
12
- // ПОДПИСЬ, А НЕ ПРОСТО ПОДКЛЮЧЕНИЕ. Подключение кошелька - только канал связи. Доказательство владения
13
- // адресом даёт лишь подпись сообщения, и подписывать приходится КАЖДЫЙ РАЗ заново: пропуск живёт в
14
- // sessionStorage, то есть умирает вместе с вкладкой. Так решено сознательно: безопасность важнее удобства.
12
+ // WHAT GOES TO THE SERVER AND WHAT DOES NOT. Only public fields: escrow address, amounts, deadlines,
13
+ // hashlock, network, swap id. THE SECRET NEVER GOES: it grants the money and stays in the recovery file.
15
14
  //
16
- // ЧТО УХОДИТ НА СЕРВЕР И ЧТО НЕТ. Уходят только публичные поля: адрес эскроу, суммы, сроки, hashlock,
17
- // сеть, id сделки. СЕКРЕТ НЕ УХОДИТ НИКОГДА: он даёт право на деньги и остаётся в файле восстановления.
18
- // Сервер не сможет распорядиться средствами, даже если захочет, - у него нет ни ключей, ни секрета.
19
- //
20
- // ОТКАЗ СЕРВЕРА НЕ ЛОМАЕТ СДЕЛКУ. Синхронизация - удобство, а не условие: если она не удалась, сделка
21
- // продолжается на локальной записи, и об этом честно сказано в консоли. Хуже было бы наоборот.
15
+ // A SERVER REFUSAL DOES NOT BREAK THE SWAP: sync is a convenience, and on failure the swap continues on
16
+ // the local record (said honestly in the console).
22
17
  import { signMessage, address, isConnected } from "./session.js";
23
18
 
24
- const TOKEN_KEY = "arrakis.auth.token";
25
- const ADDR_KEY = "arrakis.auth.address";
19
+ const TOKEN_KEY = "ninsei.auth.token";
20
+ const ADDR_KEY = "ninsei.auth.address";
26
21
 
27
- // Пропуск и адрес держим в sessionStorage: он переживает перезагрузку страницы, но исчезает вместе с
28
- // вкладкой - ровно то поведение, которое выбрано ("пока открыт браузер валидно").
29
- // СЕССИОННОЕ ХРАНИЛИЩЕ - СНАРУЖИ, как и хранилище состояния (www/js/core/store.js). В браузере это
30
- // sessionStorage, в проверках и в SDK - память: модуль обязан грузиться без DOM.
22
+ // The pass and the address live in sessionStorage: it survives a reload but dies with the tab - exactly the
23
+ // chosen behaviour ("valid while the browser is open"). SESSION STORAGE COMES FROM OUTSIDE, like the state
24
+ // store (www/js/core/store.js): sessionStorage in the browser, memory in the checks and the SDK - the module
25
+ // must load without a DOM.
31
26
  const sessionAdapters = () => {
32
27
  const store = typeof globalThis !== "undefined" ? globalThis.sessionStorage : undefined;
33
28
  if (!store || typeof store.getItem !== "function") return null;
@@ -43,17 +38,17 @@ const memoryStore = () => {
43
38
  };
44
39
  let sessionStore = sessionAdapters() || memoryStore();
45
40
 
46
- /** Подменить сессионное хранилище: годится и sessionStorage, и память, и адаптер SDK. */
41
+ /** Swap the session storage: sessionStorage, memory or an SDK adapter all qualify. */
47
42
  export function setSessionStore(next) {
48
43
  const ok = next && typeof next.get === "function" && typeof next.set === "function" && typeof next.remove === "function";
49
- if (!ok) throw new Error("сессионное хранилище должно уметь get/set/remove");
44
+ if (!ok) throw new Error("session storage must support get/set/remove");
50
45
  sessionStore = next;
51
46
  return sessionStore;
52
47
  }
53
48
  export const sessionStoreInUse = () => sessionStore;
54
49
 
55
50
  function readSession(key) { try { return sessionStore.get(key); } catch { return null; } }
56
- function writeSession(key, value) { try { value == null ? sessionStore.remove(key) : sessionStore.set(key, value); } catch { /* тишина намеренно: недоступное хранилище не должно ломать вход */ } }
51
+ function writeSession(key, value) { try { value == null ? sessionStore.remove(key) : sessionStore.set(key, value); } catch { /* silence on purpose: an unavailable store must not break login */ } }
57
52
 
58
53
  export function token() { return readSession(TOKEN_KEY); }
59
54
  export function authedAddress() { return readSession(ADDR_KEY); }
@@ -62,17 +57,17 @@ export function forgetSession() { writeSession(TOKEN_KEY, null); writeSession(AD
62
57
  async function api(path, init = {}) {
63
58
  const res = await fetch(path, init);
64
59
  let body = null;
65
- try { body = await res.json(); } catch { /* не JSON - ниже вернём как есть */ }
60
+ try { body = await res.json(); } catch { /* not JSON - returned as is below */ }
66
61
  return { status: res.status, body };
67
62
  }
68
63
 
69
- // Вход: получаем одноразовое число, подписываем сообщение КОТОРОЕ ДАЛ СЕРВЕР (не своё: иначе расхождение
70
- // в одном символе давало бы "подпись не сходится" без объяснения) и обмениваем подпись на пропуск.
64
+ // Login: get a one-time number, sign the message GIVEN BY THE SERVER (not our own: a one-char mismatch
65
+ // would give "signature does not match" with no explanation) and exchange the signature for a pass.
71
66
  export async function signIn() {
72
- if (!isConnected()) throw new Error("сначала подключите кошелёк");
67
+ if (!isConnected()) throw new Error("connect the wallet first");
73
68
  const nonce = await api("/api/auth/nonce");
74
69
  if (nonce.status !== 200 || !nonce.body || !nonce.body.message) {
75
- throw new Error("сервер не выдал сообщение для подписи (ответ " + nonce.status + ")");
70
+ throw new Error("the server did not issue a message to sign (response " + nonce.status + ")");
76
71
  }
77
72
  const signature = await signMessage(nonce.body.message);
78
73
  const verified = await api("/api/auth/verify", {
@@ -81,17 +76,16 @@ export async function signIn() {
81
76
  body: JSON.stringify({ nonce: nonce.body.nonce, address: address(), signature }),
82
77
  });
83
78
  if (verified.status !== 200 || !verified.body || !verified.body.token) {
84
- const why = verified.body && verified.body.error ? verified.body.error : "ответ " + verified.status;
85
- throw new Error("вход не принят: " + why);
79
+ const why = verified.body && verified.body.error ? verified.body.error : "response " + verified.status;
80
+ throw new Error("login not accepted: " + why);
86
81
  }
87
82
  writeSession(TOKEN_KEY, verified.body.token);
88
83
  writeSession(ADDR_KEY, verified.body.address);
89
84
  return verified.body.address;
90
85
  }
91
86
 
92
- // Запрос с пропуском. При 401 пропуск мог истечь или сервер перезапуститься - пробуем один раз войти
93
- // заново, и только если и это не вышло, честно возвращаем отказ: молчаливое повторение подписи
94
- // раздражало бы кошелёк запросами без причины.
87
+ // A request with the pass. On 401 the pass may have expired or the server restarted - try to log in once
88
+ // more, and only if that also fails return the refusal honestly: silent re-signing would spam the wallet.
95
89
  async function authed(path, init = {}, retry = true) {
96
90
  const t = token();
97
91
  const headers = { ...(init.headers || {}) };
@@ -105,12 +99,12 @@ async function authed(path, init = {}, retry = true) {
105
99
  return r;
106
100
  }
107
101
 
108
- // Отправить запись о сделке. Возвращает { ok, reason }: отказ не исключение, а факт, о котором вызывающий
109
- // решает сам - сделка из-за него прерываться не должна.
102
+ // Send a swap record. Returns { ok, reason }: a refusal is a fact, not an exception, and the caller
103
+ // decides - the swap must not abort because of it.
110
104
  export async function saveServerSwap(swap, network) {
111
105
  const escrow = swap && swap.escrow && swap.escrow.address;
112
- if (!escrow) return { ok: false, reason: "в записи нет адреса эскроу" };
113
- if (!isConnected()) return { ok: false, reason: "кошелёк не подключён" };
106
+ if (!escrow) return { ok: false, reason: "the record has no escrow address" };
107
+ if (!isConnected()) return { ok: false, reason: "wallet not connected" };
114
108
  try {
115
109
  if (!token() || authedAddress() !== String(address()).toLowerCase()) { forgetSession(); await signIn(); }
116
110
  const r = await authed("/api/evm-swaps", {
@@ -118,9 +112,9 @@ export async function saveServerSwap(swap, network) {
118
112
  headers: { "content-type": "application/json" },
119
113
  body: JSON.stringify({
120
114
  id: swap.id, network: network || swap.network || swap.chain, escrow,
121
- // ССЫЛКА НА КОТИРОВКУ. По ней сервер и нода провайдера понимают, ЧЕЙ это ордер: без неё адрес эскроу
122
- // остаётся ноде неизвестным, и забрать ETH она не сможет. Отсутствие поля не ошибка: старые записи
123
- // и сделки без провайдера отправляются как есть.
115
+ // QUOTE REFERENCE. Through it the server and the provider node know WHOSE order this is: without it
116
+ // the escrow address stays unknown to the node and it cannot claim the ETH. A missing field is not
117
+ // an error: old records and provider-less swaps are sent as is.
124
118
  quoteId: swap.orderQuoteId || null,
125
119
  providerId: swap.providerId || null,
126
120
  hashlock: (swap.escrow && swap.escrow.hashlock) || swap.hashlock || null,
@@ -130,25 +124,24 @@ export async function saveServerSwap(swap, network) {
130
124
  }),
131
125
  });
132
126
  if (r.status === 201 || r.status === 200) {
133
- // СЕРВЕР ОТВЕЧАЕТ И ПРО ПРИВЯЗКУ КОТИРОВКИ. Без неё нода провайдера не заберёт ETH: её половина
134
- // осталась бы невостребованной. Отказ привязки сделку не прерывает, но и молчать о нём нельзя -
135
- // поэтому он уезжает вызывающему, а тот говорит в консоль.
127
+ // THE SERVER ALSO ANSWERS ABOUT THE QUOTE BINDING. Without it the provider node cannot claim the ETH.
128
+ // A binding refusal does not abort the swap, but must not be silent either - it goes to the caller, which logs it.
136
129
  return { ok: true, orderBinding: (r.body && r.body.orderBinding) || null };
137
130
  }
138
- const why = r.body && r.body.error ? r.body.error : "ответ " + r.status;
131
+ const why = r.body && r.body.error ? r.body.error : "response " + r.status;
139
132
  return { ok: false, reason: why };
140
133
  } catch (e) {
141
134
  return { ok: false, reason: String((e && e.message) || e) };
142
135
  }
143
136
  }
144
137
 
145
- // Список сделок с сервера. Пустой результат и отказ - разные вещи, и вызывающий видит разницу.
138
+ // The swap list from the server. An empty result and a refusal are different, and the caller sees the difference.
146
139
  export async function loadServerSwaps() {
147
- if (!isConnected()) return { ok: false, reason: "кошелёк не подключён", swaps: [] };
140
+ if (!isConnected()) return { ok: false, reason: "wallet not connected", swaps: [] };
148
141
  try {
149
142
  if (!token()) await signIn();
150
143
  const r = await authed("/api/evm-swaps");
151
- if (r.status !== 200 || !r.body) return { ok: false, reason: "ответ " + r.status, swaps: [] };
144
+ if (r.status !== 200 || !r.body) return { ok: false, reason: "response " + r.status, swaps: [] };
152
145
  return { ok: true, swaps: r.body.swaps || [] };
153
146
  } catch (e) {
154
147
  return { ok: false, reason: String((e && e.message) || e), swaps: [] };
@@ -1,38 +1,23 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/claimGas.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
- // ГАЗ ЗАБОРА ПРИ ПРОДАЖЕ XMR (claim): ОДИН ИСТОЧНИК ПРАВДЫ ДЛЯ КЛИЕНТА И ДЛЯ НОДЫ (issue #127).
7
- //
8
- // СУТЬ. В обратном обмене человек отдаёт XMR и забирает ETH вызовом `claim` в сети расчёта. Своего ETH у
9
- // него нет, поэтому газ на claim ПРИСЫЛАЕТ НОДА (issue #78, rfq/reverseGas.mjs). Но правила газа жили в
10
- // ДВУХ местах: страница/SDK отправляли claim с пределом 250 000 и комиссией `2 x baseFee + 1 gwei`
11
- // (движок сделки теперь в пакете: sdk/src/swap-flow.mjs; www/js/evm/session.js), а нода считала подарок как `eth_gasPrice x 120 000`
12
- // (rfq/reverseGas.mjs). Числа расходились примерно в 100 раз, и человек с одним подарком claim отправить
13
- // не мог: узел принимает транзакцию, только если на балансе есть `gasLimit x maxFeePerGas`, а подарка на
14
- // это не хватало. Теперь оба числа считает ЭТОТ модуль.
15
- //
16
- // ПОЧЕМУ ОТДЕЛЬНЫЙ МОДУЛЬ И ПОЧЕМУ ПОД www/js. Формула обязана быть ОДНА. Её читает и страница/SDK
17
- // (движок живёт под www/js), и нода - rfq/reverseGas.mjs, а rfq уже импортирует из www/js (см.
18
- // rfq/payoutFee.mjs, `../www/js/core/format.js`). Обратный импорт невозможен: боевой веб-корень - www/,
19
- // каталог rfq/ браузеру не отдаётся. Поэтому единственный общий дом - www/js/evm/.
20
- //
21
- // ЧТО ЗДЕСЬ ОДНО. Предел газа claim, правило комиссии (2 x baseFee + чаевые) и РАЗМЕР ПОДАРКА - та же
22
- // стоимость claim плюс ЯВНЫЙ ЗАПАС на рост базовой цены между выдачей билета и забором. Второго расчёта
23
- // размера подарка нет ни у ноды, ни у прогонов: они зовут claimGasGiftWei отсюда.
5
+ // CLAIM GAS FOR THE XMR SELL (claim): ONE SOURCE OF TRUTH FOR THE CLIENT AND THE NODE.
6
+ // In a reverse swap the person gives XMR and claims ETH via `claim` on the settlement chain. They have no ETH,
7
+ // so the node sends the claim gas. The gas rules used to live in TWO places and the numbers differed ~100x, so
8
+ // a person with one gift could not send the claim. Now this module computes both numbers.
9
+ // WHY ONE MODULE UNDER www/js: the formula must be ONE. The EVM engine lives under www/js, and the node imports
10
+ // from www/js too; the reverse import is impossible (the browser is never served the node directory).
11
+ // WHAT IS ONE HERE: the claim gas limit, the fee rule (2 x baseFee + tip) and the GIFT SIZE - the same claim
12
+ // cost plus an EXPLICIT margin for base-fee growth between ticket and claim.
24
13
 
25
- // ПРЕДЕЛ ГАЗА CLAIM - ОДНО ЧИСЛО НА ОБЕИХ СТОРОНАХ: его берут и страница/SDK (sdk/src/swap-flow.mjs), и
26
- // нода (rfq/payout.mjs). ИЗМЕРЕНО: честный claim, когда получателю комиссии ещё НЕ начислено (первая запись
27
- // в кассе = SSTORE нуля), стоит 120 721 газа на живой цепи (124 072 в gas-report канонических контрактов) -
28
- // больше прежних 120 000, чем законный забор и резался. Лимит задан с запасом: 200 000.
14
+ // THE CLAIM GAS LIMIT IS ONE NUMBER ON BOTH SIDES. MEASURED: an honest claim costs 120 721 gas on a live chain,
15
+ // more than the old 120 000, which cut off legitimate claims. The limit has a margin: 200 000.
29
16
  export const CLAIM_GAS_LIMIT = 200_000n;
30
17
 
31
- // ПОЛ ЧАЕВЫХ - ПОЛИТИКА СЕТИ, А НЕ КОНСТАНТА В КОДЕ ОТПРАВКИ. Раньше session.js ставил минимум 1 gwei
32
- // ВСЕГДА. На сети с дешёвым газом (Arbitrum One: baseFee ~0.02 gwei) это раздувало maxFeePerGas примерно
33
- // в 50 раз и поднимало ТРЕБУЕМЫЙ БАЛАНС пропорционально - то есть ломало ровно того, кому газ и
34
- // предназначен. Для Arbitrum чаевые не нужны: пол 0. Сеть со своей политикой передаёт minTipWei
35
- // аргументом (session.js берёт сеть из реестра, www/js/core/config.js).
18
+ // THE TIP FLOOR IS NETWORK POLICY, NOT A CONSTANT IN THE SENDING CODE. A fixed 1 gwei minimum inflated
19
+ // maxFeePerGas ~50x on cheap-gas networks and broke exactly those the gas is for. Arbitrum needs no tip
20
+ // (floor 0); a network with its own policy passes minTipWei, taken from the network registry.
36
21
  export const CLAIM_TIP_FLOOR_WEI = 0n;
37
22
 
38
23
  const toBig = (v) => {
@@ -40,10 +25,9 @@ const toBig = (v) => {
40
25
  try { return BigInt(typeof v === "bigint" ? v : String(v)); } catch { return null; }
41
26
  };
42
27
 
43
- // КОМИССИЯ ЗАБОРА (EIP-1559): `maxFeePerGas = 2 x baseFee + чаевые`. Удвоение базовой цены перекрывает её
44
- // рост за время подтверждения СВОЕЙ транзакции; чаевые - это разница gasPrice - baseFee (её отдаёт любая
45
- // цепь), не ниже пола сети. baseFee = 0 (сеть без EIP-1559) - полей комиссии нет вовсе, и это null, а не
46
- // ноль: неизвестное обязано называться, а не выдаваться за измеренное.
28
+ // CLAIM FEE (EIP-1559): `maxFeePerGas = 2 x baseFee + tip`. Doubling the base fee covers its growth during the
29
+ // transaction; the tip is gasPrice - baseFee, not below the network floor. baseFee = 0 (no EIP-1559): no fee
30
+ // fields at all, and that is null, not zero.
47
31
  export function claimMaxFeePerGasWei({ baseFeeWei, priorityFeeWei = 0n, minTipWei = CLAIM_TIP_FLOOR_WEI } = {}) {
48
32
  const base = toBig(baseFeeWei);
49
33
  if (base === null || base <= 0n) return null;
@@ -54,8 +38,8 @@ export function claimMaxFeePerGasWei({ baseFeeWei, priorityFeeWei = 0n, minTipWe
54
38
  return base * 2n + tip;
55
39
  }
56
40
 
57
- // СКОЛЬКО НУЖНО НА БАЛАНСЕ ДЛЯ CLAIM - это же правило приёма транзакции узлом: `gasLimit x maxFeePerGas`.
58
- // Один из множителей не измерен - null (не «ноль газа»).
41
+ // HOW MUCH THE BALANCE NEEDS FOR CLAIM - the node acceptance rule: `gasLimit x maxFeePerGas`. If a factor is
42
+ // unmeasured - null (not "zero gas").
59
43
  export function claimRequiredWei({ gasLimit = CLAIM_GAS_LIMIT, maxFeePerGasWei } = {}) {
60
44
  const limit = toBig(gasLimit);
61
45
  const maxFee = toBig(maxFeePerGasWei);
@@ -63,21 +47,18 @@ export function claimRequiredWei({ gasLimit = CLAIM_GAS_LIMIT, maxFeePerGasWei }
63
47
  return limit * maxFee;
64
48
  }
65
49
 
66
- // ЗАПАС НА РОСТ БАЗОВОЙ ЦЕНЫ МЕЖДУ ВЫДАЧЕЙ БИЛЕТА И ЗАБОРОМ, в базисных пунктах (10 000 = +100%).
50
+ // MARGIN FOR BASE-FEE GROWTH BETWEEN TICKET AND CLAIM, in basis points (10 000 = +100%).
67
51
  //
68
- // ПОЧЕМУ ОН НУЖЕН И ПОЧЕМУ СТОЛЬКО. Стоимость claim считается на момент БИЛЕТА, а сам claim случается
69
- // позже: билет живёт RFQ_ORDER_QUOTE_TTL_MS, по умолчанию 40 минут (rfq/config.mjs). Правило клиента
70
- // (2 x baseFee) перекрывает рост цены ТОЛЬКО на время подтверждения своей транзакции, а не эти 40 минут.
71
- // Подарок без запаса не проходит, если базовая цена хоть немного поднялась с момента билета - это и
72
- // показал issue #127. +100% перекрывает УДВОЕНИЕ базовой цены за окно билета, то есть подарок остаётся
73
- // достаточным, даже если цена газа к забору вырастет вдвое. Значение - настройка ноды
74
- // (RFQ_REVERSE_GAS_RESERVE_BPS), а не догадка: её видно и можно переопределить без правки кода.
52
+ // WHY IT IS NEEDED AND WHY THIS MUCH. The claim cost is computed at TICKET time, but the claim happens later:
53
+ // the ticket lives RFQ_ORDER_QUOTE_TTL_MS, 40 minutes by default. The client rule (2 x baseFee) covers growth
54
+ // only during its own transaction, not those 40 minutes. +100% covers a doubling of the base fee over the
55
+ // ticket window, so the gift stays sufficient. The value is a node setting (RFQ_REVERSE_GAS_RESERVE_BPS), not
56
+ // a guess: it is visible and can be overridden.
75
57
  export const DEFAULT_GIFT_RESERVE_BPS = 10_000n;
76
58
 
77
- // РАЗМЕР ПОДАРКА - ТА ЖЕ стоимость claim, что требует клиент, ПЛЮС запас. Это ЕДИНСТВЕННЫЙ расчёт размера
78
- // подарка: нода (rfq/reverseGas.mjs, requiredGasWei) и живой прогон зовут ИМЕННО ЕГО, а не свою копию.
79
- // base берётся у цепи на момент билета; priorityFeeWei - реальные чаевые сети (gasPrice - baseFee), если
80
- // известны. Нечитаемая база - null, а не ноль.
59
+ // GIFT SIZE - the same claim cost the client requires, PLUS the margin. This is the ONLY computation of the
60
+ // gift size: the node and the live run call IT, not their own copy. base comes from the chain at ticket time;
61
+ // priorityFeeWei is the real network tip. An unreadable base is null, not zero.
81
62
  export function claimGasGiftWei({ baseFeeWei, priorityFeeWei, minTipWei, gasLimit = CLAIM_GAS_LIMIT, reserveBps = DEFAULT_GIFT_RESERVE_BPS } = {}) {
82
63
  const maxFee = claimMaxFeePerGasWei({ baseFeeWei, priorityFeeWei, minTipWei });
83
64
  const need = claimRequiredWei({ gasLimit, maxFeePerGasWei: maxFee });
@@ -1,34 +1,31 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/depositor.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
- // ВНОСЯЩИЙ ПО ПОДПИСИ, КЛИЕНТСКАЯ СТОРОНА (issue #97, часть C): EIP-712 typed data над ДАЙДЖЕСТОМ КОТИРОВКИ.
5
+ // THE DEPOSITOR BY SIGNATURE, CLIENT SIDE: EIP-712 typed data over the QUOTE DIGEST.
7
6
  //
8
- // ЗАЧЕМ. Часть A дала фабрике путь `createOrderAndFundByDepositor`: транзакцию вправе отправить КТО УГОДНО
9
- // (релейщик, наш сервис), но вносившим записывается тот, кто подписал дайджест котировки. Чтобы релейщик
10
- // мог отправить транзакцию, вносящий обязан сперва подписать. Подпись кошелька - это typed data, а не
11
- // транзакция: она не стоит газа и не двигает деньги, поэтому человек БЕЗ своего ETH тоже может её дать.
7
+ // WHY. The factory has a path createOrderAndFundByDepositor: ANYONE may send the transaction (a relayer, our
8
+ // service), but the depositor is whoever signed the quote digest, so the depositor must sign first. A wallet
9
+ // signature is typed data, not a transaction: it costs no gas and moves no money, so a person without ETH can give it.
12
10
  //
13
- // ПОЧЕМУ ФОРМА ЗДЕСЬ, А НЕ ВЫДУМАНА. Набор полей и домен - ЧАСТЬ ПРОТОКОЛА: тот же OrderQuote, что
14
- // проверяет эскроу (mvp/contracts/OrderQuote.sol) и подписывает нода (rfq/lib/quoteEip712Spec.mjs,
15
- // sdk/src/quoteEip712Spec.mjs). Перестановка двух полей даёт ДРУГОЙ дайджест, подпись восстанавливается в
16
- // чужой адрес, и фабрика отказывает `BadDepositorSignature`. Поэтому форма лежит ОДНИМ списком ниже, а
17
- // проверка (tools/check-usdc-without-eth.mjs) считает этой формой дайджест и сверяет его с дайджестом,
18
- // посчитанным СПЕКОЙ ноды/SDK на той же котировке - разойтись молча они не могут.
11
+ // WHY THIS FORM, NOT AN INVENTED ONE. The field set and domain are PART OF THE PROTOCOL - the same OrderQuote
12
+ // the escrow verifies and the node signs. Two swapped fields give a DIFFERENT digest and the factory refuses
13
+ // BadDepositorSignature. So the form is ONE list below, and a check computes the digest with it and compares.
19
14
  //
20
- // ДОМЕН. verifyingContract - АДРЕС ФАБРИКИ ИЗ САМОЙ КОТИРОВКИ (поле `factory`), chainId - живой из котировки:
21
- // подпись, сделанная для другой фабрики или сети, здесь не восстановится. Второго источника домена нет.
15
+ // DOMAIN. verifyingContract is the FACTORY ADDRESS FROM THE QUOTE (field `factory`), chainId is live: a signature
16
+ // made for another factory or network will not recover here. There is no second source of the domain.
22
17
  //
23
- // ЧЕГО ЗДЕСЬ НЕТ. Криптографии: дайджест считает КОШЕЛЁК (eth_signTypedData_v4), а не этот модуль. Здесь -
24
- // только форма и её проверка, чтобы кошелёк подписал ровно то, что проверит цепь.
18
+ // NO CRYPTOGRAPHY HERE: the wallet computes the digest (eth_signTypedData_v4). Here is only the form and its check.
25
19
 
26
- // Имя и версия домена - протокол котировок v8, дословно как в OrderQuote.sol.
27
- export const DEPOSITOR_DOMAIN_NAME = "NinseiEscrowFactory";
28
- export const DEPOSITOR_DOMAIN_VERSION = "1";
20
+ import { SIGNING_DOMAIN } from "../core/config.js";
29
21
 
30
- // НАБОР ПОЛЕЙ В ПОРЯДКЕ КОНТРАКТА. Порядок - часть ABI: перестановка даёт другой дайджест. Список один -
31
- // из него строится и строка типа, и сообщение, разойтись им нечем.
22
+ // The domain name and version are quote protocol v8. They are NOT brand literals here: they belong to the registry
23
+ // contract (it returns DOMAIN_SEPARATOR()), so they are read from the config. The defaults equal today's values.
24
+ export const DEPOSITOR_DOMAIN_NAME = SIGNING_DOMAIN.name;
25
+ export const DEPOSITOR_DOMAIN_VERSION = SIGNING_DOMAIN.version;
26
+
27
+ // FIELD SET IN CONTRACT ORDER. The order is part of the ABI: a swap gives another digest. One list feeds both
28
+ // the type string and the message, so they cannot diverge.
32
29
  export const ORDER_QUOTE_TYPED_FIELDS = [
33
30
  ["provider", "address"],
34
31
  ["quoteKey", "address"],
@@ -48,47 +45,46 @@ export const ORDER_QUOTE_TYPED_FIELDS = [
48
45
  ["feeRecipient", "address"],
49
46
  ["validUntil", "uint64"],
50
47
  ["nonce", "uint256"],
51
- // THE XMR SIDE OF THE DEAL (#110 step 3), in atomic units: it joined the signed set together with the contract
48
+ // THE XMR SIDE OF THE DEAL, in atomic units: it joined the signed set together with the contract
52
49
  // (OrderQuote.sol), the node/SDK spec and the vector, so the depositor's signature covers the same XMR the chain
53
50
  // verifies. A field the chain could not see used to stay OUT of this list on purpose - that is no longer the case.
54
51
  ["xmrAmount", "uint256"],
55
52
  ];
56
53
 
57
- // Тип сообщения. Собирается из списка: правка списка меняет и тип, и подпись - то есть не может остаться
58
- // незамеченной, в отличие от второго литерала рядом.
54
+ // The message type. Built from the list: editing the list changes both the type and the signature, so it cannot
55
+ // go unnoticed.
59
56
  export const ORDER_QUOTE_TYPED_TYPE = "OrderQuote(" + ORDER_QUOTE_TYPED_FIELDS.map(([n, t]) => t + " " + n).join(",") + ")";
60
57
 
61
58
  const isHex = (v, bytes) => new RegExp("^(0x)?[0-9a-fA-F]{" + bytes * 2 + "}$").test(String(v === undefined || v === null ? "" : v).trim());
62
59
  const isNum = (v) => /^(0x[0-9a-fA-F]+|[0-9]+)$/.test(String(v === undefined || v === null ? "" : v).trim());
63
60
 
64
- // ФОРМА ПРИВОДИТСЯ, А НЕ ТРЕБУЕТСЯ. Точки ed25519 котировка несёт БЕЗ префикса 0x (так их пишет нода),
65
- // а кошелёк и контракт ждут одинаковые БАЙТЫ. Поэтому приводим к одному виду здесь, на границе, и
66
- // отвергаем всё, что не адрес/не bytes32/не число: молчаливое выравнивание нулями ушло бы ДРУГИМ значением.
61
+ // FORM IS NORMALISED, NOT DEMANDED. The quote carries ed25519 points WITHOUT the 0x prefix, while the wallet and
62
+ // contract expect the same BYTES. So we normalise here and reject anything that is not address/bytes32/number.
67
63
  function normaliseField(name, type, value) {
68
64
  const raw = String(value === undefined || value === null ? "" : value).trim();
69
- if (raw === "") throw new Error("котировка не несёт поля для подписи вносящего: " + name);
65
+ if (raw === "") throw new Error("the quote carries no depositor-signature field: " + name);
70
66
  if (type === "address") {
71
- if (!isHex(raw, 20)) throw new Error("поле " + name + " - не адрес: " + raw);
67
+ if (!isHex(raw, 20)) throw new Error("field " + name + " is not an address: " + raw);
72
68
  return "0x" + raw.replace(/^0x/, "").toLowerCase();
73
69
  }
74
70
  if (type === "bytes32") {
75
- if (!isHex(raw, 32)) throw new Error("поле " + name + " - не bytes32: " + raw);
71
+ if (!isHex(raw, 32)) throw new Error("field " + name + " is not bytes32: " + raw);
76
72
  return "0x" + raw.replace(/^0x/, "").toLowerCase();
77
73
  }
78
- // uint64 / uint256: кошелёк принимает строку; десятичную и hex-форму даёт один и тот же разбор.
79
- if (!isNum(raw)) throw new Error("поле " + name + " - не число: " + raw);
74
+ // uint64 / uint256: the wallet accepts a string; decimal and hex are parsed the same.
75
+ if (!isNum(raw)) throw new Error("field " + name + " is not a number: " + raw);
80
76
  return String(BigInt(raw));
81
77
  }
82
78
 
83
79
  /**
84
- * EIP-712 typed data, которое подписывает вносящий (eth_signTypedData_v4).
85
- * Домен строится ИЗ КОТИРОВКИ: verifyingContract = quote.factory, chainId = quote.chainId.
80
+ * EIP-712 typed data that the depositor signs (eth_signTypedData_v4).
81
+ * The domain is built FROM THE QUOTE: verifyingContract = quote.factory, chainId = quote.chainId.
86
82
  * @returns {{ domain: object, types: object, primaryType: string, message: object }}
87
83
  */
88
84
  export function orderQuoteTypedData(quote) {
89
- if (!quote || typeof quote !== "object") throw new Error("нет подписанной котировки: подписывать нечего");
90
- if (!isHex(quote.factory, 20)) throw new Error("в котировке нет адреса фабрики (factory) - домен подписи не собрать");
91
- if (!isNum(quote.chainId)) throw new Error("в котировке нет chainId - домен подписи не собрать");
85
+ if (!quote || typeof quote !== "object") throw new Error("no signed quote: nothing to sign");
86
+ if (!isHex(quote.factory, 20)) throw new Error("the quote has no factory address - cannot build the signing domain");
87
+ if (!isNum(quote.chainId)) throw new Error("the quote has no chainId - cannot build the signing domain");
92
88
  const message = {};
93
89
  for (const [name, type] of ORDER_QUOTE_TYPED_FIELDS) {
94
90
  message[name] = normaliseField(name, type, quote[name]);
@@ -101,7 +97,7 @@ export function orderQuoteTypedData(quote) {
101
97
  verifyingContract: "0x" + String(quote.factory).replace(/^0x/, "").toLowerCase(),
102
98
  },
103
99
  types: {
104
- // EIP712Domain явно: кошельки показывают его человеку, и он обязан совпасть с тем, что считает цепь.
100
+ // EIP712Domain explicit: wallets show it to the person, and it must match what the chain computes.
105
101
  EIP712Domain: [
106
102
  { name: "name", type: "string" },
107
103
  { name: "version", type: "string" },
@@ -115,13 +111,13 @@ export function orderQuoteTypedData(quote) {
115
111
  };
116
112
  }
117
113
 
118
- // ПОДПИСЬ НА ПРОВОДЕ - 65 БАЙТ r || s || v. Проверяем форму ЗДЕСЬ: фабрика принимает подпись тремя словами,
119
- // и «похожая на подпись» строка не должна уехать в контракт как согласие.
114
+ // SIGNATURE ON THE WIRE - 65 BYTES r || s || v. The form is checked HERE: the factory takes the signature as
115
+ // three words, and a "signature-like" string must not reach the contract as consent.
120
116
  export function depositorSignatureShape(signature) {
121
117
  const hex = String(signature === undefined || signature === null ? "" : signature).replace(/^0x/, "");
122
- if (!/^[0-9a-fA-F]{130}$/.test(hex)) return { ok: false, reason: "подпись вносящего обязана быть 65 байт (r || s || v), а пришло " + hex.length / 2 + " байт" };
118
+ if (!/^[0-9a-fA-F]{130}$/.test(hex)) return { ok: false, reason: "the depositor signature must be 65 bytes (r || s || v), got " + hex.length / 2 + " bytes" };
123
119
  let v = parseInt(hex.slice(128, 130), 16);
124
120
  if (v < 27) v += 27;
125
- if (v !== 27 && v !== 28) return { ok: false, reason: "v подписи вносящего вне {27,28}: " + v };
121
+ if (v !== 27 && v !== 28) return { ok: false, reason: "depositor signature v outside {27,28}: " + v };
126
122
  return { ok: true, r: "0x" + hex.slice(0, 64).toLowerCase(), s: "0x" + hex.slice(64, 128).toLowerCase(), v: 27 + (v - 27) };
127
123
  }