@ninsei-labs/sdk 0.39.3 → 0.40.0

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 +55 -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 +561 -451
  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 +20 -8
  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
@@ -1,39 +1,28 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/core/rfqSource.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
- // ЖИВЫЕ ИНДИКАТИВНЫЕ КОТИРОВКИ ОТ НАШЕГО ПРОВАЙДЕРА - ЧЕРЕЗ ЯДРО SDK.
5
+ // LIVE INDICATIVE QUOTES FROM OUR PROVIDER - THROUGH THE SDK CORE.
6
+ // WHY THROUGH THE CORE, NOT OUR OWN REQUEST. The read rules (zero = "no direction", staleness by the quote own
7
+ // stamp, a self-contradicting row) used to live here and, separately, in the core, and two editions of one rule
8
+ // diverge silently. So the book now comes from the CORE: watch - quotes.watch, snapshot - quotes.snapshot, and
9
+ // the shape is translated by the bridge. NO selection rule is left here: the core decides. Core silence or
10
+ // refusal gives an EMPTY book.
11
+ // THE SNAPSHOT SHAPE, in short: no STEPS (one RANGE, one price); one quote KIND - quoteKind is gone, the only
12
+ // firm quote is the order one; rate: 0 means "direction disabled" with a why, and zero is NEVER a price; the
13
+ // network is part of the market identity and is checked against ours; seq and at travel with the quote for
14
+ // freshness; the SIZE is in units of the ASSET (the first pair element).
7
15
  //
8
- // ПОЧЕМУ ЧЕРЕЗ ЯДРО, А НЕ СВОИМ ЗАПРОСОМ. Раньше этот модуль САМ ходил в /api/rate и САМ разбирал книгу:
9
- // правила чтения (ноль = "направления нет", просрочка по СОБСТВЕННОЙ метке котировки, строка, противоречащая
10
- // себе) жили здесь, а рядом - в ядре (sdk/src/quotes.mjs). Две редакции одних правил расходятся молча
11
- // (док 41/54), поэтому книга теперь берётся у ЯДРА: опрос - quotes.watch, снимок - quotes.snapshot, а
12
- // ПЕРЕВОД ФОРМЫ делает склейник (www/js/sdk/bridge.js). Здесь не осталось НИ ОДНОГО правила отбора: что
13
- // предложение, что отказ, что лучший - решает ядро. Молчание или отказ ядра даёт ПУСТУЮ книгу: прежняя
14
- // (движковая) книга на экран не подставляется.
15
16
  //
16
- // ЧТО ЗНАЧИТ ЭТА ФОРМА СНИМКА (полный текст формы - .hermes/docs/41-quote-protocol-v4.md; действующие
17
- // правила приёма - .hermes/docs/54-quote-protocol-v5.md), если коротко:
18
- // - СТУПЕНЕЙ НЕТ. min/max/step/ttlMs описывают ДИАПАЗОН целиком, а цена одна на весь диапазон: объём либо
19
- // попадает в диапазон провайдера, либо он за него не берётся;
20
- // - ВИД КОТИРОВКИ ОДИН. quoteKind (live/fixed) ушёл: твёрдая котировка ровно одна - ордерная;
21
- // - rate: 0 ЗНАЧИТ "НАПРАВЛЕНИЕ ОТКЛЮЧЕНО" и рядом стоит why. Это ЕДИНСТВЕННЫЙ сигнал отсутствия
22
- // предложения, поэтому ноль НИКОГДА не становится ценой и НИКОГДА не делится;
23
- // - СЕТЬ - ЧАСТЬ ЛИЧНОСТИ РЫНКА: assetNetwork/currencyNetwork сверяются со своей сетью (quoteNetworksFit);
24
- // - seq (номер запроса) и at (метка времени) едут вместе с котировкой: по ним читается свежесть;
25
- // - ОБЪЁМ МЕРЯЕТСЯ В ЕДИНИЦАХ ASSET ПАРЫ (первого элемента): у XMR/<токен> это XMR, у <токен>/XMR - токен.
26
17
  import { API, DEFAULT_CHAIN } from "./config.js";
