@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
package/engine/evm/dex.js CHANGED
@@ -1,84 +1,83 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/dex.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
- // DEX-нога: КОТИРОВКА Uniswap v3, полученная из цепи. ТОЛЬКО ЧТЕНИЕ: eth_call, ни одной транзакции.
5
+ // THE DEX LEG: a Uniswap v3 quote read from the chain. READ ONLY: eth_call, not one transaction.
7
6
  //
8
- // ЧТО ЗДЕСЬ ЗАМЕНЕНО. Раньше в разбивке цены стоял мок: "Uniswap v3 USDC/WETH 0.05%" и спред 0.0004
9
- // брались из констант конфига (www/js/mock/market.js:76-77), то есть комиссия пула и выход были
10
- // ВЫДУМАНЫ. Теперь и то и другое читается у самой цепи через QuoterV2 - тот же путь, которым страница
11
- // уже читает цену и газ (evm/prices.js): POST к chain.rpcUrl, никаких новых доменов, никаких новых
12
- // параметров в URL и никаких новых внешних адресов. Хост уже разрешён в CSP - он берётся из реестра сетей.
7
+ // WHAT IS REPLACED HERE. The price breakdown used to hold a mock: "Uniswap v3 USDC/WETH 0.05%" and the spread
8
+ // 0.0004 came from config constants, i.e. the pool fee and the output were INVENTED. Now both are read from the
9
+ // chain itself via QuoterV2 - the same path the page already uses to read price and gas (evm/prices.js): a POST
10
+ // to chain.rpcUrl, no new domains, no new URL parameters and no new external addresses. The host is already
11
+ // allowed in CSP - it comes from the network registry.
13
12
  //
14
- // СЕЛЕКТОРЫ И СТРУКТУРА АРГУМЕНТОВ ВЗЯТЫ ИЗ ВЫЗОВА, А НЕ ИЗ ПАМЯТИ (проверено 2026-09-23):
15
- // * selector = keccak256(подпись)[0..4]. Считается в тесте (tools/check-uniswap-quote.mjs) из строки
16
- // подписи, лежащей здесь рядом с селектором, и наличие селектора сверяется в КОДЕ контракта.
17
- // * АРГУМЕНТЫ ИДУТ ПЯТЬЮ СЛОВАМИ ПОДРЯД, БЕЗ СЛОВА-СМЕЩЕНИЯ. Подпись формально принимает структуру
18
- // (address,address,uint256,uint24,uint160), и по спецификации ABI первым словом должно идти
19
- // смещение 0x20. НА РАЗВЁРНУТОМ QuoterV2 ЭТО ЛОЖЬ: вариант со смещением ОТКАТЫВАЕТ (execution
20
- // reverted), а пять слов подряд отвечают четырьмя словами (amountOut, sqrtPriceX96After,
21
- // initializedTicksCrossed, gasEstimate). Поэтому кодируем подряд - и это записано здесь, чтобы
22
- // никто не "починил" код по спецификации и не сломал котировку молча.
13
+ // SELECTORS AND ARGUMENT SHAPE ARE TAKEN FROM A CALL, NOT FROM MEMORY (checked 2026-09-23):
14
+ // * selector = keccak256(signature)[0..4]. Computed by a check from the signature string lying here next to
15
+ // the selector, and the selector's presence is cross-checked in the contract CODE.
16
+ // * THE ARGUMENTS GO AS FIVE CONSECUTIVE WORDS, WITHOUT AN OFFSET WORD. The signature formally takes a struct
17
+ // (address,address,uint256,uint24,uint160) and by the ABI spec the first word should be the offset 0x20. ON
18
+ // THE DEPLOYED QuoterV2 THAT IS FALSE: the offset variant REVERTS (execution reverted), while five consecutive
19
+ // words answer with four (amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate). So we encode
20
+ // consecutively - and this is written here so nobody "fixes" the code to the spec and silently breaks the quote.
23
21
  //
24
- // ЧЕГО МОДУЛЬ НЕ ДЕЛАЕТ: не отправляет транзакции, не делает approve, не свопает и НЕ трогает путь
25
- // фондирования. Оценка газа здесь - это gasEstimate, который вернул сам QuoterV2 (число из цепи), а не
26
- // eth_estimateGas по чужой транзакции.
27
22
  //
28
- // ЧЕСТНОЕ "НЕТ КОТИРОВКИ" ВМЕСТО ВЫДУМАННОГО ЧИСЛА. Пустой пул, отсутствие пула, откат котировки и
29
- // цена, разошедшаяся с эталоном, - ЧЕТЫРЕ РАЗНЫХ ответа, и каждый назван словами (why). Число
30
- // возвращается только тогда, когда его действительно назвала цепь и оно прошло сверку с эталоном.
23
+ // WHAT THE MODULE DOES NOT DO: it sends no transactions, makes no approve, swaps nothing and does NOT touch the
24
+ // funding path. The gas figure here is the gasEstimate QuoterV2 itself returned (a number from the chain), not
25
+ // eth_estimateGas over someone else's transaction.
31
26
  //
32
- // МАРШРУТ ЗДЕСЬ ОБЪЯВЛЕН, А НЕ ВЫБРАН. Перебора "прямой путь или через хаб" в рантайме нет и быть не
33
- // должно: человек объявил маршрут в реестре (DEX_ROUTES), а модуль только переводит его в путь и
34
- // спрашивает цепь ОДНИМ вызовом. Если объявленного маршрута нет (или в нём пусто) - это честное
35
- // "маршрута нет", причина лежит в данных (route.why), и такая нога блокирует подпись, а не ищет обход.
27
+ // AN HONEST "NO QUOTE" INSTEAD OF AN INVENTED NUMBER. An empty pool, no pool, a reverted quote and a price that
28
+ // diverged from the reference - FOUR DIFFERENT answers, each named in words (why). A number is returned only
29
+ // when the chain really named it and it passed the reference check.
30
+ //
31
+ // THE ROUTE IS DECLARED HERE, NOT CHOSEN. There is no runtime search for "a direct path or via a hub" and there
32
+ // must not be: a person declared the route in the registry (DEX_ROUTES), and the module only turns it into a path
33
+ // and asks the chain IN ONE CALL. If there is no declared route (or it is empty) - that is an honest "no route",
34
+ // the cause lies in the data (route.why), and such a leg blocks signing rather than looking for a detour.
35
+ //
36
+ // THE PRICE CHECK (priceGateVerdict) - A SECOND GUARD OF THE SAME FAMILY as coverage: a wrong price means the
37
+ // deal will not settle and signing does not start. It is computed two ways, neither needing a feed per token: the
38
+ // implied native price from the quote against the reference (Chainlink) and a "near 1:1" band on steps between
39
+ // stables (with no oracle at all). The thresholds are in the registry (DEX_PRICE_GATE), and the testnet exception
40
+ // is data too (a network's dexPriceGate): there the price is wrong by nature, and a wrong price is marked in
41
+ // words rather than stopping the flow. There is still no liquidity scoring here: pool depth is a person's call.
42
+ //
43
+
44
+ // Selectors. Next to each is the signature string it was computed from: the check recomputes keccak256 over these
45
+ // strings and fails if even one selector diverged from its signature.
46
+ // WHAT IS VERIFIED BY A CALL HERE (2026-09-23, Arbitrum One and Ethereum):
47
+ // * quoteExactInputSingle 0xc6a5026a - a ONE-step quote, five consecutive words with no offset;
48
+ // * quoteExactInput 0xcdca1753 - a WHOLE-PATH quote in one call. Signature quoteExactInput(bytes,uint256), the
49
+ // selector found in the QuoterV2 code on both networks, and a single-step path checked against the single
50
+ // quote gave EXACTLY the same number (36685744820778786 for 100 USDC), so the path encoding (20-byte address
51
+ // + 3-byte fee, consecutively) is confirmed by a number, not from memory. There is no reference value - there
52
+ // is a match of two DIFFERENT calls.
36
53
  //
