@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,55 +1,51 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/core/preSignGuards.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
- // ЗАЩИТЫ ПЕРЕД ПОДПИСЬЮ: НЕДОПУСТИМАЯ СДЕЛКА НЕ НАЧИНАЕТСЯ (док 45).
5
+ // PRE-SIGNING GUARDS: AN INADMISSIBLE DEAL DOES NOT START.
7
6
  //
8
- // ЗАЧЕМ ОТДЕЛЬНЫЙ МОДУЛЬ. До сих пор защиты жили врозь: одна - в реестре маршрутов (evm/dex.js:
9
- // покрытие суммы и цена DEX-ноги), прочие - в виде проверок, разбросанных по форме и по потоку ордера. От
10
- // этого заводится ровно тот дефект, из-за которого файл и написан: правило есть, а подпись не
11
- // останавливается, потому что экран его не спросил. Здесь правила сведены в ОДНО место, у них ОДНА
12
- // форма ответа, и сводка (signingGateVerdict) отвечает на единственный вопрос, который решает дело:
13
- // начинать подпись или нет.
7
+ // WHY A SEPARATE MODULE. The guards used to live apart: one in the route registry (the DEX leg's coverage and
8
+ // price), the rest scattered across the form and the order flow. That breeds exactly the defect this file exists
9
+ // for: the rule is there but the signature does not stop, because the screen never asked. Here the rules sit in
10
+ // ONE place with ONE answer shape, and the summary (signingGateVerdict) answers the only question that matters:
11
+ // start the signature or not.
14
12
  //
15
- // ЧТО ЗНАЧИТ ОТВЕТ. Каждый приговор несёт три поля, и они разные по смыслу:
16
- // blocks - подписывать НЕЛЬЗЯ (это и есть защита; вызывающий ОБЯЗАН считаться с этим полем);
17
- // checked - удалось ли вообще проверить (true/false);
18
- // reason - слова, и в них ЧИСЛА: человеку нужно не "нельзя", а "нельзя, потому что вот столько".
19
- // Отличать checked от blocks обязательно, и вот почему на двух примерах из этого файла:
13
+ // WHAT AN ANSWER MEANS. Every verdict carries three fields of different meaning:
14
+ // blocks - signing is NOT ALLOWED (the guard itself; the caller MUST honour this field);
15
+ // checked - whether the check could be performed at all (true/false);
16
+ // reason - words, and they carry NUMBERS: a person needs not "no" but "no, because this much".
17
+ // Telling checked from blocks is mandatory; two examples from this file show why:
20
18
  //
21
- // * НЕПРОВЕРЕННЫЙ БАЛАНС БЛОКИРУЕТ. Прочитать баланс мы умеем всегда, и "не прочитали" здесь значит
22
- // "не знаем, хватает ли денег на то, что человек собирается подписать". Пропустить это значило бы
23
- // отправить человека подписывать отказ кошелька.
24
- // * НЕИЗМЕРЕННЫЙ ЗАПАС НА ГАЗ НЕ БЛОКИРУЕТ, И ЭТО РЕШЕНИЕ, А НЕ ЗАБЫВЧИВОСТЬ. Отличие от проверки
25
- // цены DEX-ноги принципиальное: негодная цена означает, что сделка НЕВЕРНАЯ, а неизмеренный газ -
26
- // всего лишь "запас неизвестен". Сама сделка от этого не становится недопустимой, а нехватку
27
- // топлива на транзакцию назовёт кошелёк. Поэтому такой случай помечается словами (checked: false,
28
- // kind reserve-unstated) и НЕ запирает подпись.
29
- // * НЕ НАЗВАННЫЙ ПРОВАЙДЕРОМ ДИАПАЗОН ОБЪЁМА - ТАК ЖЕ: помечается, не запирает (решение уже принято
30
- // рынком: "объём не назван - ограничивать нечем", www/js/mock/market.js: coversSize).
19
+ // * AN UNCHECKED BALANCE BLOCKS. Reading a balance always works here, so "could not read" means "we do not
20
+ // know whether there is enough money for what the person is about to sign". Skipping it would send the person
21
+ // to sign a wallet refusal.
22
+ // * AN UNMEASURED GAS RESERVE DOES NOT BLOCK, AND THAT IS A DECISION, NOT AN OVERSIGHT. Unlike the DEX price
23
+ // check: a bad price means the deal is WRONG, while an unmeasured gas figure only means "the reserve is
24
+ // unknown". The deal does not become inadmissible, and a wallet will name a shortfall anyway. So this case is
25
+ // marked (checked: false, kind reserve-unstated) and does NOT lock the signature.
26
+ // * A SIZE RANGE THE PROVIDER DID NOT NAME - SAME THING: marked, not blocking (the market already decided:
27
+ // "no size named - nothing to cap", see coversSize in the mock market module).
31
28
  //