27
18
 
28
- // КОТИРОВКА ПОД КОНКРЕТНЫЙ ОРДЕР (orderQuote). Запрос идёт ЧЕРЕЗ НАШ БЭКЕНД, а не напрямую в ноду
29
- // провайдера: прямой запрос потребовал бы от ноды заголовков CORS, то есть открытой наружу ноды. Наш
30
- // бэкенд маршрутизирует запрос и заодно проверяет подпись провайдера по списку разрешённых ключей
31
- // (док 21, D5.1; док 22, версия 4). Тело котировки бэкенд не хранит и не логирует.
19
+ // QUOTE FOR A SPECIFIC ORDER (orderQuote). The request goes THROUGH OUR BACKEND, not straight to the provider
20
+ // node: a direct request would require CORS headers, i.e. a node exposed to the internet. Our backend routes
21
+ // the request and also checks the provider signature against an allowlist of keys.
32
22
  export async function requestOrderQuote({ providerId, order }) {
33
- if (!providerId) throw new Error("не выбран провайдер: запрашивать котировку под ордер не у кого");
34
- // ПУТЬ БЕЗ ВТОРОГО "api": apiBase() уже возвращает "/api", и лишний префикс давал /api/api/order-quote -
35
- // 404 на каждом запросе. Снаружи это выглядело как «котировка под ордер не запрашивается вовсе», хотя
36
- // запрос уходил и получал отказ: сбивало с толку и разбор, и поиск причины.
23
+ if (!providerId) throw new Error("no provider selected: there is no one to ask for an order quote");
24
+ // A PATH WITHOUT A SECOND "api": apiBase() already returns "/api", and a stray prefix gave /api/api/order-quote -
25
+ // a 404 on every request.
37
26
  const res = await fetch(apiBase() + "/order-quote", {
38
27
  method: "POST",
39
28
  headers: { "content-type": "application/json" },
@@ -43,70 +32,65 @@ export async function requestOrderQuote({ providerId, order }) {
43
32
  try { body = await res.json(); } catch { body = null; }
44
33
  if (!res.ok || !body || body.ok !== true || !body.quote) {
45
34
  const why = (body && (body.error || body.why)) || ("HTTP " + res.status);
46
- throw new Error("провайдер не дал котировку под ордер: " + why);
35
+ throw new Error("the provider gave no order quote: " + why);
47
36
  }
48
- // ПОДПИСЬ ПРОВЕРЯЕТ БЭКЕНД, НО РЕЗУЛЬТАТ ПРИХОДИТ ПОЛЕМ, А НЕ МОЛЧАНИЕМ. Непроверенная подпись -
49
- // это не «наверное всё хорошо»: котировка под ордер несёт обязательство по ключам, и принимать её без
50
- // подтверждения нельзя. Отсутствие флага тоже считаем непроверенной: обещать больше, чем сделано, нельзя.
37
+ // THE BACKEND CHECKS THE SIGNATURE, BUT THE RESULT COMES AS A FIELD, NOT BY SILENCE. An unverified signature
38
+ // is not "probably fine": an order quote commits to keys. A missing flag also counts as unverified.
51
39
  if (body.signatureVerified !== true) {
52
- throw new Error("подпись провайдера не подтверждена: " + String(body.signatureWhy || "бэкенд не проверил"));
40
+ throw new Error("provider signature not confirmed: " + String(body.signatureWhy || "the backend did not verify"));
53
41
  }
54
42
  return body.quote;
55
43
  }
56
44
 
57
- // ПРОВЕРКА КОТИРОВКИ ПОД ОРДЕР - то, ради чего она вообще запрашивается. Проверяющий код тот же, что и
58
- // у своей стороны: доказательство в контексте ордера, и доказанная точка ed25519 обязана совпасть с той,
59
- // которую мы отдадим в контракт. Второе без первого бессмысленно, первое без второго не защищает.
45
+ // ORDER-QUOTE CHECK - the reason it is requested at all. The verifier code is the same as our own side: a proof
46
+ // in the order context, and the proven ed25519 point must match the one we will send to the contract.
60
47
  export function checkOrderQuoteShape(quote) {
61
- // ПОЛОВИНА ПРОСМОТРА В СПИСКЕ ОБЯЗАТЕЛЬНЫХ: без неё по котировке не собрать адрес Monero (док 21, D14.3).
48
+ // THE VIEW HALF IS MANDATORY: without it the Monero address cannot be built from the quote.
62
49
  const missing = ["providerId", "amount", "claimer", "edPointClaimer", "commitHalfClaimer", "viewHalfClaimer", "proof", "context", "expiresAt"]
63
50
  .filter((f) => quote[f] === undefined || quote[f] === null || quote[f] === "");
64
- if (missing.length) throw new Error("в котировке под ордер не хватает полей: " + missing.join(", "));
65
- // ЕДИНСТВЕННЫЙ ТВЁРДЫЙ ВИД - ОРДЕРНЫЙ. В версии 4 второго вида (fixed) в продукте нет, поэтому проверка
66
- // не "какой это вид", а "это вообще котировка под ордер или нет": рассылочная котировка сюда не годится.
67
- if (quote.quoteKind !== "orderQuote") throw new Error("пришла не котировка под ордер, а " + String(quote.quoteKind));
68
- if (Number(quote.expiresAt) <= Date.now()) throw new Error("котировка под ордер просрочена");
51
+ if (missing.length) throw new Error("the order quote is missing fields: " + missing.join(", "));
52
+ // THE ONLY FIRM KIND IS THE ORDER ONE: a broadcast quote does not fit here.
53
+ if (quote.quoteKind !== "orderQuote") throw new Error("not an order quote, but " + String(quote.quoteKind));
54
+ if (Number(quote.expiresAt) <= Date.now()) throw new Error("the order quote has expired");
69
55
  }
70
56
 
71
57
  let baseOverride = null;
72
58
  const apiBase = () => baseOverride || API.base;
73
59
 
74
- // ЧТО СПРАШИВАЕМ У ЯДРА. Сторона и РАЗМЕР - в единицах ASSET пары (см. формулировку выше); сеть и токен
75
- // нужны ядру, чтобы выбрать СВОЙ экземпляр (у каждой сети свой опрос). Своих чисел здесь не появляется:
76
- // размер приходит от экрана, который выровнял его по сетке провайдера.
60
+ // WHAT WE ASK THE CORE. The side and SIZE are in units of the ASSET; the network and token let the core pick ITS
61
+ // instance. No numbers appear here: the size comes from the screen, aligned to the provider grid.
77
62
  let query = { size: 1, side: "buy", chain: DEFAULT_CHAIN, token: null };
78
63
  export function setRfqQuery(next) {
79
64
  if (!next) return;
80
65
  if (next.side === "buy" || next.side === "sell") query.side = next.side;
81
- // ВИДА КОТИРОВКИ В ЗАПРОСЕ БОЛЬШЕ НЕТ: второго вида не существует, и поле mode ушло вместе с ним.
66
+ // NO QUOTE KIND IN THE REQUEST anymore: the second kind does not exist.
82
67
  if (next.size !== null && next.size !== undefined && Number(next.size) > 0) query.size = Number(next.size);
83
- else query.size = 1; // сумма не введена - спрашиваем единицу ASSET, а не "ничего"
68
+ else query.size = 1; // no amount entered - ask for one ASSET unit, not "nothing"
84
69
  if (typeof next.chain === "string" && next.chain) query.chain = next.chain;
85
70
  if (typeof next.token === "string" && next.token) query.token = next.token;
86
71
  }
87
72
 
88
- // Текущий запрос: экрану нужно знать, под какой объём и сторону спрашивали - иначе он не отличит "провайдер
89
- // молчит" от "провайдер не берётся за такой объём".
73
+ // The current request: the screen needs to know the side and size asked for, else it cannot tell "the provider is
74
+ // silent" from "the provider does not take this size".
90
75
  export function rfqQuery() {
91
76
  return { ...query };
92
77
  }
93
78
 
94
- // СНИМОК, КОТОРЫЙ ЧИТАЮТ ЭКРАНЫ. Форма та же, что была: { ok, error, data, at }. В data - references
95
- // (справочный курс) и best, а предложения и отказы отдаёт rfqOffers(). Ядро не опрошено ни разу или
96
- // отказало - данных нет, и это видно по ok. Прежние числа как "живые" не подставляются.
79
+ // THE SNAPSHOT THE SCREENS READ: { ok, error, data, at }. In data - references and best, while offers and
80
+ // refusals come from rfqOffers(). If the core was never polled or refused, there is no data, shown by ok.
81
+ // Old numbers are not pasted as "live".
97
82
  const snapshot = { ok: false, error: null, data: null, at: 0, fetching: false };
98
83
  export function rfqSnapshot() {
99
84
  return snapshot;
100
85
  }
101
86
 
102
- // Пара, которую пользователь ВИДИТ: он покупает XMR, значит смотрит на пары XMR/<актив>; продаёт - на обратные.
87
+ // The pair the user SEES: buying XMR means the pairs XMR/<asset>; selling means the reverse.
103
88
  export function pairFacingUser(side = query.side, asset = "ETH") {
104
89
  return side === "sell" ? asset.toUpperCase() + "/XMR" : "XMR/" + asset.toUpperCase();
105
90
  }
106
91
 
107
- // СТОРОНЫ ПАРЫ ЧИТАЮТСЯ ПО МЕСТУ (док 41, §1): первый элемент - ASSET (в нём же меряется объём), второй -
108
- // CURRENCY (то, чем платят). Отдельные функции, потому что это правило конвенции, а не разбор строки:
109
- // "XMR/ETH" значит "сколько ETH за 1 XMR" и ничего другого.
92
+ // PAIR SIDES ARE READ BY POSITION: the first element is the ASSET (the size is measured in it), the second is
93
+ // the CURRENCY (what is paid). Separate functions, because this is a convention.
110
94
  export function pairAsset(pair) {
111
95
  return String(pair || "").split("/")[0].toUpperCase();
112
96
  }
@@ -114,10 +98,9 @@ export function pairCurrency(pair) {
114
98
  return String(pair || "").split("/")[1] ? String(pair).split("/")[1].toUpperCase() : "";
115
99
  }
116
100
 
117
- // ПРЕДЛОЖЕНИЯ И ОТКАЗЫ - ИЗ СНИМКА ЯДРА, БЕЗ ВТОРОГО ОТБОРА. Ядро уже разделило строки книги на
118
- // предложения и отказы (sdk/src/quotes.mjs, readBook: ноль/противоречие/просрочка) и назвало лучшего.
119
- // Здесь они ТОЛЬКО переносятся: своего отсева, своей сортировки и своих порогов не появляется. Пустой
120
- // снимок - пустая книга; прежняя книга на экране не остаётся.
101
+ // OFFERS AND REFUSALS - FROM THE CORE SNAPSHOT, NO SECOND SELECTION. The core already split the rows into
102
+ // offers and refusals and named the best. Here they are only transferred: no own filtering, sorting or
103
+ // thresholds. An empty snapshot is an empty book.
121
104
  export function rfqOffers() {
122
105
  const data = snapshot.ok && snapshot.data ? snapshot.data : null;
123
106
  if (!data) return { offers: [], refused: [], seq: null, at: null };
@@ -129,10 +112,10 @@ export function rfqOffers() {
129
112
  };
130
113
  }
131
114
 
132
- // СВЕРКА СЕТИ КОТИРОВКИ СО СВОЕЙ (док 41, §3). Сеть - часть личности рынка, а не украшение: arbitrum и
133
- // arbitrum-sepolia - это ОДНО СЛОВО ДЛЯ РАЗНЫХ ДЕНЕГ, и кошелёк между ними монеты не переводит. Сторона,
134
- // где стоит XMR, обязана говорить на языке Monero, вторая - на языке реестра сетей EVM (его id).
135
- // Возвращаем вердикт, а не throw: отказ одной котировки не закрывает рынок (см. market.js).
115
+ // CHECKING THE QUOTE NETWORK AGAINST OURS. The network is part of the market identity: arbitrum and
116
+ // arbitrum-sepolia are ONE WORD FOR DIFFERENT MONEY, and a wallet does not move coins between them. The side
117
+ // holding XMR must speak Monero, the other the EVM registry language (its id). We return a verdict, not a
118
+ // throw: one quote refusal does not close the market.
136
119
  export function quoteNetworksFit(quote, { monero, evm }) {
137
120
  const xmrOnAsset = quote.asset === "XMR";
138
121
  const xmrOnCurrency = quote.currency === "XMR";
@@ -147,15 +130,14 @@ export function quoteNetworksFit(quote, { monero, evm }) {
147
130
  if (evmNet && evmNet !== evm) {
148
131
  return { ok: false, stated: true, why: quote.pair + ": " + evmSide + " is " + evmNet + ", but this page settles on " + evm };
149
132
  }
150
- // НЕ НАЗВАНА - НЕ ЗНАЧИТ "НАША". Сверять нечего, и это надо сказать вслух (в лог), но отказом за это не
151
- // закрываем рынок: правило §3 ловит СЕТЬ, НАЗВАННУЮ НЕВЕРНО, а не поле, которого котировка ещё не несёт.
133
+ // NOT NAMED - DOES NOT MEAN "OURS". There is nothing to compare, and that is logged, but we do not close the
134
+ // market for it: the rule catches a network NAMED WRONG, not a missing field.
152
135
  return { ok: true, stated: quote.networkStated, moneroNet: moneroNet || null, evmNet: evmNet || null, why: null };
153
136
  }
154
137
 
155
- // ОПРОС ИДЁТ ЧЕРЕЗ ЯДРО. Склейник (www/js/sdk/bridge.js) отдаёт ядру то, что нужно ему снаружи (сеть, размер,
156
- // направление), и возвращает ПЕРЕВЕДЁННЫЙ снимок. Ядро грузится ДИНАМИЧЕСКИ: этот модуль читают и без
157
- // браузера (проверки рынка), и тогда книга просто не запрашивается - динамический импорт в Node не тянет
158
- // страничные модули. Отказ загрузки ядра - ЭТО ОТКАЗ, а не повод взять книгу где-то ещё.
138
+ // POLLING GOES THROUGH THE CORE. The bridge hands the core what it needs (network, size, direction) and returns
139
+ // the TRANSLATED snapshot. The core is loaded DYNAMICALLY: this module is also read without a browser, and then
140
+ // the book is simply not requested. A load failure IS a failure, not a reason to take the book elsewhere.
159
141
  export async function refreshRfq() {
160
142
  if (snapshot.fetching) return snapshot;
161
143
  snapshot.fetching = true;
@@ -168,8 +150,8 @@ export async function refreshRfq() {
168
150
  snapshot.data = next.data;
169
151
  snapshot.at = next.at || Date.now();
170
152
  } catch (error) {
171
- // ОТКАЗ НЕ ПОДМЕНЯЕМ ПРОШЛЫМИ ЧИСЛАМИ: показать устаревшую цену как живую - хуже, чем сказать, что
172
- // связи нет. Данных не остаётся вовсе: книги, которую можно было бы показать, больше нет.
153
+ // A REFUSAL IS NOT REPLACED BY OLD NUMBERS: showing a stale price as live is worse than saying there is no
154
+ // connection. No data is left at all.
173
155
  snapshot.ok = false;
174
156
  snapshot.error = String((error && error.message) || error);
175
157
  snapshot.data = null;
@@ -182,9 +164,9 @@ export async function refreshRfq() {
182
164
 
183
165
  let timer = null;
184
166
 
185
- // ОПРАШИВАЕМ ВСЕГДА, как и раньше. Опрос здесь не для того, чтобы "поймать новую цену": он подтверждает,
186
- // что провайдер на месте. Сам опрос держит ЯДРО (на время подписки), а этот такт заставляет книгу
187
- // обновляться вместе с экраном. Молчание ноды видно как отсутствие свежей метки, а не как "цена та же".
167
+ // ALWAYS POLL. The poll is not to "catch a new price": it confirms the provider is there. The core holds the
168
+ // poll for the subscription; this tick keeps the book updating with the screen. Node silence shows as a missing
169
+ // fresh stamp, not as "the price is the same".
188
170
  export function startRfqPolling(chain = null, ms = API.pollMs) {
189
171
  if (typeof chain === "string" && chain) query.chain = chain;
190
172
  if (timer) return;
@@ -1,29 +1,28 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/core/sellWindow.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 (issue #111): ОДНО ПРАВИЛО НА ДВЕ СТОРОНЫ, А НЕ ДВА ЧИСЛА.
5
+ // XMR SELL WINDOW: ONE RULE FOR TWO SIDES, NOT TWO NUMBERS.
7
6
  //
8
- // ЗАЧЕМ ОТДЕЛЬНЫЙ МОДУЛЬ. У окна продажи XMR (человек отдаёт XMR, получает натив сети) два числа, и они
9
- // разной природы:
10
- // * ЖЁСТКОЕ (hard) - срок отметки готовности ИЗ КОНТРАКТА. Позже него отметку не примет никто, и он входит
11
- // в ПРИВЯЗКУ ордера (termsHash), поэтому после выдачи билета не меняется. В контракт ВСЕГДА идёт максимум:
12
- // 10 подтверждений Monero - это примерно 1200 с, и меньше контрактного окна быть не может, иначе отметить
13
- // готовность было бы нечем (отметку контракт после readyBy запрещает, XMR заперлись бы для обоих).
14
- // * МЯГКОЕ (soft) - ОБЕЩАНИЕ НА ЭКРАНЕ: сколько провайдер держит цену, пока человек отправляет XMR.
15
- // "Десять минут" владельца и партнёра.
16
- // ПРАВИЛО ПРОДЛЕНИЯ ОДНО. Пока в пуле НЕ видно ПОЛНОЙ ожидаемой суммы, обещание продлевается до контрактного
17
- // максимума: человек мог отправить, и ему нужен первый блок. Как только полная сумма видна в пуле, окно
18
- // мягкое - обещание сдержано, и продлевать нечего.
7
+ // WHY A SEPARATE MODULE. The XMR sell window (a person gives XMR, receives the network native coin) has two
8
+ // numbers, and they are of different nature:
9
+ // * HARD - the readiness-mark deadline FROM THE CONTRACT. Past it nobody accepts the mark, and it goes into
10
+ // the order BINDING (termsHash), so it does not change after the ticket is issued. The contract ALWAYS
11
+ // gets the maximum: 10 Monero confirmations is about 1200 s, and it cannot be less than the contract
12
+ // window, else there would be nothing to mark readiness with (the contract forbids the mark after readyBy,
13
+ // and the XMR would be locked for both).
14
+ // * SOFT - THE ON-SCREEN PROMISE: how long the provider holds the price while the person sends XMR.
15
+ // The owner's and partner's "ten minutes".
16
+ // THE EXTENSION RULE IS ONE. While the pool does NOT show the FULL expected amount, the promise is extended to
17
+ // the contract maximum: the person may have sent, and needs the first block. Once the full amount is visible,
19
18
  //
20
- // ПОЧЕМУ ФУНКЦИЯ, А НЕ ДВА ЧИСЛА В ДВУХ МЕСТАХ. Это правило решает НОДА (rfq/reverseArrival.mjs - отметка,
21
- // тревога, ранний возврат) и показывает СТРАНИЦА (www/js/core/swap.js - отсчёт на экране). Вторая редакция
22
- // разошлась бы с первой молча: человек видел бы одно окно, а нода жила бы по другому. Направление импорта
23
- // rfq -> www в проекте уже принято (образец: rfq/reverseGas.mjs импортирует ../www/js/evm/claimGas.js).
19
+ // WHY A FUNCTION, NOT TWO NUMBERS IN TWO PLACES. This rule is decided by the NODE (rfq/reverseArrival.mjs -
20
+ // the mark, the alarm, the early refund) and shown by the PAGE (www/js/core/swap.js - the on-screen countdown).
21
+ // A second edition would diverge from the first silently: the person would see one window while the node lived by
22
+ // another. The rfq -> www import direction is already accepted (precedent: rfq/reverseGas.mjs imports ../www/js/evm/claimGas.js).
24
23
  export const SELL_WINDOW = {
25
- softSec: 600, // "десять минут": обещание провайдера на экране
26
- hardSec: 1200, // контрактное окно: 10 подтверждений Monero, максимум, попадающий в termsHash
24
+ softSec: 600, // "ten minutes": the provider's on-screen promise
25
+ hardSec: 1200, // contract window: 10 Monero confirmations, the maximum that goes into termsHash
27
26
  };
28
27
 
29
28
  const positiveSec = (v, dflt) => {
@@ -31,10 +30,10 @@ const positiveSec = (v, dflt) => {
31
30
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : dflt;
32
31
  };
33
32
 
34
- // СКОЛЬКО СЕКУНД ИДЁТ ОКНО ПРОДАЖИ при названном состоянии пула. poolSeen - в пуле видна ПОЛНАЯ ожидаемая
35
- // сумма, и только она (не "что-то видно" и не недоплата: сделка повисла бы на чужой копейке - см. poolCoverage
36
- // в rfq/reverseArrival.mjs). Контрактное окно - ПОТОЛОК: мягкое обещание не может быть длиннее контрактного,
37
- // иначе мы обещали бы то, чего цепь не даст.
33
+ // HOW MANY SECONDS THE SELL WINDOW LASTS given the named pool state. poolSeen - the FULL expected amount is
34
+ // visible in the pool, and only it (not "something is visible" and not an underpayment: the swap would hang on
35
+ // someone else's penny - see poolCoverage in rfq/reverseArrival.mjs). The contract window is a CEILING: the soft
36
+ // promise cannot be longer than the contract window, else we would promise what the chain will not give.
38
37
  export function sellWindowSec({ poolSeen, soft = SELL_WINDOW.softSec, hard = SELL_WINDOW.hardSec } = {}) {
39
38
  const s = positiveSec(soft, SELL_WINDOW.softSec);
40
39
  const h = positiveSec(hard, SELL_WINDOW.hardSec);
@@ -1,33 +1,32 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/core/store.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
+ // Demo state storage.
7
6
  //
8
- // Зачем оно вообще: литпепер §5.1.7 - «вкладку можно закрыть, браузер восстановит состояние». Состояние сделки
9
- // выводится из сохранённых таймстемпов и событий, поэтому вкладку действительно можно закрыть.
7
+ // Why it exists at all: litepaper §5.1.7 - "the tab can be closed, the browser will restore the state". The swap
8
+ // state is derived from saved timestamps and events, so the tab really can be closed.
10
9
  //
11
- // ХРАНИЛИЩЕ ПРИХОДИТ СНАРУЖИ. Раньше здесь стоял `localStorage` прямо в коде, и это делало модуль непригодным
12
- // ни в Node (проверки репозитория), ни в SDK (пакет обещает «без DOM»). Теперь хранилище - адаптер с тремя
13
- // действиями (get/set/remove), а по умолчанию берётся localStorage, ЕСЛИ он есть в среде, иначе память
14
- // процесса: молча не сохранять нельзя - сделка после перезагрузки потерялась бы.
10
+ // STORAGE COMES FROM OUTSIDE. Before, `localStorage` stood right in the code, and that made the module unusable
11
+ // both in Node (the repository checks) and in the SDK (the package promises "no DOM"). Now storage is an adapter
12
+ // with three actions (get/set/remove), and by default localStorage is taken IF it exists in the environment,
13
+ // otherwise process memory: silently not saving is not allowed - the swap would be lost after a reload.
15
14
 
16
15
  import { APP, DEMO } from "./config.js";
17
16
 
18
17
  const EMPTY = {
19
18
  version: 1,
20
- // payToken - выбранный актив оплаты. Хранится рядом с сетью: иначе форма при перезагрузке возвращала
21
- // нативный токен и выбор пользователя терялся.
19
+ // payToken - the chosen payment asset. Stored next to the network: otherwise the form on reload returned
20
+ // the native token and the user's choice was lost.
22
21
  settings: { speed: DEMO.defaultSpeed, scenario: DEMO.defaultScenario, chainMode: DEMO.defaultChainMode, chain: DEMO.defaultChain, payToken: "" },
23
22
  wallet: { connected: false, address: null, balances: {} },
24
23
  swaps: {},
25
24
  activeSwapId: null,
26
25
  };
27
26
 
28
- // АДАПТЕР ПО УМОЛЧАНИЮ: web storage, если он есть, иначе память. Проверка среды - через typeof, а не
29
- // try/catch вокруг обращения: обращение к несуществующему имени в Node - это ReferenceError, и ловить его
30
- // как «нет хранилища» значит прятать опечатки.
27
+ // DEFAULT ADAPTER: web storage, if present, otherwise memory. The environment is probed via typeof, not
28
+ // try/catch around an access: accessing a non-existent name in Node is a ReferenceError, and catching it
29
+ // as "no storage" would hide typos.
31
30
  export function memoryStorage(map = new Map()) {
32
31
  return {
33
32
  get: (key) => (map.has(key) ? map.get(key) : null),
@@ -48,15 +47,15 @@ const webStorage = (name) => {
48
47
 
49
48
  let adapter = webStorage("localStorage") || memoryStorage();
50
49
 
51
- /** Подменить хранилище. Проверяется форма, а не происхождение: годится и localStorage, и память, и SDK. */
50
+ /** Swap the storage. The shape is checked, not the origin: localStorage, memory and the SDK all qualify. */
52
51
  export function setStorage(next) {
53
52
  const ok = next && typeof next.get === "function" && typeof next.set === "function" && typeof next.remove === "function";
54
- if (!ok) throw new Error("хранилище должно уметь get/set/remove");
53
+ if (!ok) throw new Error("storage must support get/set/remove");
55
54
  adapter = next;
56
55
  return adapter;
57
56
  }
58
57
 
59
- /** Что используется сейчас: это видно снаружи, и по этому признаку проверки отличают память от браузера. */
58
+ /** What is used right now: this is visible from outside, and by this the checks tell memory from a browser. */
60
59
  export const storage = () => adapter;
61
60
 
62
61
  export function loadState() {
@@ -70,10 +69,10 @@ export function loadState() {
70
69
  }
71
70
  }
72
71
 
73
- // BigInt В JSON НЕ СЕРИАЛИЗУЕТСЯ - а половины ключей это BigInt. JSON.stringify на них ПАДАЕТ
74
- // ("Do not know how to serialize a BigInt", bigintSafe), и падение молчаливое на вид: запись сделки просто не сохраняется,
75
- // файл восстановления не выпускается, а экран показывает адрес, которого на цепи нет.
76
- // Приводим BigInt к шестнадцатеричной строке: это тот же смысл, но сериализуемый, и секрет не теряется.
72
+ // BigInt IS NOT SERIALISED IN JSON - and the key halves are BigInt. JSON.stringify FAILS on them
73
+ // ("Do not know how to serialize a BigInt", bigintSafe), and the failure looks silent: the swap record is
74
+ // simply not saved, the recovery file is not issued, and the screen shows an address that is not on chain.
75
+ // We turn BigInt into a hex string: the same meaning, but serialisable, and the secret is not lost.
77
76
  const bigintSafe = (key, value) => (typeof value === "bigint" ? "0x" + value.toString(16) : value);
78
77
  export function saveState(state) {
79
78
  try {