37
- // ПРОВЕРКА ЦЕНЫ (priceGateVerdict) - ВТОРАЯ ЗАЩИТА ТОЙ ЖЕ СЕМЬИ, что покрытие суммы: неверная цена
38
- // означает, что сделка не сойдётся, и подпись не начинается. Считается двумя способами, ни один из
39
- // которых не требует фида на каждый токен: неявная цена натива из котировки против эталона (Chainlink)
40
- // и полоса "около 1:1" на шагах между стейблами (без оракула вовсе). Пороги - в реестре
41
- // (DEX_PRICE_GATE), исключение для тестнета - тоже данные (dexPriceGate у сети): там цена негодная по
42
- // природе, и негодная цена помечается словами, а не останавливает поток. Оценок ликвидности здесь
43
- // по-прежнему нет: глубину пула решает человек.
44
-
45
- // Селекторы. Рядом с каждым - строка подписи, из которой он посчитан: проверка
46
- // (tools/check-uniswap-quote.mjs) пересчитывает keccak256 от этих строк и падает, если хоть один
47
- // селектор разошёлся с подписью.
48
- // ЧТО ЗДЕСЬ ПРОВЕРЕНО ВЫЗОВОМ (2026-09-23, Arbitrum One и Ethereum, tools/check-uniswap-quote.mjs):
49
- // * quoteExactInputSingle 0xc6a5026a - котировка ОДНОЙ ступени, пять слов подряд без смещения;
50
- // * quoteExactInput 0xcdca1753 - котировка ВСЕГО ПУТИ ОДНИМ ВЫЗОВОМ. Подпись
51
- // quoteExactInput(bytes,uint256), селектор найден в коде QuoterV2 в обеих сетях, а сверка
52
- // одноступенчатого пути против одиночной котировки дала РОВНО то же число (36685744820778786 на
53
- // 100 USDC), то есть кодирование пути (20 байт адрес + 3 байта комиссия, подряд) подтверждено числом,
54
- // а не по памяти. Эталонного значения нет - есть совпадение двух РАЗНЫХ вызовов.
55
54
  export const DEX_METHOD = {
56
- // ФОРМА С ТОЧНЫМ ВЫХОДОМ. Это НЕ "сколько выйдет", а "получить ровно столько, потратив не больше
57
- // такого-то": при ней вопроса о разнице сумм не существует вовсе (док 45 §"Что добавит DEX-нога").
58
- // ОБЕ формы подтверждены вызовом на Arbitrum One и основной сети (tools/check-uniswap-quote.mjs):
59
- // - одиночная даёт amountIn на ровно 0.035 WETH;
60
- // - многошаговая по пути из двух ног тоже отвечает, но ПУТЬ У НЕЁ ЗАДАЁТСЯ В ОБРАТНУЮ СТОРОНУ - от
61
- // получаемого токена к оплачиваемому (прямой путь отказывает: "Unexpected error"). Это измерено, а
62
- // не выведено: перепутать направление здесь стоит отказа вызова.
55
+ // THE EXACT-OUT SHAPE. This is NOT "how much comes out" but "receive exactly this much, spending no more than
56
+ // that": with it the question of a difference in amounts does not exist at all.
57
+ // BOTH shapes are confirmed by a call on Arbitrum One and mainnet:
58
+ // - the single one gives amountIn for exactly 0.035 WETH;
59
+ // - the multi-step on a two-leg path also answers, but ITS PATH IS SET IN REVERSE - from the received token
60
+ // to the paid one (a forward path fails: "Unexpected error"). This is measured, not deduced: getting the
61
+ // direction wrong here costs a call refusal.
63
62
  quoteExactOutputSingle: { hex: "0xbd21704a", signature: "quoteExactOutputSingle((address,address,uint256,uint24,uint160))" },
64
63
  quoteExactOutput: { hex: "0x2f80bb1d", signature: "quoteExactOutput(bytes,uint256)" },
65
64
  quoteExactInputSingle: { hex: "0xc6a5026a", signature: "quoteExactInputSingle((address,address,uint256,uint24,uint160))" },
66
- // Многошаговая форма: путь передаётся УПАКОВАННЫМ (token|fee|token|fee|token), аргумент динамический,
67
- // поэтому здесь смещение 0x40 (см. encodeQuoteExactInput), в отличие от одиночной формы.
65
+ // The multi-step shape: the path is passed PACKED (token|fee|token|fee|token), the argument is dynamic, so the
66
+ // offset is 0x40 here (see encodeQuoteExactInput), unlike the single shape.
68
67
  quoteExactInput: { hex: "0xcdca1753", signature: "quoteExactInput(bytes,uint256)" },
69
68
  getPool: { hex: "0x1698ee82", signature: "getPool(address,address,uint24)" },
70
69
  liquidity: { hex: "0x1a686502", signature: "liquidity()" },
71
70
  balanceOf: { hex: "0x70a08231", signature: "balanceOf(address)" },
72
- // withdraw у WETH9: НЕ для отправки, только для оценки газа unwrap (и только если у нас есть
73
- // измеренный держатель WETH - см. tools/check-uniswap-quote.mjs).
71
+ // withdraw on WETH9: NOT for sending, only for estimating the unwrap gas (and only if we have a measured WETH
72
+ // holder - see the DEX quote check).
74
73
  withdraw: { hex: "0x2e1a7d4d", signature: "withdraw(uint256)" },
75
74
  };
76
75
 