32
- // И ЕЩЁ ОДНО, ЧТО ЗДЕСЬ ПРИНЦИПИАЛЬНО. Все приговоры - ЧИСТЫЕ функции: ни сети, ни кошелька, ни DOM.
33
- // Поэтому каждую защиту можно прогнать на числах, которые её ЛОМАЮТ, и увидеть, что она краснеет:
34
- // "код есть" не равно "отказ работает" (tools/check-signing-guards.mjs).
29
+ // ONE MORE THING THAT IS FUNDAMENTAL HERE. All verdicts are PURE functions: no network, no wallet, no DOM. So
30
+ // each guard can be run against the numbers that BREAK it and be seen red: "the code is there" is not "the
31
+ // refusal works".
35
32
  //
36
- // ПОРОГИ ПРИХОДЯТ ДАННЫМИ (реестр: SIGNING_GUARDS в core/config.js), а не лежат здесь числами: менять
37
- // их - правка данных, а не логики.
33
+ // THE THRESHOLDS COME IN AS DATA (the registry: SIGNING_GUARDS in core/config.js), not as numbers here: changing
34
+ // them is a data edit, not a logic edit.
38
35
 
39
- // wei из того, что дал вызывающий (BigInt, строка, число). Не разобралось - null, и это НЕ ноль:
40
- // ноль баланса и "баланс неизвестен" - два разных состояния, и путать их нельзя.
36
+ // wei from whatever the caller supplied (BigInt, string, number). Unparsable - null, and that is NOT zero: a zero
37
+ // balance and an unknown balance are two different states and must not be confused.
41
38
  const wei = (v) => {
42
39
  if (v === null || v === undefined || v === "") return null;
43
40
  try { const n = BigInt(v); return n >= 0n ? n : null; } catch { return null; }
44
41
  };
45
- // КОЛИЧЕСТВО В ЧЕЛОВЕЧЕСКОМ ВИДЕ - ОТДЕЛЬНО ОТ human. Размер объёма и границы диапазона приходят УЖЕ
46
- // человеческими числами (единицы ASSET пары, док 41 §5), а не wei, и делить их на 10^decimals нечего:
47
- // общая функция дала бы в отказе числа, отличающиеся от настоящих в 10^14 раз - то есть уверенный, но
48
- // неверный ответ ("минимум 0.00001 XMR" вместо "0.1 XMR"). Поэтому формат свой.
42
+ // A QUANTITY IN HUMAN FORM - SEPARATE FROM wei. The swap size and the range bounds arrive ALREADY human (ASSET
43
+ // units of the pair), not wei, so there is nothing to divide by 10^decimals: a shared function would put numbers
44
+ // into the refusal that differ from the real ones by a factor of 10^14 - a confident but wrong answer.
49
45
  const qty = (v) => String(Number(Number(v).toFixed(4)));
50
46
  const finite = (v) => (v === null || v === undefined || v === "" ? null : (Number.isFinite(Number(v)) ? Number(v) : null));