77
- // ПУТЬ ИЗ ОБЪЯВЛЕННОГО МАРШРУТА (DEX_ROUTES в реестре). Здесь НЕТ никакого выбора: человек объявил шаги
78
- // со ступенями, а мы только переводим символы в адреса. Обёрнутый нативный токен берётся из данных сети
79
- // (u.wrapped), а не пишется строкой "WETH": у сети на BNB обёрнутый натив - WBNB.
80
- // Возвращает null и причину, если объявленный маршрут собрать нельзя (шаг назван токеном, которого в
81
- // этой сети нет) - это НЕ "маршрута нет", а ошибка в данных, и называть её надо иначе.
76
+ // THE PATH FROM THE DECLARED ROUTE (DEX_ROUTES in the registry). There is NO choice here: a person declared the
77
+ // steps with their fees, and we only map symbols to addresses. The wrapped native comes from the network data
78
+ // (u.wrapped), not written as the string "WETH": a BNB network's wrapped native is WBNB.
79
+ // Returns null and a cause if the declared route cannot be assembled (a step names a token this network does not
80
+ // have) - that is NOT "no route" but a data error, and it must be named differently.
82
81
  export function declaredRoutePath({ token, tokens = [], wrapped, route }) {
83
82
  if (!token || !route || !Array.isArray(route.steps) || !route.steps.length) return null;
84
83
  const bySym = new Map((tokens || []).map((t) => [String(t.symbol).toUpperCase(), t]));
@@ -103,8 +102,8 @@ export function declaredRoutePath({ token, tokens = [], wrapped, route }) {
103
102
  return { tokens: addresses, symbols, fees, pools };
104
103
  }
105
104
 
106
- // Путь одной строкой с комиссиями ступеней: "USDe → USDC 0.01% → WETH 0.05%". Одна и та же запись в
107
- // интерфейсе и в отчёте - разойдясь, они начнут объяснять человеку одно, а оператору другое.
105
+ // The path as one string with step fees: "USDe → USDC 0.01% → WETH 0.05%". The same record in the interface and
106
+ // in the report - once they diverge they explain one thing to the person and another to the operator.
108
107
  export function routeLabel(route) {
109
108
  if (!route || !route.symbols) return "";
110
109
  let s = route.symbols[0];
@@ -112,13 +111,13 @@ export function routeLabel(route) {
112
111
  return s;
113
112
  }
114
113
 
115
- // Разбор пути на шаги: [{from, to, fee, feeLabel}] - для пошаговой строки в интерфейсе и в отчёте.
114
+ // Split the path into steps: [{from, to, fee, feeLabel}] - for the step-by-step line in the interface and report.
116
115
  export function routeSteps(route) {
117
116
  if (!route || !route.symbols) return [];
118
117
  return route.fees.map((fee, i) => ({ from: route.symbols[i], to: route.symbols[i + 1], fee, feeLabel: feeTierLabel(fee) }));
119
118
  }
120
119
 
121
- const CACHE_MS = 20_000; // котировка живёт 20 с: дольше держать её нельзя (цена в пуле движется), чаще спрашивать незачем
120
+ const CACHE_MS = 20_000; // a quote lives 20 s: holding it longer is unsafe (the pool price moves), asking more often is needless
122
121
 
123
122
  const cache = new Map();
124
123
 
@@ -135,8 +134,8 @@ export function resetDexCache() {
135
134
  cache.clear();
136
135
  }
137
136
 
138
- // Один eth_call в сеть сети из реестра. Никогда не бросает: молчание узла - это состояние интерфейса,
139
- // а не падение экрана (так же сделано в evm/prices.js).
137
+ // One eth_call to the network's node from the registry. Never throws: node silence is a UI state, not a screen
138
+ // crash (the same as in evm/prices.js).
140
139
  async function rpc(chain, method, params = [], { timeoutMs = 15_000 } = {}) {
141
140
  if (!chain || !chain.rpcUrl) return { ok: false, why: "network has no RPC endpoint" };
142
141
  const ctl = typeof AbortController === "function" ? new AbortController() : null;
@@ -160,14 +159,14 @@ async function rpc(chain, method, params = [], { timeoutMs = 15_000 } = {}) {
160
159
  }
161
160
  }
162
161
 
163
- // --- кодирование/декодирование ABI без библиотек (в проекте ноль зависимостей в app/) ---------------
162
+ // --- ABI encode/decode without libraries (the project has zero dependencies in app/) ----------------------
164
163
  const hexBody = (s) => String(s || "").replace(/^0x/, "");
165
164
  const word = (v) => hexBody(v).toLowerCase().padStart(64, "0");
166
- // АДРЕС - ЭТО ЦЕЛОЕ СЛОВО ABI (24 нуля и 20 байт адреса), а не только 20 байт. Здесь была ошибка ровно
167
- // наоборот от той, что кажется очевидной: сначала адрес дополнялся нулями до слова, а потом у слова
168
- // срезались первые 24 знака - то есть дополнение выбрасывалось, и в calldata уходили аргументы,
169
- // сдвинутые на 12 байт. Вызов на этом откатывался ("execution reverted"), и выглядело это как "в пуле
170
- // нет ликвидности", хотя пул отвечал - врал наш кодировщик (поймано сверкой с рабочим вызовом).
165
+ // AN ADDRESS IS A WHOLE ABI WORD (24 zeros and 20 address bytes), not just 20 bytes. The bug here was exactly the
166
+ // opposite of the obvious one: the address was first padded with zeros to a word, and then the first 24 characters
167
+ // of the word were cut - i.e. the padding was thrown away and calldata carried arguments shifted by 12 bytes. The
168
+ // call reverted ("execution reverted") and it looked like "no liquidity in the pool" although the pool answered -
169
+ // our encoder lied (caught by comparing against a working call).
171
170
  const addrWord = (a) => word(a);
172
171
  const uintWord = (v) => {
173
172
  let h = "";
@@ -198,16 +197,16 @@ export function encodeGetPool({ tokenA, tokenB, fee }) {
198
197
  return DEX_METHOD.getPool.hex + addrWord(tokenA) + addrWord(tokenB) + uintWord(fee);
199
198
  }
200
199
 
201
- // ОБРАТНАЯ ЗАДАЧА ПО ОДНОМУ ПУЛУ: сколько надо отдать, чтобы получить РОВНО amountOutWei. Раскладка та же,
202
- // что у одиночной формы на вход (статическая структура идёт ВНУТРИ, без слова смещения): адрес, адрес,
203
- // сумма, комиссия, предел цены. Подтверждено вызовом, см. DEX_METHOD.quoteExactOutputSingle.
200
+ // THE INVERSE FOR ONE POOL: how much you must give to get EXACTLY amountOutWei. The layout is the same as the
201
+ // single input shape (the static struct goes INSIDE, with no offset word): address, address, amount, fee, price
202
+ // limit. Confirmed by a call, see DEX_METHOD.quoteExactOutputSingle.
204
203
  export function encodeQuoteExactOutputSingle({ tokenIn, tokenOut, amountOutWei, fee, sqrtPriceLimitX96 = 0 }) {
205
204
  return DEX_METHOD.quoteExactOutputSingle.hex +
206
205
  addrWord(tokenIn) + addrWord(tokenOut) + uintWord(amountOutWei) + uintWord(fee) + uintWord(sqrtPriceLimitX96);
207
206
  }
208
207
 
209
- // ЛИБО СКОЛЬКО ВЫЙДЕТ, ЛИБО ПОЧЕМУ НЕТ - так же, как у формы на вход. Здесь ЭТО НЕ ПРОГНОЗ, а ответ на
210
- // вопрос "хватает ли пула, чтобы получить ровно столько": отказ и есть "не хватает".
208
+ // EITHER HOW MUCH COMES OUT, OR WHY NOT - same as the input shape. Here this is NOT a forecast but the answer to
209
+ // "does the pool have enough to give exactly this much": the refusal IS "not enough".
211
210
  export async function quoteExactOutSingle(chain, { tokenIn, tokenOut, amountOutWei, fee }, opts = {}) {
212
211
  const u = chain && chain.uniswap;
213
212
  if (!u || !u.quoter) return { ok: false, why: "no verified QuoterV2 for this network" };
@@ -224,12 +223,12 @@ export async function quoteExactOutSingle(chain, { tokenIn, tokenOut, amountOutW
224
223
  };
225
224
  }
226
225
 
227
- // ОБРАТНАЯ ЗАДАЧА ПО ВСЕМУ ПУТИ: сколько надо отдать, чтобы получить РОВНО amountOutWei.
228
- // ПУТЬ ЗДЕСЬ ИДЁТ В ОБРАТНУЮ СТОРОНУ - от получаемого токена к оплачиваемому, и комиссии в том же
229
- // обратном порядке. Это не наше соглашение, а требование контракта: на прямом пути вызов отказывает
230
- // ("Unexpected error"), на обратном отвечает числом. Измерено на обеих сетях, см. DEX_METHOD.
231
- // ПРЕДПОЧТЕНИЕ ЭТОЙ ФОРМЫ: при ней минимум на выходе не "примерно равен" требуемой сумме, а равен ей
232
- // точно, и вопроса "а что делать с разницей" не возникает вовсе (док 45).
226
+ // THE INVERSE OVER THE WHOLE PATH: how much you must give to get EXACTLY amountOutWei.
227
+ // THE PATH GOES IN REVERSE HERE - from the received token to the paid one, with fees in the same reverse order.
228
+ // This is not our convention but the contract's requirement: a forward path fails ("Unexpected error"), a reverse
229
+ // one answers with a number. Measured on both networks, see DEX_METHOD.
230
+ // THE PREFERENCE FOR THIS SHAPE: with it the output minimum is not "roughly equal" to the required amount but
231
+ // exactly it, and the question "what to do with the difference" does not arise at all.
233
232
  export async function quoteExactOutPath(chain, { route, amountOutWei }, opts = {}) {
234
233
  const u = chain && chain.uniswap;
235
234
  if (!u || !u.quoter) return { ok: false, why: "no verified QuoterV2 for this network" };
@@ -237,12 +236,12 @@ export async function quoteExactOutPath(chain, { route, amountOutWei }, opts = {
237
236
  const tokens = route.tokens.slice().reverse();
238
237
  const fees = (route.fees || []).slice().reverse();
239
238
  const path = encodePath({ tokens, fees });
240
- // Динамический аргумент (bytes) идёт со словом смещения: 0x40, затем сумма точного выхода, затем сам путь.
239
+ // A dynamic argument (bytes) goes with an offset word: 0x40, then the exact output amount, then the path itself.
241
240
  const data = DEX_METHOD.quoteExactOutput.hex + uintWord(0x40) + uintWord(amountOutWei) + uintWord(path.bytes) +
242
241
  path.packed.padEnd(Math.ceil(path.packed.length / 64) * 64, "0");
243
242
  const res = await rpc(chain, "eth_call", [{ to: u.quoter, data }, "latest"], opts);
244
243
  if (!res.ok) return { ok: false, why: res.why };
245
- const out = decodeQuoteExactInput(res.result); // форма ответа та же: сумма, массивы, газ
244
+ const out = decodeQuoteExactInput(res.result); // the answer shape is the same: amount, arrays, gas
246
245
  if (!out || out.amountOutWei <= 0n) return { ok: false, why: "pool cannot serve exactly this output" };
247
246
  return {
248
247
  ok: true,
@@ -253,10 +252,10 @@ export async function quoteExactOutPath(chain, { route, amountOutWei }, opts = {
253
252
  };
254
253
  }
255
254
 
256
- // ПУТЬ ДЛЯ МНОГОШАГОВОЙ КОТИРОВКИ. Путь в Uniswap v3 - это УПАКОВАННАЯ последовательность
257
- // "адрес(20) + комиссия(3)" с завершающим адресом: token0|fee0|token1|fee1|token2. Никаких слов ABI
258
- // внутри пути нет: 43 байта на одну ногу, 66 на две. Проверено числом: одноступенчатый путь через
259
- // quoteExactInput вернул ровно тот же amountOut, что и quoteExactInputSingle по той же ступени.
255
+ // THE PATH FOR A MULTI-STEP QUOTE. A Uniswap v3 path is a PACKED sequence "address(20) + fee(3)" with a trailing
256
+ // address: token0|fee0|token1|fee1|token2. There are no ABI words inside the path: 43 bytes per leg, 66 for two.
257
+ // Confirmed by a number: a single-step path through quoteExactInput returned exactly the same amountOut as
258
+ // quoteExactInputSingle on the same step.
260
259
  export function encodePath({ tokens, fees }) {
261
260
  const parts = [];
262
261
  for (let i = 0; i < tokens.length; i++) {
@@ -267,10 +266,10 @@ export function encodePath({ tokens, fees }) {
267
266
  return { packed, bytes: packed.length / 2 };
268
267
  }
269
268
 
270
- // ВЫЗОВ quoteExactInput(bytes path, uint256 amountIn) - ОДИН eth_call на ВЕСЬ маршрут. Собран по
271
- // спецификации ABI, потому что здесь аргумент динамический: слово смещения (0x40), затем amountIn,
272
- // затем длина пути и сам путь, добитый до границы слова. ЗДЕСЬ, В ОТЛИЧИЕ ОТ ОДИНОЧНОЙ ФОРМЫ, СМЕЩЕНИЕ
273
- // ДЕЙСТВИТЕЛЬНО НУЖНО - одиночная форма принимает статичную структуру и идёт словами подряд.
269
+ // THE CALL quoteExactInput(bytes path, uint256 amountIn) - ONE eth_call for the WHOLE route. Built to the ABI
270
+ // spec because the argument is dynamic here: an offset word (0x40), then amountIn, then the path length and the
271
+ // path itself, padded to a word boundary. HERE, UNLIKE THE SINGLE SHAPE, THE OFFSET IS REALLY NEEDED - the single
272
+ // shape takes a static struct and goes as consecutive words.
274
273
  export function encodeQuoteExactInput({ tokens, fees, amountInWei }) {
275
274
  const { packed, bytes } = encodePath({ tokens, fees });
276
275
  const padded = packed.padEnd(Math.ceil(bytes / 32) * 64, "0");
@@ -279,11 +278,11 @@ export function encodeQuoteExactInput({ tokens, fees, amountInWei }) {
279
278
  const wordToAddress = (w) => (w === undefined || w === null ? null : "0x" + w.toString(16).padStart(40, "0"));
280
279
  const ZERO_ADDR = "0x" + "0".repeat(40);
281
280
 
282
- // Ступень комиссии в процентах: 500 -> "0.05%". Это ЕДИНСТВЕННАЯ величина, которая раньше бралась из
283
- // мока, и теперь она считается из самой ступени, а не берётся константой.
284
- // Арифметика: fee измеряется в сотых долях базисной точки, 1 000 000 = 100%. Значит проценты = fee/10 000,
285
- // то есть 500 -> 0.05%. Здесь сначала стояло ЕЩЁ деление на 100, и ступень 500 подписывалась как
286
- // "0.0005%" - то есть комиссия занижалась в сто раз (поймано прогоном, а не на глаз).
281
+ // The step fee in percent: 500 -> "0.05%". This is the ONLY value that used to come from the mock, and now it is
282
+ // computed from the step itself rather than taken as a constant.
283
+ // Arithmetic: fee is measured in hundredths of a basis point, 1,000,000 = 100%. So percent = fee/10,000, i.e.
284
+ // 500 -> 0.05%. It first had ANOTHER division by 100 here, and step 500 was labelled "0.0005%" - the fee was
285
+ // understated a hundredfold (caught by a run, not by eye).
287
286
  export function feeTierPct(fee) {
288
287
  const n = Number(fee);
289
288
  if (!Number.isFinite(n)) return null;
@@ -296,10 +295,10 @@ export function feeTierLabel(fee) {
296
295
  }
297
296
 
298
297
  // ---------------------------------------------------------------------------
299
- // Чтения
298
+ // Reads
300
299
  // ---------------------------------------------------------------------------
301
300
 
302
- // Адрес пула у фабрики. null - пула нет (это ОТВЕТ, а не ошибка: значит менять не на что).
301
+ // The pool address at the factory. null - no pool (that is an ANSWER, not an error: nothing to change it into).
303
302
  export async function poolAddress(chain, { tokenIn, tokenOut, fee }, opts = {}) {
304
303
  const u = chain && chain.uniswap;
305
304
  if (!u || !u.factory) return { ok: false, why: "no verified Uniswap factory for this network" };
@@ -311,9 +310,9 @@ export async function poolAddress(chain, { tokenIn, tokenOut, fee }, opts = {})
311
310
  return { ok: true, pool };
312
311
  }
313
312
 
314
- // Состояние пула: активная ликвидность и запасы обеих сторон. Нужно для ЧЕСТНОГО ответа "ликвидности
315
- // нет": котировка пустого пула отвечает нулём, и без этого чтения мы не отличим "пула нет" от "пул есть,
316
- // но пустой". Читается только в диагностическом пути (когда котировки нет или она неправдоподобна).
313
+ // Pool state: active liquidity and both-side reserves. Needed for an HONEST "no liquidity" answer: an empty
314
+ // pool quotes zero, and without this read we cannot tell "no pool" from "a pool exists but is empty". Read only
315
+ // on the diagnostic path (when there is no quote or it is implausible).
317
316
  export async function poolState(chain, pool, { tokenIn, tokenOut }, opts = {}) {
318
317
  if (!pool) return null;
319
318
  const [liq, balIn, balOut] = await Promise.all([
@@ -329,7 +328,7 @@ export async function poolState(chain, pool, { tokenIn, tokenOut }, opts = {}) {
329
328
  };
330
329
  }
331
330
 
332
- // ОДНА КОТИРОВКА ОДНОЙ СТУПЕНИ. Откат котировки - это НАЗВАННЫЙ отказ, а не исключение.
331
+ // ONE QUOTE OF ONE STEP. A reverted quote is a NAMED refusal, not an exception.
333
332
  export async function quoteTier(chain, { tokenIn, tokenOut, amountInWei, fee }, opts = {}) {
334
333
  const u = chain && chain.uniswap;
335
334
  if (!u || !u.quoter) return { fee, ok: false, why: "no verified QuoterV2 for this network" };
@@ -350,10 +349,10 @@ export async function quoteTier(chain, { tokenIn, tokenOut, amountInWei, fee },
350
349
  };
351
350
  }
352
351
 
353
- // Разбор ответа quoteExactInput. Ответ ДРУГОЙ, чем у одиночной формы: (uint256 amountOut,
354
- // uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate) - то есть
355
- // на каждый шаг пути по элементу в двух массивах. Смещения читаются из самого ответа, а не
356
- // предполагаются: длина массивов равна числу ног пути, и это можно СВЕРИТЬ, а не поверить.
352
+ // Parsing the quoteExactInput answer. The answer DIFFERS from the single shape: (uint256 amountOut,
353
+ // uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate) - i.e. one element
354
+ // per path step in two of the arrays. Offsets are read from the answer itself, not assumed: the array lengths
355
+ // equal the number of path legs, and that can be CHECKED, not trusted.
357
356
  export function decodeQuoteExactInput(hex) {
358
357
  const words = decodeWords(hex);
359
358
  if (words.length < 4) return null;
@@ -373,8 +372,8 @@ export function decodeQuoteExactInput(hex) {
373
372
  };
374
373
  }
375
374
 
376
- // КОТИРОВКА ВСЕГО ПУТИ ОДНИМ ВЫЗОВОМ. Здесь НЕТ цепочки "котировка шага 1, потом шаг 2 от её выхода":
377
- // такая цепочка считает оба шага по одному и тому же состоянию пулов и даёт оптимистично неверное число.
375
+ // A WHOLE-PATH QUOTE IN ONE CALL. There is NO chain of "step 1 quote, then step 2 from its output": such a chain
376
+ // prices both steps against the same pool state and gives a optimistically wrong number.
378
377
  export async function quotePath(chain, { route, amountInWei }, opts = {}) {
379
378
  const u = chain && chain.uniswap;
380
379
  if (!u || !u.quoter) return { ...route, ok: false, why: "no verified QuoterV2 for this network" };
@@ -392,42 +391,42 @@ export async function quotePath(chain, { route, amountInWei }, opts = {}) {
392
391
  sqrtPriceX96After: out.sqrtPriceX96After,
393
392
  initializedTicksCrossed: out.initializedTicksCrossed,
394
393
  hops: route.fees.length,
395
- // ЧИСЛО ШАГОВ ОТВЕТА СВЕРЯЕТСЯ С ЧИСЛОМ НОГ ПУТИ: если контракт вернул не по элементу на шаг, то это
396
- // не тот ответ, который мы умеем читать, и лучше сказать это словами, чем показать число наугад.
394
+ // THE ANSWER STEP COUNT IS CHECKED AGAINST THE PATH LEG COUNT: if the contract returned not one element per
395
+ // step, this is not an answer we can read, and it is better to say so in words than to show a number at random.
397
396
  shapeOk: out.sqrtPriceX96After.length === route.fees.length,
398
397
  };
399
398
  }
400
399
 
401
- // ПОДПИСЬ ОБЪЯВЛЕННОГО МАРШРУТА: одна строка, по которой видно, ИЗ ЧЕГО он собран. Нужна для ключа
402
- // кеша: если объявление поменяли (другая ступень, другой хаб), старый ответ на новый вопрос не годится.
400
+ // THE DECLARED-ROUTE SIGNATURE: one string showing WHAT it is assembled from. Needed for the cache key: if the
401
+ // declaration changed (another fee, another hub), the old answer does not fit the new question.
403
402
  export function routeSignature(route) {
404
403
  if (!route || !Array.isArray(route.steps) || !route.steps.length) return route && route.unwrap ? "unwrap" : "none";
405
404
  return route.steps.map((s) => String(s.to).toUpperCase() + ":" + Number(s.fee) + (s.pool ? ":" + String(s.pool).toLowerCase() : "")).join("+");
406
405
  }
407
406
 
408
407
  // ---------------------------------------------------------------------------
409
- // ПРОВЕРКА ЦЕНЫ: СДЕЛКА НЕВЕРНАЯ ИЛИ НЕТ.
408
+ // THE PRICE CHECK: IS THE DEAL WRONG OR NOT.
410
409
  //
411
- // Это НЕ оценка ликвидности - её, как и раньше, делает человек, и приговоров о глубине пулов здесь нет.
412
- // Это проверка того же рода, что покрытие суммы: если цена не та, сделка не сойдётся, и она не должна
413
- // начинаться. Признак неверной цены берётся ДВУМЯ способами, и ни одному из них не нужен фид на каждый
414
- // токен (иначе USDe/USDS/PyUSD без своего фида сломали бы проверку вовсе):
410
+ // This is NOT liquidity scoring - that is still a person's job, and there are no verdicts about pool depth here.
411
+ // It is a check of the same kind as amount coverage: if the price is wrong the deal will not settle and must not
412
+ // start. A sign of a wrong price is taken TWO ways, and neither needs a feed per token (else USDe/USDS/PyUSD
413
+ // without their own feed would break the check entirely):
415
414
  //
416
- // 1) ПОСЛЕДНИЙ ШАГ В НАТИВ. Из котировки выводится НЕЯВНАЯ цена натива в долларах (сколько входит в
417
- // долларах / сколько натива выходит) и сравнивается с эталоном цены натива - тем самым Chainlink,
418
- // который страница уже читает (evm/prices.js). Это работает для любого входа: у стейбла цена в
419
- // долларах известна из реестра, у остальных маршрут всё равно заканчивается нативом;
420
- // 2) ШАГ МЕЖДУ ДВУМЯ СТЕЙБЛАМИ (USDe→USDC, USDS→USDC, PyUSD→USDC): цена обязана быть около 1:1, и это
421
- // проверяется САМОЙ КОТИРОВКОЙ, без оракула вообще. Абсурд вида "100 USDe дают 0.5 USDC" ловится
422
- // именно здесь.
415
+ // 1) THE LAST STEP INTO NATIVE. The implied dollar price of native is derived from the quote (dollars in /
416
+ // native out) and compared against the native price reference - the same Chainlink the page already reads
417
+ // (evm/prices.js). This works for any input: a stable's dollar price is known from the registry, and the
418
+ // rest end at native anyway;
419
+ // 2) A STEP BETWEEN TWO STABLES (USDe→USDC, USDS→USDC, PyUSD→USDC): the price must be near 1:1, and that is
420
+ // checked BY THE QUOTE ITSELF, with no oracle at all. An absurdity like "100 USDe give 0.5 USDC" is caught
421
+ // exactly here.
423
422
  //
424
- // ПОРОГИ ПРИХОДЯТ ДАННЫМИ (реестр: DEX_PRICE_GATE), а не лежат здесь числами: менять их - правка данных.
425
- // ПРИЗНАК СЕТИ tolerated (реестр: dexPriceGate === "tolerated") ОТМЕНЯЕТ БЛОКИРОВКУ, НО НЕ ФАКТ: цена
426
- // остаётся негодной и названа таковой (bad), а интерфейс обязан её показать. Так тестнет не встаёт на
427
- // цене, которую мы не выбирали, и при этом не притворяется, что цена рыночная.
423
+ // THE THRESHOLDS COME IN AS DATA (the registry: DEX_PRICE_GATE), not as numbers here: changing them is a data edit.
424
+ // A NETWORK MARK tolerated (registry: dexPriceGate === "tolerated") CANCELS THE BLOCK BUT NOT THE FACT: the price
425
+ // stays wrong and is named so (bad), and the interface must show it. Thus a testnet does not stall on a price we
426
+ // did not choose, and does not pretend the price is market.
428
427
  //
429
- // Функция ЧИСТАЯ: ни одного вызова в сеть, только числа ноги и эталон. Поэтому её можно прогнать на
430
- // любых числах без сети - чем и доказывается, что защита краснеет (tools/check-dex-gate.mjs).
428
+ // The function is PURE: not one network call, only the leg numbers and the reference. So it can be run on any
429
+ // numbers without a network - which is how the guard is proven to go red (the DEX gate check).
431
430
  // ---------------------------------------------------------------------------
432
431
  export function priceGateVerdict({
433
432
  amountInWei = null, amountOutWei = null, inDecimals = null, outDecimals = null,
@@ -437,7 +436,7 @@ export function priceGateVerdict({
437
436
  const band = gates && Number.isFinite(Number(gates.stableBandPct)) && Number(gates.stableBandPct) >= 0 ? Number(gates.stableBandPct) : null;
438
437
  const why = [];
439
438
 
440
- // (1) ШАГИ МЕЖДУ СТЕЙБЛАМИ: около 1:1, без оракула.
439
+ // (1) STEPS BETWEEN STABLES: near 1:1, no oracle.
441
440
  const stable = (Array.isArray(stableSteps) ? stableSteps : []).map((s) => {
442
441
  const row = {
443
442
  from: s.from, to: s.to, fee: s.fee, feeLabel: feeTierLabel(s.fee),
@@ -445,8 +444,8 @@ export function priceGateVerdict({
445
444
  quoted: Boolean(s.ok), ok: false, ratio: null, deviationPct: null, why: null,
446
445
  };
447
446
  if (!s.ok) {
448
- // ШАГ СТЕЙБЛ-СТЕЙБЛ НЕ ОТВЕТИЛ - И ЭТО НАЗВАНО, А НЕ ПРОПУЩЕНО: неизмеренная цена не проходит
449
- // проверку, иначе "не проверили" выглядело бы как "цена в порядке".
447
+ // A STABLE-STABLE STEP DID NOT ANSWER - AND THAT IS NAMED, NOT SKIPPED: an unmeasured price does not pass
448
+ // the check, else "we did not check" would look like "the price is fine".
450
449
  row.why = "the 1:1 step could not be quoted (" + (s.why || "no answer") + ")";
451
450
  why.push(row.why);
452
451
  return row;
@@ -470,7 +469,7 @@ export function priceGateVerdict({
470
469
  return row;
471
470
  });
472
471
 
473
- // (2) НЕЯВНАЯ ЦЕНА НАТИВА ПРОТИВ ЭТАЛОНА.
472
+ // (2) THE IMPLIED NATIVE PRICE AGAINST THE REFERENCE.
474
473
  const inUnits = Number.isFinite(Number(inDecimals)) && Number(amountInWei) > 0 ? Number(amountInWei) / 10 ** Number(inDecimals) : null;
475
474
  const outUnits = Number.isFinite(Number(outDecimals)) && Number(amountOutWei) > 0 ? Number(amountOutWei) / 10 ** Number(outDecimals) : null;
476
475
  const valueUsd = inUnits !== null && Number(tokenUsd) > 0 ? inUnits * Number(tokenUsd) : null;
@@ -481,7 +480,7 @@ export function priceGateVerdict({
481
480
  if (nativeBad) {
482
481
  why.push(`the route implies the native coin at $${impliedUsd.toFixed(2)} while the reference price is $${ref.toFixed(2)} - ${deviationPct.toFixed(1)}% apart, and the limit is ${maxDev}%: this is not the market price`);
483
482
  } else if (deviationPct === null) {
484
- // НЕ ПРОВЕРЯЛИ - ЭТО НЕ "ВСЁ ХОРОШО". Причину называем ту, по которой проверить не вышло.
483
+ // NOT CHECKED IS NOT "ALL GOOD". We name the cause that made the check impossible.
485
484
  why.push(ref === null
486
485
  ? "the reference price of the native coin is not known, so the price cannot be checked"
487
486
  : "the price of the token being paid is not known, so the price cannot be checked");
@@ -489,14 +488,14 @@ export function priceGateVerdict({
489
488
  why.push("the price limit is not declared for this route, so the price cannot be checked");
490
489
  }
491
490
 
492
- // ПРОВЕРКА СОСТОЯЛАСЬ - значит НАТИВНАЯ ЧАСТЬ состоялась (эталон есть, лимит объявлен). Шаги
493
- // стейбл-стейбл не входят сюда намеренно: неизмеренный или вышедший из полосы шаг - это уже bad
494
- // (см. выше), а не "не проверено".
491
+ // THE CHECK TOOK PLACE - so the NATIVE PART did (a reference exists, a limit is declared). Stable-stable steps
492
+ // do not enter here on purpose: an unmeasured or out-of-band step is already bad (see above), not "unchecked".
493
+ //
495
494
  const checked = deviationPct !== null && maxDev !== null;
496
495
  const bad = nativeBad || stable.some((r) => !r.ok);
497
- // БЛОКИРУЕТ ЛИ: негодная цена - да (кроме сети с признаком tolerated); непроверенная цена - тоже да,
498
- // и по той же логике, по которой блокирует неизвестная требуемая сумма: "не проверили" нельзя
499
- // показывать как "проверили и всё хорошо". На тестнете не блокирует ни то, ни другое.
496
+ // DOES IT BLOCK: a wrong price - yes (except a network marked tolerated); an unchecked price - also yes, by the
497
+ // same logic that blocks an unknown required amount: "we did not check" must not be shown as "checked and fine".
498
+ // On a testnet neither blocks.
500
499
  const blocks = tolerated ? false : (bad || !checked);
501
500
  return {
502
501
  checked, bad, tolerated: Boolean(tolerated), blocks,
@@ -509,68 +508,68 @@ export function priceGateVerdict({
509
508
  }
510
509
 
511
510
  // ---------------------------------------------------------------------------
512
- // ПОЧЕМУ МАРШРУТА НЕТ - СЛОВАМИ, И ЭТИ СЛОВА - ИЗ ДАННЫХ. Причина лежит в самом объявлении (route.why):
513
- // её писал человек, объявляя маршрут, и она подтверждена замером (tools/check-uniswap-quote.mjs - раздел
514
- // "объявленные маршруты"). Одна и та же формулировка в интерфейсе и в отчёте: разойдясь, они начнут
515
- // объяснять человеку одно, а оператору другое.
516
- // Маршрута в реестре НЕТ ВОВСЕ (токен есть, объявления нет) - это ТОЖЕ честный ответ, а не "не нашли":
517
- // значит, обход не объявляли, и подпись по нему не начинается.
511
+ // WHY THERE IS NO ROUTE - IN WORDS, AND THOSE WORDS COME FROM THE DATA. The cause lies in the declaration itself
512
+ // (route.why): it was written by the person declaring the route, and it is confirmed by measurement. The same
513
+ // wording in the interface and in the report: once they diverge they explain one thing to the person and another
514
+ // to the operator.
515
+ // A route is NOT in the registry at all (the token is there, no declaration) - that is ALSO an honest answer, not
516
+ // "not found": the detour was not declared, and signing on it does not start.
518
517
  export function noRouteWords({ paySymbol, wrappedSymbol, chainName, route = null }) {
519
- // ТЕМПЛЕЙТ, А НЕ СКЛЕЙКА СТРОК: текст тот же, но подстроки `from "` в исходнике больше нет (склейка
520
- // случайно читалась проверкой голых импортов пакета SDK как спецификатор). Правило то же, слова те же.
518
+ // A TEMPLATE, NOT STRING GLUING: the text is the same, but the substring `from "` no longer appears in the source
519
+ // (the gluing was accidentally read by the SDK bare-import check as a specifier). Same rule, same words.
521
520
  const head = `no route from ${paySymbol} to ${wrappedSymbol} on ${chainName}`;
522
521
  const why = route && route.why ? String(route.why) : null;
523
- // В ВИДИМОМ ТЕКСТЕ НЕТ НИ "registry", НИ "DEX_ROUTES": это имена НАШЕГО устройства, а не то, что
524
- // человеку нужно понять. Смысл тот же: маршрут не ищут перебором, его объявляет человек.
522
+ // THE VISIBLE TEXT HAS NEITHER "registry" NOR "DEX_ROUTES": those are names of OUR machinery, not what a person
523
+ // needs to understand. The meaning is the same: a route is not found by brute force, it is declared by a person.
525
524
  return why ? head + ": " + why : head + ": no route for " + paySymbol + " is declared - routes are declared by hand, not searched";
526
525
  }
527
526
 
528
527
 
529
528
  // ---------------------------------------------------------------------------
530
- // ВЕРДИКТ НОГИ: МОЖНО ЛИ ПО НЕЙ ПОДПИСЫВАТЬ (док 45). Две защиты ОДНОЙ СЕМЬИ, и обе БЛОКИРУЮТ подпись:
531
- // * ПОКРЫТИЕ СУММЫ - маршрут обязан дать не меньше, чем требует ордер (сумма эскроу);
532
- // * ЦЕНА - цена маршрута обязана быть рыночной (priceGateVerdict): неверная цена означает то же самое,
533
- // что и недостача, - сделка не сойдётся.
534
- // Отличается от наблюдений именно этим: "не покрывает" и "цена не та" - такие же ФАКТЫ, как "пула нет",
535
- // и они обязаны останавливать человека ДО подписи, а не показываться строкой в разбивке цены. Отсюда
536
- // правило: функция возвращает либо { blocked: false }, либо { blocked: true, kind, reason } - и
537
- // вызывающий ОБЯЗАН считаться с blocked.
538
- // Приговоров о ликвидности здесь нет: "не покрывает на 0.4%" - это число, а не оценка качества пула.
529
+ // THE LEG VERDICT: MAY ONE SIGN ON IT. Two guards OF ONE FAMILY, both BLOCKING signing:
530
+ // * AMOUNT COVERAGE - the route must give at least what the order requires (the escrow amount);
531
+ // * PRICE - the route price must be market (priceGateVerdict): a wrong price means the same as a shortfall -
532
+ // the deal will not settle.
533
+ // It differs from observations exactly in this: "does not cover" and "the price is wrong" are FACTS just like "no
534
+ // pool", and they must stop the person BEFORE signing, not be shown as a line in the price breakdown. Hence the
535
+ // rule: the function returns either { blocked: false } or { blocked: true, kind, reason } - and the caller MUST
536
+ // honour blocked.
537
+ // There are no liquidity verdicts here: "covers 0.4% short" is a number, not a judgement of pool quality.
538
+ //
539
+ // kind: "unquoted" - there is no quote yet (nothing to ask, no waiting allowed: block)
540
+ // "no-route" - no route: causes lie in leg.why (the registry declaration + the chain refusal)
541
+ // "price-unchecked" - the price cannot be checked (no native price reference): block - "we did not
542
+ // check" must not be shown as "checked and fine"
543
+ // "bad-price" - the price is not market: numbers in verdict.price (blocks on a live network)
544
+ // "unchecked" - the required amount is unknown (no XMR/native rate to compute it)
545
+ // "short" - a route exists but the output is LESS than required: here is by how much
546
+ // "bad-price-tolerated" - the price is wrong but the network is marked dexPriceGate: signing is NOT
547
+ // blocked, and the wrongness is NAMED (a testnet: testing the flow, not the market)
548
+ // "covers" - all agrees: signing allowed (the page's other guards stay in force)
539
549
  //
540
- // kind: "unquoted" - котировки ещё нет вовсе (спрашивать нечего, ждать нельзя: блокируем)
541
- // "no-route" - маршрута нет: причины лежат в leg.why (объявление реестра + отказ цепи)
542
- // "price-unchecked" - цену проверить НЕЧЕМ (нет эталона цены натива): блокируем - "не
543
- // проверили" нельзя показывать как "проверили и всё хорошо"
544
- // "bad-price" - цена не рыночная: числа в verdict.price (блокирует на боевой сети)
545
- // "unchecked" - требуемая сумма неизвестна (нет курса XMR/<натив>, чтобы её посчитать)
546
- // "short" - маршрут есть, но выход МЕНЬШЕ требуемой суммы: вот насколько
547
- // "bad-price-tolerated" - цена негодная, но у сети признак dexPriceGate: подпись НЕ блокируется,
548
- // а негодность НАЗВАНА (тестнет: проверяем поток, а не рынок)
549
- // "covers" - всё сходится: подпись разрешена (прочие защиты страницы в силе)
550
+ // COVERAGE NUMBERS ARE RETURNED WHENEVER THEY ARE KNOWN (needWei/getsWei/shortfallWei/covers) - even when signing
551
+ // is locked by price: the person must see WHAT the verdict came from, not only the word "blocked".
550
552
  //
551
- // ЧИСЛА ПОКРЫТИЯ ВОЗВРАЩАЮТСЯ ВСЕГДА, КОГДА ОНИ ИЗВЕСТНЫ (needWei/getsWei/shortfallWei/covers) - даже
552
- // когда подпись заперта ценой: человеку должно быть видно, ИЗ ЧЕГО взялся вердикт, а не только слово
553
- // "заблокировано".
554
553
  //
555
- // МИНИМУМ НА ВЫХОДЕ БУДУЩЕГО СВОПА БЕРЁТСЯ ОТСЮДА ЖЕ - из требуемой суммы эскроу, а не "на глазок"
556
- // (docs 45). Поэтому недостача невозможна по построению, а излишек остаётся пользователю: НИКАКОЙ
557
- // автоматической конвертации остатка в коде нет и не должно появиться - фондирование вносит ровно сумму
558
- // ордера, всё прочее остаётся у человека.
554
+ // THE OUTPUT MINIMUM OF THE FUTURE SWAP COMES FROM HERE TOO - from the required escrow amount, not "by eye". So a
555
+ // shortfall is impossible by construction and any surplus stays with the user: there is NO automatic conversion of
556
+ // the remainder in the code and there must not be - funding deposits exactly the order amount, everything else
557
+ // stays with the person.
559
558
  export function dexLegVerdict(leg, requiredOutWei = null) {
560
559
  if (leg && leg.unwrap) return { blocked: false, kind: "unwrap", reason: null, price: null };
561
560
  if (!leg) return { blocked: true, kind: "unquoted", reason: "the swap leg has not been quoted yet", price: null };
562
561
  if (!leg.ok) return { blocked: true, kind: "no-route", reason: leg.why || "no route", price: null };
563
- // ЦЕНА ПРИХОДИТ ИЗ САМОЙ НОГИ: её считает quoteDexOut тем же ответом цепи, что и выход. Нога без
564
- // проверки цены (её собрал кто-то руками) эту защиту не подтверждает - проверить её здесь нечем,
565
- // поэтому и вердикта о цене у такой ноги нет.
562
+ // THE PRICE COMES FROM THE LEG ITSELF: quoteDexOut computes it from the same chain answer as the output. A leg
563
+ // without a price check (someone assembled it by hand) does not confirm this guard - there is nothing here to
564
+ // check it with, so such a leg has no price verdict either.
566
565
  const price = leg.price || null;
567
566
  const priceBad = Boolean(price && price.bad);
568
567
  const priceUnchecked = Boolean(price && price.checked === false);
569
568
  const priceBlocks = Boolean(price && price.blocks && (priceBad || priceUnchecked));
570
- // ТРЕБУЕМАЯ СУММА ПРИХОДИТ СЮДА АРГУМЕНТОМ, А НЕ БЕРЁТСЯ ИЗ САМОЙ НОГИ: котировка живёт в кеше 20 с, а
571
- // размер ордера человек может поменять в любой момент. Возьми мы требуемую сумму из кешированного
572
- // ответа - вердикт отставал бы от того, что человек видит в форме, и это ровно тот случай, когда
573
- // защита "есть, но не срабатывает".
569
+ // THE REQUIRED AMOUNT COMES IN AS AN ARGUMENT, NOT TAKEN FROM THE LEG ITSELF: the quote lives in a 20 s cache,
570
+ // while the person can change the order size at any moment. If we took the required amount from the cached
571
+ // answer, the verdict would lag behind what the person sees in the form - exactly the case where a guard "exists
572
+ // but does not fire".
574
573
  const need = requiredOutWei !== null && requiredOutWei !== undefined && Number(requiredOutWei) > 0 ? BigInt(requiredOutWei) : null;
575
574
  const gets = BigInt(leg.amountOutWei);
576
575
  const coverage = need === null ? null : {
@@ -584,9 +583,9 @@ export function dexLegVerdict(leg, requiredOutWei = null) {
584
583
  decimals: leg.wrapped ? leg.wrapped.decimals : null,
585
584
  wrappedSymbol: leg.wrapped ? leg.wrapped.symbol : null,
586
585
  };
587
- // ПОРЯДОК ПРИЧИН: сначала "подписывать нельзя вообще" (маршрута нет - выше; цена непроверена; цена не
588
- // та), потом "суммы не хватает", потом "всё сошлось". Он важен: при неверной цене вопрос о покрытии
589
- // числам уже не верится.
586
+ // ORDER OF CAUSES: first "signing is impossible at all" (no route - above; price unchecked; price wrong), then
587
+ // "the amount is short", then "all agrees". It matters: with a wrong price, the coverage numbers can no longer
588
+ // be believed.
590
589
  if (priceBlocks) return { blocked: true, ...base, kind: priceBad ? "bad-price" : "price-unchecked", reason: price.why };
591
590
  if (coverage === null) {
592
591
  return { blocked: true, ...base, kind: "unchecked", reason: "the required escrow amount is not known yet, so coverage cannot be checked" };
@@ -597,27 +596,27 @@ export function dexLegVerdict(leg, requiredOutWei = null) {
597
596
  }
598
597
 
599
598
  // ---------------------------------------------------------------------------
600
- // Главный вход для страницы: сколько натива даст оплата этим токеном ПО ОБЪЯВЛЕННОМУ МАРШРУТУ.
599
+ // The main entry for the page: how much native the payment with this token gives ALONG THE DECLARED ROUTE.
601
600
  //
602
- // token - токен оплаты из реестра (адрес, decimals и цена)
603
- // amountWei - сумма оплаты в его минимальных единицах
604
- // requiredOutWei - СКОЛЬКО НАТИВА ТРЕБУЕТ ОРДЕР (сумма эскроу на этот размер входа). Это НЕ
605
- // наблюдение, а условие выполнимости: по нему считается вердикт "покрывает /
606
- // не покрывает", и "не покрывает" блокирует подпись (см. dexLegVerdict).
607
- // route - ОБЪЯВЛЕННЫЙ маршрут из реестра (DEX_ROUTES[chainId][symbol]). Выбора здесь нет
608
- // и быть не должно: что объявлено - то и котируется, ОДНИМ вызовом. Объявления
609
- // нет или в нём пусто - это честное "маршрута нет" (слова - из route.why), и
610
- // подпись по нему не начинается.
611
- // tokens - токены этой сети: по символу из шага берутся адрес и decimals
612
- // referenceNativeUsd - ЭТАЛОН цены натива в долларах (Chainlink у страницы). Без него проверка цены
613
- // не состоится, и это не повод её пропустить (см. priceGateVerdict).
614
- // gates - пороги проверки цены из реестра (DEX_PRICE_GATE)
601
+ // token - the payment token from the registry (address, decimals and price)
602
+ // amountWei - the payment amount in its smallest units
603
+ // requiredOutWei - HOW MUCH NATIVE THE ORDER REQUIRES (the escrow amount for this input size). This is NOT
604
+ // an observation but a feasibility condition: from it the "covers / does not cover" verdict
605
+ // is computed, and "does not cover" blocks signing (see dexLegVerdict).
606
+ // route - the DECLARED route from the registry (DEX_ROUTES[chainId][symbol]). There is no choice
607
+ // here and there must not be: what is declared is what is quoted, in ONE call. No
608
+ // declaration, or an empty one - an honest "no route" (the words come from route.why), and
609
+ // signing on it does not start.
610
+ // tokens - this network's tokens: the address and decimals are taken by the symbol from the step
611
+ // referenceNativeUsd - the REFERENCE native price in dollars (the page's Chainlink). Without it the price check
612
+ // cannot take place, and that is no reason to skip it (see priceGateVerdict).
613
+ // gates - the price-check thresholds from the registry (DEX_PRICE_GATE)
615
614
  //
616
- // ХАБОВ ЗДЕСЬ БОЛЬШЕ НЕТ: обход через USDC/USDT - это тоже объявленный маршрут, а не поиск в рантайме.
615
+ // THERE ARE NO HUBS HERE ANY MORE: a detour via USDC/USDT is also a declared route, not a runtime search.
617
616
  //
618
- // Возвращает ЛИБО { ok: true, ... } с числами из цепи, ЛИБО { ok: false, why } словами. Числа и
619
- // "почему нет" вместе не возвращаются: показать человеку и то и другое - значит предложить ему
620
- // догадаться, какому из двух ответов верить.
617
+ // Returns EITHER { ok: true, ... } with numbers from the chain, OR { ok: false, why } in words. Numbers and "why
618
+ // not" are not returned together: showing a person both means asking him to guess which of the two answers to
619
+ // believe.
621
620
  // ---------------------------------------------------------------------------
622
621
  export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = null, route = null, tokens = [], referenceNativeUsd = null, gates = null } = {}, opts = {}) {
623
622
  const u = chain && chain.uniswap;
@@ -628,29 +627,29 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
628
627
  const wrapped = u.wrapped;
629
628
  const chainName = (chain && chain.name) || (chain && chain.id) || "this network";
630
629
  if (String(token.address).toLowerCase() === String(wrapped.address).toLowerCase()) {
631
- // Обёрнутый нативный токен меняется на натив НЕ в пуле: это withdraw у WETH9, один к одному.
630
+ // The wrapped native is changed to native NOT in a pool: this is a WETH9 withdraw, one to one.
632
631
  return { ok: false, why: "this is the wrapped native coin: no pool needed, it unwraps 1:1", unwrap: true };
633
632
  }
634
633
  const declared = route || null;
635
- // ПРИЗНАК СЕТИ: негодная цена на тестнете не блокирует (реестр: dexPriceGate у сети). Отсутствие
636
- // признака - блокирует, и это умолчание: молчаливое "сойдёт" на боевой сети недопустимо.
634
+ // THE NETWORK MARK: a wrong price on a testnet does not block (registry: the network's dexPriceGate). A missing
635
+ // mark blocks, and that is the default: a silent "it will do" on a live network is unacceptable.
637
636
  const tolerated = String((chain && chain.dexPriceGate) || "") === "tolerated";
638
- // ОБЪЯВЛЕНИЕ, КОТОРЫМ ПОЛЬЗУЕМСЯ, ВОЗВРАЩАЕТСЯ В ОТВЕТЕ ЦЕЛИКОМ (со ступенями, пулами и датой замера):
639
- // интерфейс и отчёт обязаны называть не только ЧТО получилось, но и ПО ЧЕМУ это объявлено.
637
+ // THE DECLARATION WE USE IS RETURNED WHOLE (with steps, pools and a measurement date): the interface and the
638
+ // report must name not only WHAT came out but WHAT it was declared from.
640
639
  const declaration = declared
641
640
  ? {
642
641
  steps: (Array.isArray(declared.steps) ? declared.steps : []).map((s) => ({ to: s.to, fee: s.fee, pool: s.pool || null, verified: s.verified || null })),
643
642
  why: declared.why || null, unwrap: Boolean(declared.unwrap),
644
643
  }
645
644
  : null;
646
- // (1) МАРШРУТА НЕТ - ЭТО ОТВЕТ ИЗ ДАННЫХ, А НЕ ОШИБКА. Пустой steps значит "объявленного маршрута нет":
647
- // почему его нет - в route.why, и эти слова уходят и в интерфейс, и в отчёт (одни и те же).
645
+ // (1) NO ROUTE IS AN ANSWER FROM THE DATA, NOT AN ERROR. Empty steps means "no declared route": why there is
646
+ // none is in route.why, and those words go to both the interface and the report (the same ones).
648
647
  if (!declared || !Array.isArray(declared.steps) || !declared.steps.length) {
649
648
  if (declared && declared.unwrap) return { ok: false, why: "this is the wrapped native coin: no pool needed, it unwraps 1:1", unwrap: true };
650
649
  return { ok: false, why: noRouteWords({ paySymbol: token.symbol, wrappedSymbol: wrapped.symbol, chainName, route: declared }), declared: true, declaration, steps: [] };
651
650
  }
652
- // (2) ПУТЬ ИЗ ОБЪЯВЛЕННЫХ ШАГОВ. Не собрать объявленное (шаг называет токен, которого в этой сети нет) -
653
- // это ошибка В ДАННЫХ, и называть её надо иначе, чем "маршрута нет": по ней видно, что правки ждёт реестр.
651
+ // (2) A PATH FROM THE DECLARED STEPS. Failing to assemble the declaration (a step names a token this network
652
+ // lacks) is a DATA error and must be named differently from "no route": it shows the registry needs an edit.
654
653
  const path = declaredRoutePath({ token, tokens, wrapped, route: declared });
655
654
  if (!path || path.error) {
656
655
  return {
@@ -665,14 +664,14 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
665
664
  const h = byAddr.get(String(addr).toLowerCase());
666
665
  return h && Number.isFinite(Number(h.decimals)) ? Number(h.decimals) : null;
667
666
  };
668
- // (3) КЕШ НА 20 С ПО СЕТИ, ТОКЕНУ, СУММЕ И ПОДПИСИ ОБЪЯВЛЕНИЯ. Сменив объявление, мы спрашиваем ДРУГОЙ
669
- // маршрут, и старый ответ на новый вопрос не годится. Требуемая сумма в ключ НЕ входит: она меняется от
670
- // размера ордера, а покрытие и цена считаются ниже - каждый раз, из свежих чисел.
667
+ // (3) A 20 s CACHE BY NETWORK, TOKEN, AMOUNT AND DECLARATION SIGNATURE. Changing the declaration asks a
668
+ // DIFFERENT route, and the old answer does not fit the new question. The required amount does NOT enter the
669
+ // key: it changes with the order size, while coverage and price are computed below - every time, from fresh numbers.
671
670
  const key = [chain.id, token.symbol, amount.toString(), routeSignature(declared)].join("|");
672
671
  let base = cached(key);
673
672
  if (base === undefined) {
674
- // ОДИН ВЫЗОВ НА ВЕСЬ ПУТЬ. Цепочки "котировка шага 1, потом шаг 2 от её выхода" здесь нет и быть не
675
- // должно: такая цепочка считает оба шага по одному состоянию пулов и даёт оптимистично неверное число.
673
+ // ONE CALL FOR THE WHOLE PATH. There is no chain of "step 1 quote, then step 2 from its output" here and
674
+ // there must not be: such a chain prices both steps against one pool state and gives an optimistically wrong number.
676
675
  const quote = await quotePath(chain, { route: { tokens: path.tokens, fees: path.fees, symbols: path.symbols, via: null }, amountInWei: amount }, opts);
677
676
  if (!quote.ok) return put(key, { ok: false, why: quote.why, declared: true });
678
677
  base = { ok: true, quote };
@@ -680,10 +679,10 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
680
679
  }
681
680
  if (!base.ok) return { ...base, declaration };
682
681
  const q = base.quote;
683
- // ПУЛЫ МАРШРУТА ЧИТАЮТСЯ ВСЕГДА, КОГДА КОТИРОВКА ЕСТЬ - по одной записи на КАЖДУЮ ногу. Это ФАКТ,
684
- // который человеку нужен рядом с числом: "маршрут ответил столько-то" само по себе не говорит, много
685
- // ли лежит в пулах на его пути. Запасы в wei БЕЗ decimals человеку не читаются, поэтому decimals
686
- // едут рядом - подставлять 18 "по похожести" значило бы однажды показать 6-значный токен как 18-значный.
682
+ // ROUTE POOLS ARE READ WHENEVER A QUOTE EXISTS - one record per leg. This is a FACT the person needs next to
683
+ // the number: "the route answered so much" by itself does not say how much lies in the pools on his path.
684
+ // Reserves in wei WITHOUT decimals are unreadable to a person, so decimals ride alongside - substituting 18
685
+ // "by similarity" would mean one day showing a 6-decimal token as 18-decimal.
687
686
  if (base.hops === undefined) {
688
687
  const hops = [];
689
688
  for (let i = 0; i < path.fees.length; i++) {
@@ -697,24 +696,24 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
697
696
  step: i + 1, from: path.symbols[i], to: path.symbols[i + 1], fee, feeLabel: feeTierLabel(fee),
698
697
  decimals: [dec(from), dec(to)],
699
698
  pool,
700
- // СВЕРКА ОБЪЯВЛЕННОГО АДРЕСА ПУЛА С ЦЕПЬЮ. Если фабрика отвечает другим пулом, объявление
701
- // устарело: в отчёте инструмента это провал, здесь - факт рядом с числом (решает человек).
699
+ // CHECKING THE DECLARED POOL ADDRESS AGAINST THE CHAIN. If the factory answers another pool, the
700
+ // declaration is stale: in the tool's report that is a failure, here a fact next to the number (a person decides).
702
701
  declaredPool,
703
702
  poolMatchesDeclared: pool && declaredPool ? String(pool).toLowerCase() === String(declaredPool).toLowerCase() : null,
704
703
  verified: declaredStep.verified || null,
705
- // СОСТОЯНИЕ ПУЛА ЗДЕСЬ БОЛЬШЕ НЕ ЧИТАЕТСЯ. Три вызова eth_call на каждую ногу (liquidity(),
706
- // balanceOf(tokenIn), balanceOf(tokenOut)) собирались ради одной строки в интерфейсе - liquidity,
707
- // резервы пула, - и та убрана: после ссылки на пул человек смотрит глубину и состояние в обозревателе.
708
- // Сама функция poolState остаётся: её зовёт отчёт инструмента (tools/check-uniswap-quote.mjs), и там
709
- // эти числа нужны оператору. Здесь же они не нужны никому, а страница спрашивает их на каждом обновлении.
704
+ // POOL STATE IS NO LONGER READ HERE. Three eth_call calls per leg (liquidity(), balanceOf(tokenIn),
705
+ // balanceOf(tokenOut)) were gathered for one line in the interface - liquidity, pool reserves - and that
706
+ // line is removed: after a link to the pool a person checks depth and state in the explorer. The poolState
707
+ // function itself stays: the tool's report calls it, and the operator needs those numbers there. Here they
708
+ // are needed by nobody, while the page asks for them on every refresh.
710
709
  });
711
710
  }
712
711
  base.hops = hops;
713
712
  }
714
713
  const hops = base.hops;
715
- // ШАГИ МЕЖДУ ДВУМЯ СТЕЙБЛАМИ ПРОВЕРЯЮТСЯ САМОЙ КОТИРОВКОЙ (около 1:1, без оракула вовсе). Сумма шага -
716
- // сумма маршрута: у объявленных маршрутов такой шаг идёт ПЕРВЫМ, поэтому для него это ровно его сумма
717
- // (и это отмечено полем exact у каждой строки).
714
+ // STEPS BETWEEN TWO STABLES ARE CHECKED BY THE QUOTE ITSELF (near 1:1, with no oracle at all). The step amount
715
+ // is the route amount: in declared routes such a step comes FIRST, so for it this is exactly its amount (and
716
+ // that is marked by the exact field on every row).
718
717
  if (base.stableSteps === undefined) {
719
718
  const stables = (gates && Array.isArray(gates.stables) ? gates.stables : []).map((s) => String(s).toUpperCase());
720
719
  const steps = [];
@@ -731,9 +730,9 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
731
730
  }
732
731
  base.stableSteps = steps;
733
732
  }
734
- // ПРОВЕРКА ЦЕНЫ. Считается КАЖДЫЙ РАЗ, а не кешируется вместе с котировкой: эталон цены натива
735
- // подгружается отдельно и может появиться позже, чем ответ пула - ровно та же причина, по которой
736
- // здесь не кешируется и вердикт покрытия.
733
+ // THE PRICE CHECK. Computed EVERY TIME, not cached with the quote: the native price reference is loaded
734
+ // separately and may appear later than the pool answer - exactly the same reason the coverage verdict is not
735
+ // cached here either.
737
736
  const price = priceGateVerdict({
738
737
  amountInWei: amount, amountOutWei: q.amountOutWei,
739
738
  inDecimals: token.decimals, outDecimals: wrapped.decimals,
@@ -741,19 +740,19 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
741
740
  referenceUsd: referenceNativeUsd,
742
741
  stableSteps: base.stableSteps, tolerated, gates,
743
742
  });
744
- // ПРЕДПОЧТИТЕЛЬНАЯ ФОРМА: "получить РОВНО требуемую сумму, потратив не больше введённого" (док 45).
745
- // Спрашивается тем же путём-победителем, но В ОБРАТНУЮ СТОРОНУ - контракт требует именно так. Ответ
746
- // важен не только числом: он снимает вопрос о разнице сумм, потому что минимум на выходе становится
747
- // равен требуемой сумме по построению. Отказ здесь - тоже ответ: значит ровно столько пул не отдаёт,
748
- // и тогда остаётся обычная форма на вход с минимумом от требуемой суммы.
743
+ // THE PREFERRED SHAPE: "receive EXACTLY the required amount, spending no more than entered". It is asked on the
744
+ // same winning path but IN REVERSE - the contract requires exactly that. The answer matters not only as a
745
+ // number: it removes the question of a difference in amounts, because the output minimum becomes equal to the
746
+ // required amount by construction. A refusal here is also an answer: the pool does not give exactly that much,
747
+ // and then the usual input shape remains, with a minimum from the required amount.
749
748
  let exactOut = null;
750
749
  if (requiredOutWei !== null && requiredOutWei !== undefined && Number(requiredOutWei) > 0) {
751
750
  const eo = await quoteExactOutPath(chain, { route: q, amountOutWei: BigInt(requiredOutWei) }, opts);
752
751
  exactOut = eo.ok
753
752
  ? {
754
753
  ok: true, amountInWei: eo.amountInWei, gasEstimate: eo.gasEstimate,
755
- // ХВАТИТ ЛИ ВВЕДЁННОГО: сравнивается сумма на входе, потому что у этой формы ответ - именно
756
- // "сколько надо отдать". Излишек остаётся пользователю: никакой конвертации остатка в коде нет.
754
+ // IS THE ENTERED AMOUNT ENOUGH: the input amount is compared, because with this shape the answer is
755
+ // exactly "how much to give". Any surplus stays with the user: no conversion of the remainder in the code.
757
756
  enough: BigInt(eo.amountInWei) <= amount,
758
757
  enteredWei: amount.toString(),
759
758
  spareInWei: (BigInt(eo.amountInWei) <= amount ? amount - BigInt(eo.amountInWei) : 0n).toString(),
@@ -761,9 +760,9 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
761
760
  }
762
761
  : { ok: false, why: eo.why };
763
762
  }
764
- // ПОКРЫТИЕ СЧИТАЕТСЯ ЗДЕСЬ, А НЕ БЕРЁТСЯ ИЗ КЕША: пул и сумма входа кешируются на 20 с, а требуемая
765
- // сумма приходит из ордера и может смениться (человек поправил размер XMR) - и тогда покрытие обязано
766
- // пересчитаться, а не остаться прежним. Считается в целых wei.
763
+ // COVERAGE IS COMPUTED HERE, NOT TAKEN FROM THE CACHE: the pool and input amount are cached for 20 s, but the
764
+ // required amount comes from the order and may change (the person adjusted the XMR size) - and then coverage
765
+ // must be recomputed, not left stale. Computed in whole wei.
767
766
  const required = requiredOutWei !== null && requiredOutWei !== undefined && Number(requiredOutWei) > 0
768
767
  ? (() => {
769
768
  const need = BigInt(requiredOutWei);
@@ -782,10 +781,10 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
782
781
  venue: "Uniswap v3",
783
782
  payToken: token.symbol,
784
783
  wrapped,
785
- // ОБЪЯВЛЕННЫЙ МАРШРУТ, КОТОРЫМ СПРОШЕНА ЦЕНА: ступени, их пулы и дата замера - целиком, как в реестре.
786
- // Интерфейс обязан показывать не только ЧТО получилось, но и ПО ЧЕМУ это объявлено.
784
+ // THE DECLARED ROUTE THE PRICE WAS ASKED ON: the steps, their pools and the measurement date - whole, as in the registry.
785
+ // The interface must show not only WHAT came out but WHAT it was declared from.
787
786
  declaredRoute: declaration,
788
- // ПУТЬ - ЭТО ШАГИ С КОМИССИЯМИ, а не одна ступень: у объявленного обхода через стейбл их две.
787
+ // THE PATH IS THE STEPS WITH FEES, not one step: a declared detour via a stable has two.
789
788
  route: { symbols: q.symbols.slice(), fees: q.fees.slice(), pools: path.pools.slice(), via: null, hops: q.fees.length },
790
789
  routeLabel: routeLabel(q),
791
790
  steps: routeSteps(q),
@@ -794,31 +793,31 @@ export async function quoteDexOut(chain, { token, amountWei, requiredOutWei = nu
794
793
  amountInWei: amount.toString(),
795
794
  amountOutWei: q.amountOutWei,
796
795
  gasEstimate: q.gasEstimate,
797
- // ВЕРДИКТ ВЫПОЛНИМОСТИ: покрывает ли маршрут требуемую сумму ордера. null означает "требуемая сумма
798
- // не передана" - и тогда подпись блокируется как непроверенная (см. dexLegVerdict), а не разрешается.
796
+ // THE FEASIBILITY VERDICT: whether the route covers the required order amount. null means "the required amount
797
+ // was not passed" - and then signing is blocked as unverified (see dexLegVerdict), not allowed.
799
798
  required,
800
- // ПРОВЕРКА ЦЕНЫ: проверили ли, похожа ли цена на рыночную, и блокирует ли это подпись на этой сети.
799
+ // THE PRICE CHECK: whether it was done, whether the price looks market, and whether it blocks signing on this network.
801
800
  price,
802
- // ПРЕДПОЧТИТЕЛЬНАЯ ФОРМА (точный выход): сколько надо отдать, чтобы получить ровно требуемую сумму.
803
- // Вопрос о разнице сумм при ней не встаёт - минимум на выходе равен требуемому по построению.
801
+ // THE PREFERRED SHAPE (exact output): how much to give to receive exactly the required amount.
802
+ // The question of a difference in amounts does not arise with it - the output minimum equals the required by construction.
804
803
  exactOut,
805
804
  hopsDetail: hops,
806
- // Одиночная ступень (прежняя форма ответа) - из первой ноги пути. Читать их как "пул всего маршрута"
807
- // нельзя, и это сказано тут: у обхода через хаб ног две.
805
+ // The single step (the former answer shape) - from the first leg of the path. They must not be read as "the
806
+ // whole route's pool", and that is said here: a detour via a hub has two legs.
808
807
  pool: hops.length ? hops[0].pool : null,
809
808
  at: Date.now(),
810
809
  };
811
810
  }
812
811
 
813
812
  // ---------------------------------------------------------------------------
814
- // WETH -> ETH: unwrap у самого WETH9, один к одному, БЕЗ пула.
813
+ // WETH -> ETH: an unwrap at WETH9 itself, one to one, WITHOUT a pool.
815
814
  //
816
- // Отсюда берётся описание строки "unwrap, 1:1". Оценка газа СЮДА НЕ ВХОДИТ: чтобы назвать газ unwrap,
817
- // нужен счёт, который держит WETH и может платить за газ, а у страницы такого счёта нет (и отправлять
818
- // ничего нельзя на этом шаге). Измеренный газ unwrap лежит в отчёте инструмента
819
- // (tools/check-uniswap-quote.mjs: газ измеряется eth_estimateGas на настоящем держателе WETH, которого
820
- // инструмент находит по журналам Transfer). Подставлять это число в интерфейс как "газ этой сделки"
821
- // было бы подменой: оно измерено на чужом счёте и на другой сумме. Поэтому честно: gasEstimate === null.
815
+ // This is where the "unwrap, 1:1" line description comes from. The gas estimate does NOT enter here: to name the
816
+ // unwrap gas one needs an account that holds WETH and can pay for gas, and the page has no such account (and
817
+ // nothing may be sent at this step). The measured unwrap gas lies in the tool's report (the gas is measured via
818
+ // eth_estimateGas on a real WETH holder, which the tool finds from Transfer logs). Substituting that number into
819
+ // the interface as "this deal's gas" would be a fake: it is measured on someone else's account and another
820
+ // amount. So, honestly: gasEstimate === null.
822
821
  // ---------------------------------------------------------------------------
823
822
  export function unwrapLeg(chain) {
824
823
  const u = chain && chain.uniswap;
@@ -829,7 +828,7 @@ export function unwrapLeg(chain) {
829
828
  leg: "unwrap",
830
829
  venue: w.name || w.symbol,
831
830
  wrapped: w,
832
- rate: 1, // один к одному по определению WETH9: withdraw(wad) отдаёт ровно wad нативного
831
+ rate: 1, // one to one by WETH9's definition: withdraw(wad) returns exactly wad of native
833
832
  gasEstimate: null,
834
833
  why: null,
835
834
  };