51
- // Число в человеческом виде для слов: 6 знаков после запятой хватает и стейблу, и нативу, а полный
52
- // wei в тексте человек не читает.
47
+ // A number in human form for the words: six decimals suit both a stable and a native, and nobody reads full wei
48
+ // in text.
53
49
  const human = (v, decimals = 18) => {
54
50
  const n = Number(v) / 10 ** Number(decimals);
55
51
  if (!Number.isFinite(n)) return String(v);
@@ -57,11 +53,11 @@ const human = (v, decimals = 18) => {
57
53
  };
58
54
 
59
55
  // ---------------------------------------------------------------------------
60
- // 1) СУММА ПРОТИВ ДИАПАЗОНА КОТИРОВКИ. Диапазон называет провайдер (min/max/step), и за его границы
61
- // он не берётся: размер вне диапазона - это НЕ "попробуем", а отказ, и он должен случиться ДО подписи,
62
- // а не у провайдера после неё.
63
- // ЕДИНИЦА - ЧАСТЬ ПРОВЕРКИ, А НЕ УКРАШЕНИЕ (док 41, §5): объём и границы обязаны быть в ОДНОЙ единице
64
- // (ASSET пары), иначе сравниваются разные величины, и "в диапазоне" значит ничего.
56
+ // 1) SIZE AGAINST THE QUOTE RANGE. The range is the provider's (min/max/step) and it will not take a size outside
57
+ // it: a size out of range is a REFUSAL, not a "let us try", and it must happen BEFORE the signature, not at the
58
+ // provider after it.
59
+ // THE UNIT IS PART OF THE CHECK, NOT DECORATION: the size and the bounds must be in ONE unit (the ASSET of the
60
+ // pair), else different quantities are compared and "in range" means nothing.
65
61
  // ---------------------------------------------------------------------------
66
62
  export function rangeVerdict({ size = null, unit = null, min = null, max = null, step = null, quoteUnit = null } = {}) {
67
63
  const u = String(unit || quoteUnit || "").toUpperCase();
@@ -83,7 +79,7 @@ export function rangeVerdict({ size = null, unit = null, min = null, max = null,
83
79
  }
84
80
  const lo = finite(min), hi = finite(max), st = finite(step);
85
81
  if (lo === null && hi === null) {
86
- // Провайдер не назвал границ - это его слово, а не наша ошибка: помечаем и не запираем.
82
+ // The provider named no bounds - that is its word, not our error: mark it and do not block.
87
83
  return {
88
84
  ok: true, checked: false, blocks: false, kind: "range-unstated",
89
85
  reason: "the provider did not state a volume range (min/max), so the size could not be checked against it",
@@ -100,8 +96,8 @@ export function rangeVerdict({ size = null, unit = null, min = null, max = null,
100
96
  reason: `the order size ${qty(s)} ${u} is above the provider maximum ${qty(hi)} ${u} - over by ${qty(s - hi)} ${u}: the provider does not take this size` };
101
97
  }
102
98
  if (st !== null && st > 0) {
103
- // Шаг считается ОТ min, а не от нуля: min=0.05 при шаге 0.1 не даёт больше ни одного размера,
104
- // и это правило рынка (www/js/mock/market.js: coversSize), а не выдумка этой функции.
99
+ // The step is counted FROM min, not from zero: min=0.05 with step 0.1 yields no further size, and that is the
100
+ // market rule, not an invention of this function.
105
101
  const from = lo !== null ? lo : 0;
106
102
  const grid = (s - from) / st;
107
103
  if (Math.abs(grid - Math.round(grid)) > 1e-6) {
@@ -114,9 +110,9 @@ export function rangeVerdict({ size = null, unit = null, min = null, max = null,
114
110
  }
115
111
 
116
112
  // ---------------------------------------------------------------------------
117
- // 2) БАЛАНС ОПЛАТЫ. Не хватает того, чем платят, - подписывать нечего: транзакция откажет в кошельке,
118
- // и человек узнает о нехватке ПОСЛЕ подписи. Неизвестный баланс (не прочитан) тоже запирает: см.
119
- // рассуждение в шапке файла.
113
+ // 2) PAYMENT BALANCE. Not enough of what pays - there is nothing to sign: the transaction would fail in the
114
+ // wallet and the person would learn of the shortfall AFTER signing. An unknown balance (not read) also locks: see
115
+ // the reasoning in the header.
120
116
  // ---------------------------------------------------------------------------
121
117
  export function payBalanceVerdict({ payBalanceWei = null, payAmountWei = null, symbol = null, decimals = 18 } = {}) {
122
118
  const have = wei(payBalanceWei);
@@ -140,11 +136,11 @@ export function payBalanceVerdict({ payBalanceWei = null, payAmountWei = null, s
140
136
  }
141
137
 
142
138
  // ---------------------------------------------------------------------------
143
- // 3) ЗАПАС НА ГАЗ. Фондирование - транзакция, и она стоит газа в НАТИВНОЙ монете. Поэтому проверяется
144
- // не только сумма ордера, но и сумма ордера ПЛЮС запас: у аккаунта, где лежит ровно amount, транзакция
145
- // не пройдёт. Требуемое здесь считает вызывающий (amount + reserve), а не эта функция: одна величина
146
- // вместо двух веток - меньше места для расхождения.
147
- // Неизмеренный запас НЕ запирает: см. шапку файла (это отличие от проверки цены осознанное).
139
+ // 3) GAS RESERVE. Funding is a transaction and costs gas in the NATIVE coin, so both the order amount and the
140
+ // order amount PLUS the reserve are checked: an account holding exactly `amount` will not pass. The required value
141
+ // is computed by the caller (amount + reserve), not here: one quantity instead of two branches - less room to
142
+ // diverge.
143
+ // An unmeasured reserve does NOT lock: see the header (the difference from the price check is deliberate).
148
144
  // ---------------------------------------------------------------------------
149
145
  export function gasReserveVerdict({ nativeBalanceWei = null, requiredNativeWei = null, gasReserveWei = null, symbol = null, decimals = 18 } = {}) {
150
146
  const need = wei(requiredNativeWei);
@@ -175,12 +171,11 @@ export function gasReserveVerdict({ nativeBalanceWei = null, requiredNativeWei =
175
171
  }
176
172
 
177
173
  // ---------------------------------------------------------------------------
178
- // 4) СВЕЖЕСТЬ КОТИРОВКИ. Правило продукта: просроченную (старше её собственного ttlMs) котировку не
179
- // показывает никто, и подписать по ней нельзя - цена в ней уже не та, что человек видит. Метка
180
- // считается по СВОЕЙ метке котировки (at), а не по времени ответа: иначе возраст "омолаживался" бы
181
- // каждым опросом (док 41, §7).
182
- // Нет метки или нет срока - проверить нечего; это помечается и не запирает (у рынка то же решение:
183
- // "срок котировки провайдер не назвал" на экране, а не закрытие рынка).
174
+ // 4) QUOTE FRESHNESS. Product rule: nobody shows a stale quote (older than its own ttlMs) and it cannot be signed
175
+ // - the price in it is no longer what the person sees. The age is computed from the quote's OWN timestamp (at),
176
+ // not from the response time: otherwise the age would be rejuvenated by every poll.
177
+ // No timestamp or no ttl - nothing to check; that is marked and does not block (the market decided the same:
178
+ // "the provider named no quote ttl" on screen, not a closed market).
184
179
  // ---------------------------------------------------------------------------
185
180
  export function quoteFreshnessVerdict({ at = null, ttlMs = null, expiresAt = null, nowMs = null } = {}) {
186
181
  const now = finite(nowMs) === null ? Date.now() : Number(nowMs);
@@ -210,12 +205,11 @@ export function quoteFreshnessVerdict({ at = null, ttlMs = null, expiresAt = nul
210
205
  }
211
206
 
212
207
  // ---------------------------------------------------------------------------
213
- // 5) СРОКИ ОРДЕРА (readyBy / t1). Срок в прошлом означает ордер, созданный уже закрытым: забрать по
214
- // нему нельзя, деньги повиснут до возврата. Слишком тесные сроки означают то же самое, но незаметно:
215
- // окно отметки готовности короче, чем занимает само подтверждение XMR, а окно расчёта короче, чем нужно
216
- // мейкеру на забор. Слишком далёкий срок - третья крайность (issue #88): вернуть ETH можно только до
217
- // `readyBy` и после `t1`, значит ордер с `t1` в году запирает деньги на год. Отсюда потолок.
218
- // Пороги - данные (SIGNING_GUARDS).
208
+ // 5) ORDER DEADLINES (readyBy / t1). A deadline in the past means an order created already closed: nothing can be
209
+ // claimed, the money hangs until refund. Deadlines that are too tight mean the same, but invisibly: the readiness
210
+ // window is shorter than the XMR confirmation itself, and the settlement window shorter than the maker needs to
211
+ // claim. A deadline too far off is the third extreme: ETH can be returned only before `readyBy` and after `t1`, so
212
+ // an order with `t1` a year away locks the money for a year. Hence the ceiling. The thresholds are data.
219
213
  // ---------------------------------------------------------------------------
220
214
  export function deadlineVerdict({ nowSec = null, readyBy = null, t1 = null, minReadyLeadSec = null, minClaimWindowSec = null, maxDeadlineSec = null } = {}) {
221
215
  const now = finite(nowSec) === null ? Math.floor(Date.now() / 1000) : Number(nowSec);
@@ -241,9 +235,9 @@ export function deadlineVerdict({ nowSec = null, readyBy = null, t1 = null, minR
241
235
  return { ok: false, checked: true, blocks: true, kind: "claim-window-too-short", readyBy: r, t1: c, minClaimWindowSec: claim,
242
236
  reason: `the claim window is ${Math.round(c - r)}s while at least ${claim}s is needed to take the funds before the refund opens` };
243
237
  }
244
- // ПОТОЛОК СРОКА (issue #88). Без него срок можно поставить на год вперёд: обе стороны подпишут ордер,
245
- // ETH уйдут в эскроу - и вернуть их будет нельзя ни до `readyBy`, ни до `t1`, то есть целый год. Потолок
246
- // считается от `t1` (самого дальнего срока): `readyBy` всегда ближе, значит эта проверка покрывает оба.
238
+ // THE DEADLINE CEILING. Without it a deadline can be set a year ahead: both sides sign, the ETH goes into the
239
+ // escrow and cannot be returned before `readyBy` or after `t1` - a whole year. The ceiling is computed from `t1`
240
+ // (the farthest deadline): `readyBy` is always nearer, so this check covers both.
247
241
  const cap = finite(maxDeadlineSec);
248
242
  if (cap !== null && c - now > cap) {
249
243
  return { ok: false, checked: true, blocks: true, kind: "deadline-too-far", readyBy: r, t1: c, nowSec: now, maxDeadlineSec: cap,
@@ -254,12 +248,12 @@ export function deadlineVerdict({ nowSec = null, readyBy = null, t1 = null, minR
254
248
  }
255
249
 
256
250
  // ---------------------------------------------------------------------------
257
- // 6) СОСТОЯНИЕ ЭСКРОУ. Контракт запрещает второе внесение (AlreadyFunded) и не даёт забрать дважды,
258
- // поэтому "подписаться ещё раз" в уже зафондированный, забранный или возвращённый эскроу - это не
259
- // "на всякий случай проверим": вторая подпись либо откатится, либо создаст ВТОРОЙ ордер вместо
260
- // прежнего. Читается вызовом status() по предсказанному адресу - до подписи.
261
- // Отдельно про пустой ответ: по адресу, которого ещё нет, eth_call возвращает пусто - это "ордер
262
- // новый", а не "проверить не удалось". Ошибка вызова - другое дело, и она запирает.
251
+ // 6) ESCROW STATE. The contract forbids a second deposit (AlreadyFunded) and does not allow claiming twice, so
252
+ // "signing again" at an already funded, claimed or refunded escrow is not a "just in case" check: the second
253
+ // signature either reverts or creates a SECOND order instead of the old one. Read by calling status() at the
254
+ // predicted address - before signing.
255
+ // About an empty answer: for an address that does not exist yet eth_call returns empty - that is "a new order",
256
+ // not "the check failed". A call error is another matter, and it locks.
263
257
  // ---------------------------------------------------------------------------
264
258
  export function escrowStateVerdict({ status = null, error = null, address = null } = {}) {
265
259
  const where = address ? " the escrow at " + address : " the escrow";
@@ -288,16 +282,14 @@ export function escrowStateVerdict({ status = null, error = null, address = null
288
282
  }
289
283
 
290
284
  // ---------------------------------------------------------------------------
291
- // 7) СОГЛАСИЕ НА ERC-20. Появится вместе с исполнением DEX-ноги: своп токена требует approve, и
292
- // разрешение на списание - это ОТДЕЛЬНОЕ действие человека, а не следствие подписи свопа. Поэтому
293
- // здесь две проверки, и обе обязательны: разрешение выдано (allowance хватает) И человек на него
294
- // согласился явно. Достаток без согласия и согласие без достатка - разные отказы, и называются они
295
- // по-разному.
296
- // ПОТРЕБИТЕЛЬ У ЭТОЙ ЗАЩИТЫ УЖЕ ЕСТЬ (24 сентября 2026): сборщик исполнения DEX-ноги
297
- // (www/js/evm/dexExec.js, swapAllowanceVerdict) зовёт её, чтобы отличить "разрешения не хватает" от
298
- // "человек на него не согласился" - это РАЗНЫЕ отказы и называются они по-разному. В ПОДПИСЬ формы
299
- // она по-прежнему не включена: форма ещё не свопает (своп - платный этап, док 48), и включать проверку
300
- // в подпись раньше появления самого действия значило бы запирать человека на том, чего он ещё не делает.
285
+ // 7) ERC-20 CONSENT. It will arrive with DEX-leg execution: swapping a token requires approve, and permission to
286
+ // spend is a SEPARATE action of the person, not a consequence of signing the swap. So there are two checks, both
287
+ // mandatory: the allowance is granted (enough) AND the person explicitly consented. Enough without consent and
288
+ // consent without enough are different refusals, named differently.
289
+ // THIS GUARD ALREADY HAS A CONSUMER: the DEX-leg execution builder (swapAllowanceVerdict) calls it to tell
290
+ // "allowance short" from "the person did not consent" - different refusals, named differently. It is still NOT
291
+ // part of the form's signature: the form does not swap yet (the swap is a paid stage), and adding the check to the
292
+ // signature before the action exists would lock a person on something he does not yet do.
301
293
  // ---------------------------------------------------------------------------
302
294
  export function allowanceVerdict({ tokenIsNative = false, allowanceWei = null, amountWei = null, consent = false, symbol = null, decimals = 18 } = {}) {
303
295
  if (tokenIsNative) {
@@ -328,10 +320,10 @@ export function allowanceVerdict({ tokenIsNative = false, allowanceWei = null, a
328
320
  }
329
321
 
330
322
  // ---------------------------------------------------------------------------
331
- // 8) СЕТЬ КОШЕЛЬКА. Балансы читаются через кошелёк, то есть ИЗ ЕГО СЕТИ, и транзакция уйдёт туда же.
332
- // Поэтому несовпадение сети - не предупреждение, а отказ: "страница на одной сети, кошелёк на другой"
333
- // означает, что мы не знаем ни балансов, ни того, куда уйдут деньги. Признак сети кошелька берётся из
334
- // session.js (chainIdOf), признак сети расчёта - из реестра (chainId выбранной сети).
323
+ // 8) WALLET NETWORK. Balances are read through the wallet, i.e. FROM ITS NETWORK, and the transaction goes there
324
+ // too. So a network mismatch is a refusal, not a warning: "page on one network, wallet on another" means we know
325
+ // neither the balances nor where the money goes. The wallet's network marker comes from the session (chainIdOf),
326
+ // the settlement network marker from the registry (chainId of the selected network).
335
327
  // ---------------------------------------------------------------------------
336
328
  export function networkGateVerdict({ walletChainId = null, selectedChainId = null, settlementChainId = null, selectedChainName = null } = {}) {
337
329
  const w = finite(walletChainId), s = finite(selectedChainId), set = finite(settlementChainId);
@@ -356,18 +348,18 @@ export function networkGateVerdict({ walletChainId = null, selectedChainId = nul
356
348
  }
357
349
 
358
350
  // ---------------------------------------------------------------------------
359
- // СВОДКА. Вызывающий собирает то, что у него есть, и получает ОДИН ответ: можно ли начинать подпись.
360
- // Порядок проверок задаётся порядком массива: причина называется первая по списку, потому что при
361
- // неверной сети вопрос о покрытии суммы уже не имеет смысла.
351
+ // THE SUMMARY. The caller assembles what it has and gets ONE answer: whether the signature may start. The order
352
+ // of checks is set by the order of the array: the first cause in the list is named, because with a wrong network
353
+ // the question of size coverage no longer matters.
362
354
  // ---------------------------------------------------------------------------
363
- // ЖИВАЯ СЕТЬ НЕ ПОДПИСЫВАЕТ СДЕЛКУ С ЗАГЛУШКОЙ. В демке сторона контрагента собирается локально и помечена
364
- // standIn: тогда DLEQ проверяется сам с собой и не доказывает ничего - это режим витрины, а не сделки.
365
- // На живой сети такой подписи быть не должно: деньги ушли бы против несуществующей стороны.
355
+ // A LIVE NETWORK DOES NOT SIGN A DEAL WITH A STAND-IN. In the demo the counterparty side is assembled locally and
356
+ // marked standIn: then DLEQ is checked against itself and proves nothing - a showcase mode, not a deal. On a live
357
+ // network there must be no such signature: the money would go against a party that does not exist.
366
358
  export function counterpartyVerdict({ standIn = false, liveChain = false, network = null, chainId = null } = {}) {
367
359
  const v = (ok, kind, words) => ({ ok, checked: true, blocks: !ok, kind, words, reason: words });
368
- // СЕТЬ НАЗЫВАЕТСЯ ЧИСЛОМ И КОДОМ, а не словом «боевая»: по отказу должно быть видно, о какой сети речь
369
- // (у одной и той же ноды их может быть несколько), и это тот же стандарт, что у прочих отказов - причина
370
- // без числа считается дефектом, потому что по ней нельзя понять, что именно случилось.
360
+ // THE NETWORK IS NAMED BY NUMBER AND CODE, not by the word "live": a refusal must show which network is meant
361
+ // (one node can serve several), and that is the same standard as elsewhere - a cause without a number is a
362
+ // defect, because it leaves no way to understand what happened.
371
363
  const where = (network || "this network") + (chainId === null || chainId === undefined ? "" : " (chainId " + chainId + ")");
372
364
  if (!standIn) return v(true, "real-provider", "counterparty is a real provider: the proof is checked against it");
373
365
  if (liveChain) {
@@ -379,12 +371,12 @@ export function counterpartyVerdict({ standIn = false, liveChain = false, networ
379
371
 
380
372
 
381
373
  // ---------------------------------------------------------------------------
382
- // 9) ТОЧКИ ОРДЕРА В ЦЕПИ ПРОТИВ ТОЧЕК, НА КОТОРЫХ СОШЁЛСЯ DLEQ (находка P0-3 аудита).
383
- // DLEQ доказывает связь точки с половиной, но сам по себе не мешает положить в ордер ДРУГИЕ точки:
384
- // доказательство сойдётся на одних, а в createOrderAndFund уйдут другие - и после claim забор XMR
385
- // не соберётся, потому что ключ траты выведен из других половин. Цепь этого не проверит (в EVM нет
386
- // ed25519), поэтому проверяет та сторона, которая рискует, и ДО своего действия. Здесь только решается
387
- // вопрос «заперта ли подпись»; сама сверка берёт точки ИЗ СЛОТОВ ЖИВОГО ЭСКРОУ, а не из памяти формы.
374
+ // 9) ORDER POINTS ON CHAIN AGAINST THE POINTS DLEQ AGREED ON (an audit finding). DLEQ proves a point's link to a
375
+ // half, but by itself does not stop OTHER points from going into the order: the proof would hold for one set while
376
+ // createOrderAndFund gets another - and after claim the XMR sweep would not assemble, because the spend key is
377
+ // derived from other halves. The chain cannot check this (EVM has no ed25519), so the side that risks checks it,
378
+ // BEFORE acting. Here only the question "is the signature locked" is decided; the comparison takes the points
379
+ // FROM THE LIVE ESCROW SLOTS, not from form memory.
388
380
  // ---------------------------------------------------------------------------
389
381
  const normHex = (v) => (v === null || v === undefined ? "" : String(v).replace(/^0x/i, "").toLowerCase());
390
382
  export function escrowPointsVerdict({ role = null, what = null, point = null, onchainPoint = null, commit = null, onchainCommit = null, readError = null } = {}) {
@@ -393,9 +385,9 @@ export function escrowPointsVerdict({ role = null, what = null, point = null, on
393
385
  return { ok: false, checked: false, blocks: true, kind: "points-unchecked",
394
386
  reason: `${who} of the live escrow could not be read (${String(readError)}): whether the order holds the points that DLEQ proved is unknown - signing is blocked` };
395
387
  }
396
- // ВЕТКА ВЫБИРАЕТСЯ ТЕМ, ЧТО ПРОСЯТ СВЕРИТЬ. Порядок проверок здесь - не косметика: если спрашивают про
397
- // обязательство, а проверять сначала точку, то на отсутствии ТОЧКИ вердикт скажет "точки не прочитаны",
398
- // и настоящая причина (обязательство не то) останется неназванной - это уже было поймано зубом.
388
+ // THE BRANCH IS CHOSEN BY WHAT IS ASKED TO BE COMPARED. The order of checks here is not cosmetic: if the
389
+ // commitment is asked about while the point is checked first, a missing POINT makes the verdict say "points not
390
+ // read" and the real cause (a wrong commitment) goes unnamed - already caught by a tooth.
399
391
  let compared = false;
400
392
  if (normHex(point)) {
401
393
  compared = true;
@@ -408,8 +400,8 @@ export function escrowPointsVerdict({ role = null, what = null, point = null, on
408
400
  reason: `${who} in the escrow (0x${normHex(onchainPoint)}) is NOT the point DLEQ proved (0x${normHex(point)}): the Monero address would be assembled from other halves and the XMR could not be swept after claim` };
409
401
  }
410
402
  }
411
- // ОБА ПОЛЯ СВЕРЯЮТСЯ НЕЗАВИСИМО. Ветка "иначе" здесь была ошибкой: при совпавшей точке расхождение
412
- // обязательства просто не проверялось, и подмена половины проходила мимо вердикта. Поймано зубом.
403
+ // BOTH FIELDS ARE COMPARED INDEPENDENTLY. An "else" branch here was a bug: when the point matched, a commitment
404
+ // divergence simply went unchecked and a swapped half slipped past the verdict. Caught by a tooth.
413
405
  if (normHex(commit)) {
414
406
  compared = true;
415
407
  if (!normHex(onchainCommit)) {
@@ -429,10 +421,10 @@ export function escrowPointsVerdict({ role = null, what = null, point = null, on
429
421
  reason: `${who} in the escrow matches what we hold${normHex(point) && normHex(commit) ? " (point and commitment)" : ""}` };
430
422
  }
431
423
 
432
- // СЛОТЫ ЖИВОГО ОРДЕРА ПРОТИВ СОБРАННОГО СТРАНИЦЕЙ (блок C аудита). Один вопрос, четыре ответа: точки обеих
433
- // сторон и обязательства на обе половины. Сверяется КАЖДОЕ поле, которое у нас есть, и отсутствие поля у
434
- // цепи запирает так же, как несовпадение. Собрано из того же примитива, что и одиночная сверка точки
435
- // (escrowPointsVerdict), чтобы формулировки отказов и правила сравнения были одни на всех.
424
+ // LIVE ORDER SLOTS AGAINST WHAT THE PAGE ASSEMBLED. One question, four answers: the points of both sides and the
425
+ // commitments to both halves. EVERY field we have is compared, and a field missing on chain locks just like a
426
+ // mismatch. Built from the same primitive as the single point check (escrowPointsVerdict), so refusals and
427
+ // comparison rules are one for all.
436
428
  export function liveOrderSlotsVerdict({ slots = null, expect = null, readError = null } = {}) {
437
429
  if (readError || !slots) {
438
430
  return { ok: false, checked: false, blocks: true, kind: "slots-unchecked",
@@ -448,7 +440,7 @@ export function liveOrderSlotsVerdict({ slots = null, expect = null, readError =
448
440
  const checked = [];
449
441
  const missing = [];
450
442
  for (const [key, what, kind] of fields) {
451
- if (!normHex(expect && expect[key])) continue; // чего у нас нет, того и не сверяем - и это названо ниже
443
+ if (!normHex(expect && expect[key])) continue; // what we do not have we do not compare - and that is named below
452
444
  if (!normHex(slots[key])) { missing.push(what); continue; }
453
445
  const v = kind === "point"
454
446
  ? escrowPointsVerdict({ what, point: expect[key], onchainPoint: slots[key] })
@@ -479,14 +471,14 @@ export function signingGateVerdict(checks = []) {
479
471
  reason: blocking.length ? blocking.map((c) => c.reason).filter(Boolean).join("; ") : null,
480
472
  blockedBy: blocking.map((c) => c.kind),
481
473
  unchecked: unchecked.map((c) => c.kind),
482
- // САМО ПРОВЕРЕННОЕ ВОЗВРАЩАЕТСЯ ЦЕЛИКОМ: экран и отчёт обязаны называть не только вердикт, но и
483
- // из чего он взят, а числа там уже посчитаны.
474
+ // WHAT WAS CHECKED IS RETURNED WHOLE: the screen and the report must name not only the verdict but what it was
475
+ // taken from, and the numbers there are already computed.
484
476
  checks: list,
485
477
  };
486
478
  }
487
479
 
488
- // Слова для кнопки и подписи под ней. Одна фраза на вердикт - чтобы экран и отчёт говорили одно и то
489
- // же: разойдясь, они начнут объяснять человеку одно, а оператору другое.
480
+ // Words for the button and the caption under it. One phrase per verdict, so the screen and the report say the
481
+ // same thing: once they diverge they will explain one thing to the person and another to the operator.
490
482
  export function signingGateWords(gate) {
491
483
  if (!gate || !gate.blocked) return null;
492
484
  const first = (gate.checks || []).find((c) => c.blocks === true) || null;