@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,130 +1,133 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/funding.js for the SDK package.
2
- // Edit the source under www/js, then run: node tools/build-sdk-engine.mjs
3
- // tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
2
+ // Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
4
3
  // Everything below this header is identical to the source.
5
4
 
6
- // Фондирование ордера в контракте-эскроу: настоящие транзакции, а не имитация.
5
+ // Funding an order in the escrow contract: real transactions, not a simulation.
7
6
  //
8
- // Роли в этом потоке и почему так:
9
- // - ордер создаёт и ETH вносит тот, кого в условиях записали `locker` (контракт требует ровно его);
10
- // - забирает по секрету `claimer` - адрес задан в условиях, вызывающий на получателя не влияет;
11
- // - секрет на этом шаге генерируется ЗДЕСЬ и помечен как тестовый: в бою он рождается при трате XMR
12
- // (HTLC), и тогда эта же функция просто получит его снаружи. Больше ничего в форме менять не нужно.
7
+ // Roles and why:
8
+ // - the order is created and the ETH deposited by whoever is recorded as `locker` in the terms (the contract
9
+ // requires exactly them);
10
+ // - the claimer takes by the secret - the address is set in the terms, the caller cannot change the recipient;
11
+ // - the secret at this step is generated HERE and marked as a test one: in production it is born when the XMR is
12
+ // spent (HTLC), and then this same function simply receives it from outside.
13
13
  //
14
- // keccak256 передаётся зависимостью, как в www/js/monero/address.js: библиотеку ключей грузит тот,
15
- // кто вызвал, а не этот модуль. Монеро и Ethereum используют один и тот же Keccak-256, поэтому хеш,
16
- // посчитанный бандлом, совпадает с тем, что проверяет Solidity.
14
+ // keccak256 is passed in as a dependency, as in the address module: the caller loads the key library, not this
15
+ // module. Monero and Ethereum share Keccak-256, so the hash computed by the bundle matches what Solidity checks.
17
16
 
18
17
  import { encodeCreateOrderAndFund, encodeCreateOrderAndFundByDepositor, encodePredict, decodeAddress, ORDER_CREATED_TOPIC } from "./factory.js";
19
- // ВНОСЯЩИЙ ПО ПОДПИСИ (issue #97, части A/C): форма подписи вносящего - из одного места (evm/depositor.js),
20
- // рядом с формой протокола. Заведя проверку формы здесь, мы получили бы вторую запись того же правила.
18
+ // THE DEPOSITOR BY SIGNATURE: the signature shape comes from one place (evm/depositor.js), next to the protocol
19
+ // shape. Checking the shape here would be a second record of the same rule.
21
20
  import { depositorSignatureShape } from "./depositor.js";
22
21
  import { encodeStatus, decodeStatus } from "./escrow.js";
23
- // КОМИССИЯ: ставка, величина и получатель - ЧАСТЬ ПОДПИСАННОЙ КОТИРОВКИ. У фабрики их больше нет (роутер
24
- // удалён), поэтому величина берётся из котировки и сверяется зеркалом формулы. Без котировки фондировать
25
- // нечем: фабрика принимает подписанный набор, а не голые условия ордера.
22
+ // THE FEE: rate, amount and recipient are PART OF THE SIGNED QUOTE. The factory no longer has them (the router was
23
+ // removed), so the amount comes from the quote and is cross-checked by the formula mirror. Without a quote there
24
+ // is nothing to fund with: the factory takes a signed set, not bare order terms.
26
25
  import { feeTermsFromQuote } from "./fees.js";
27
- // ЗАЩИТЫ ПЕРЕД ПОДПИСЬЮ (док 45): сроки ордера и состояние эскроу - чистые функции, потому что проверять
28
- // их на числах, которые их ЛОМАЮТ, можно только тогда, когда они не знают ни про сеть, ни про кошелёк.
26
+ // PRE-SIGNING GUARDS: the order deadlines and the escrow state are pure functions, because they can be checked
27
+ // against the numbers that BREAK them only if they know neither the network nor the wallet.
29
28
  import { deadlineVerdict, escrowStateVerdict } from "../core/preSignGuards.js";
30
29
  import { SIGNING_GUARDS } from "../core/config.js";
31
30
 
32
31
  const ATOMIC = 1_000_000_000_000_000_000n; // 1 ETH = 10^18 wei
33
32
 
34
- // Тестовый секрет: 32 байта из криптографического генератора браузера.
33
+ // A test secret: 32 bytes from the browser's crypto generator.
35
34
  export function newSecret() {
36
35
  const bytes = new Uint8Array(32);
37
36
  crypto.getRandomValues(bytes);
38
37
  return "0x" + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
39
38
  }
40
39
 
41
- // Условия ордера. Время - абсолютное (unix), как требует контракт.
40
+ // Order terms. Time is absolute (unix), as the contract requires.
42
41
  //
43
- // ПОСЫЛКИ ЗДЕСЬ БОЛЬШЕ НЕТ (док 27): вместо неё в ордер идут ОБА обязательства на половины и ОБЕ
44
- // публичные точки ed25519. Функция ТРЕБУЕТ их и падает, если их нет, - и это осознанно: молча
45
- // отправить ордер с отсутствующими полями значило бы создать эскроу, по которому нельзя ни забрать,
46
- // ни доказать связь половины с адресом. Пусть лучше откажет здесь и громко.
42
+ // NO PREIMAGE HERE ANY MORE: instead, BOTH half commitments and BOTH ed25519 public points go into the order. The
43
+ // function REQUIRES them and fails if they are missing - on purpose: silently sending an order with missing fields
44
+ // would create an escrow that can neither be claimed nor have its half-to-address link proven. Better to refuse
45
+ // here and loudly.
47
46
  //
48
- // СРОКА ДВА, И ОНИ НЕ СОВПАДАЮТ: readyBy - до него внёсший отмечает готовность ("XMR вижу"), t1 - с
49
- // него забор закрыт, а вернуть может кто угодно. Совпадение вернуло бы окно, где оба пути валидны
50
- // одновременно, и спор решал бы газ (док 26).
47
+ // TWO DEADLINES THAT DO NOT COINCIDE: until readyBy the depositor marks readiness ("I see the XMR"), from t1 a
48
+ // claim is closed and anyone may refund. Coincidence would restore a window where both paths are valid and gas
49
+ // would decide the dispute.
51
50
  export function testOrderTerms({ locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker, amountWei, salt, claimWindowSeconds = 3600, readyWindowSeconds = 14400, readyBy, t1 }) {
52
- // ТОЧКА ПРОСМОТРА В СПИСКЕ ОБЯЗАТЕЛЬНЫХ: без неё адрес Monero не собрать из полей ордера, и узнал бы
53
- // об этом не тот, кто ошибся, а забирающий - уже после фондирования.
51
+ // THE VIEW POINT IS ON THE REQUIRED LIST: without it the Monero address cannot be assembled from the order
52
+ // fields, and the one who learns about it would be the claimer, after funding.
54
53
  for (const [k, v] of Object.entries({ commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker })) {
55
- if (!v) throw new Error("не задано условие ордера: " + k);
54
+ if (!v) throw new Error("order term not set: " + k);
56
55
  }
57
56
  const now = Math.floor(Date.now() / 1000);
58
- // СРОКИ МОЖНО ЗАДАТЬ ГОТОВЫМИ, И ЭТО НЕ УДОБСТВО, А НЕОБХОДИМОСТЬ. Они входят в канонический контекст
59
- // ордера, к которому привязано доказательство, а доказательство считается ДО фондирования. Если сроки
60
- // считать здесь и сейчас, они окажутся посчитаны ПОСЛЕ, то есть доказательство будет привязано к одним
61
- // срокам, а в контракт уйдут другие. Поэтому вызывающий решает их заранее и передаёт сюда.
57
+ // DEADLINES MAY BE PASSED IN READY-MADE, AND THAT IS NECESSITY, NOT CONVENIENCE. They enter the canonical order
58
+ // context the proof is bound to, and the proof is computed BEFORE funding. Computing the deadlines here and now
59
+ // would put them AFTER the proof, so the proof would bind one set of deadlines while the contract gets another.
60
+ // The caller therefore fixes them in advance and passes them here.
62
61
  const hasReady = readyBy !== undefined && readyBy !== null && readyBy !== "";
63
62
  const hasT1 = t1 !== undefined && t1 !== null && t1 !== "";
64
63
  const readyByValue = hasReady ? Number(readyBy) : now + readyWindowSeconds;
65
- // t1 - ОКНО ПОСЛЕ readyBy, а не "от сейчас" (issue #88): плоская формула при 4-часовом окне готовности
66
- // давала t1 раньше readyBy. Тот же смысл, что в sdk/src/swap-flow.mjs и rfq/reverseOrder.mjs.
64
+ // t1 is a WINDOW AFTER readyBy, not "from now": the flat formula with a 4-hour readiness window gave t1 earlier
65
+ // than readyBy. Same meaning as in the package's swap flow and the node's reverse order.
67
66
  const t1Value = hasT1 ? Number(t1) : readyByValue + claimWindowSeconds;
68
- // ИНВАРИАНТ ОРДЕРА ПРОВЕРЯЕТСЯ ЗДЕСЬ: до readyBy отмечают готовность, с t1 забор закрыт. Совпадение или
69
- // перестановка дали бы окно, где оба пути валидны одновременно, и спор решал бы газ (док 26).
70
- if (!(t1Value > readyByValue)) throw new Error("срок расчёта должен быть позже срока готовности: t1=" + t1Value + ", readyBy=" + readyByValue);
71
- // СРОКИ ПРОВЕРЯЮТСЯ ЗДЕСЬ НЕ ТОЛЬКО НА "ПОЗЖЕ": ордер со сроком в прошлом или со слишком тесным окном -
72
- // это ордер, по которому нельзя ни отметить готовность, ни забрать, то есть деньги, повисшие до
73
- // возврата. Правило (и числа порогов) живёт в модуле защит (core/preSignGuards.js), здесь - отказ словами.
67
+ // THE ORDER INVARIANT IS CHECKED HERE: readiness is marked before readyBy, a claim is closed from t1.
68
+ // Coincidence or a swap would give a window where both paths are valid and gas would decide the dispute.
69
+ if (!(t1Value > readyByValue)) throw new Error("the settlement deadline must be later than the readiness deadline: t1=" + t1Value + ", readyBy=" + readyByValue);
70
+ // DEADLINES ARE CHECKED NOT ONLY FOR "LATER": an order with a past deadline or too tight a window can neither be
71
+ // marked ready nor claimed - money hanging until refund. The rule (and the thresholds) live in the guard module;
72
+ // here it is a refusal in words.
74
73
  const deadlines = deadlineVerdict({ nowSec: now, readyBy: readyByValue, t1: t1Value, ...SIGNING_GUARDS });
75
- if (deadlines.blocks) throw new Error("сроки ордера не годны: " + deadlines.reason);
74
+ if (deadlines.blocks) throw new Error("order deadlines are not usable: " + deadlines.reason);
76
75
  return {
77
76
  locker,
78
77
  claimer,
79
- commitHalfLocker, // обязательство половины внёсшего
80
- commitHalfClaimer, // обязательство половины забирающего
81
- edPointLocker, // P_a: по ней вторая сторона собирает общий адрес
78
+ commitHalfLocker, // commitment to the depositor's half
79
+ commitHalfClaimer, // commitment to the claimer's half
80
+ edPointLocker, // P_a: the other side assembles the joint address from it
82
81
  edPointClaimer, // P_b
83
- edViewPointLocker, // V_a: половина ПРОСМОТРА внёсшего (нужна для адреса Monero)
82
+ edViewPointLocker, // V_a: the depositor's VIEW half (needed for the Monero address)
84
83
  amount: String(amountWei),
85
- // ОКНО ГОТОВНОСТИ - ЭТО ПРО MONERO, А НЕ ПРО EVM. Мейкер залочит XMR только после фондирования, и
86
- // отметить готовность осмысленно лишь тогда, когда XMR реально подтверждён: блоки Monero идут по
87
- // Пуассону, среднее до первого подтверждения около 2,5 минут, но один медленный блок легко даёт
88
- // 10-20. Плюс шаги самого мейкера: кошелёк, комиссия, отправка. На пятнадцати минутах хвост не
89
- // помещался, поэтому 30 - по замеру, а не «на всякий случай». Замеры и источники: док 26.
84
+ // THE READINESS WINDOW IS ABOUT MONERO, NOT EVM. The maker locks the XMR only after funding, and marking
85
+ // readiness makes sense only once the XMR is really confirmed: Monero blocks are Poisson, about 2.5 minutes on
86
+ // average to the first confirmation, but one slow block easily gives 10-20. Plus the maker's own steps: wallet,
87
+ // fee, send. Fifteen minutes did not fit the tail, so 30 - by measurement, not "just in case".
90
88
  readyBy: String(readyByValue),
91
- t1: String(t1Value), // граница расчёта: час по умолчанию
92
- // СОЛЬ ВХОДИТ В УСЛОВИЯ, А НЕ ДОБАВЛЯЕТСЯ К НИМ. Она определяет адрес эскроу (CREATE2), и predict
93
- // обязан считать адрес ПО ТЕМ ЖЕ полям, что уедут в создание. Раньше её добавляли только в вызов
94
- // создания, а predict шёл без соли - и падал на её проверке в кодировщике calldata. Принцип файла
95
- // («один источник полей») нарушался ровно на этом поле.
89
+ t1: String(t1Value), // settlement boundary: one hour by default
90
+ // THE SALT ENTERS THE TERMS, IT IS NOT ADDED TO THEM. It determines the escrow address (CREATE2), and predict
91
+ // must compute the address FROM THE SAME FIELDS that go into creation. It used to be added only to the creation
92
+ // call while predict ran without it - and failed its own check in the calldata encoder. "One source of fields"
93
+ // was broken on exactly this field.
96
94
  salt,
97
95
  };
98
96
  }
99
97
 
100
- // Сумма в wei из человеческого значения (строка) и decimals токена.
98
+ // Amount in wei from a human value (string) and the token decimals.
101
99
  export function toWei(human, decimals = 18) {
102
100
  const [intPart, fracPart = ""] = String(human).split(".");
103
101
  const frac = (fracPart + "0".repeat(decimals)).slice(0, decimals);
104
102
  return BigInt(intPart || "0") * 10n ** BigInt(decimals) + BigInt(frac || "0");
105
103
  }
106
104
 
107
- // ГАЗ ВЫСТАВЛЯЕМ САМИ И С ЗАПАСОМ - ЭТО ЛЕЧЕНИЕ КОНКРЕТНОЙ ОШИБКИ, А НЕ ПЕРЕСТРАХОВКА.
108
- // createOrderAndFund уходит ОДНОЙ транзакцией, но кошелёк оценивает её по адресу, где эскроу ещё НЕ создан:
109
- // вместо создания клона и записи взноса он считает обычный перевод (21000 плюс данные). Оценку кошелька
110
- // здесь доверять нельзя, поэтому лимит задаём явно, с запасом от ИЗМЕРЕННОГО значения.
111
- // ИЗМЕРЕНО: полный createOrderAndFund после перехода на клоны EIP-1167 - 315 530 газа (у прежнего целого
112
- // контракта было 1 190 683); замер и методика - в описании PR в репозитории контрактов. Лимит примерно
113
- // вдвое выше измеренного: недобор стоит человеку подписи, а лишний газ в лимите почти ничего не стоит.
114
- const GAS_CREATE_ORDER_AND_FUND = 700_000n;
115
- // ОТДЕЛЬНОЙ КОНСТАНТЫ «СОЗДАНИЕ БЕЗ ВЗНОСА» НЕТ: целого контракта больше не создаётся - эскроу каждого
116
- // ордера есть клон EIP-1167 одной общей реализации, условия лежат в коде клона, а фабрика умеет только
117
- // createOrderAndFund. GAS_LOCK УБРАН вместе с вызовом lock(bytes): взнос идёт в той же транзакции.
105
+ // WE SET THE GAS OURSELVES, WITH A MARGIN - A CURE FOR A REAL BUG, NOT CAUTION.
106
+ // createOrderAndFund goes as ONE transaction, but the wallet estimates it at an address where the escrow does NOT
107
+ // exist yet: instead of cloning and recording the deposit it counts a plain transfer (21000 plus data). The wallet
108
+ // estimate cannot be trusted here, so the limit is set explicitly, with a margin over the MEASURED value.
109
+ // MEASURED: a full createOrderAndFund after the move to EIP-1167 clones - 315 530 gas (the old whole contract was
110
+ // 1,190,683). The limit is about twice the measured value: a shortfall costs the person a signature, while spare
111
+ // gas in a limit costs almost nothing.
112
+ // EXPORTED ON PURPOSE: the buyer's gas reserve (evm/gasReserve.js) must account for THIS deposit on the path
113
+ // where the wallet holds no native of its own - there the deposit is paid AFTER the swap, not before it, so
114
+ // its gas is part of what the wallet has to be able to afford. One constant, one source (the check ties the
115
+ // reserve's copy to this declaration).
116
+ export const GAS_CREATE_ORDER_AND_FUND = 700_000n;
117
+ // THERE IS NO SEPARATE "CREATE WITHOUT A DEPOSIT" CONSTANT: no whole contract is created any more - each order's
118
+ // escrow is an EIP-1167 clone of one shared implementation, the terms live in the clone's code, and the factory
119
+ // only knows createOrderAndFund. The old lock(bytes) call and its gas constant are gone: the deposit rides in the
120
+ // same transaction.
118
121
 
119
- // СОБЫТИЕ OrderCreated: topic0 посчитан по фактической подписи (и сверяется проверкой watchtower),
120
- // topics = [topic0, escrow, locker, claimer]; в data - оба обязательства, обе точки, сумма, readyBy, t1, соль.
121
- // ТЕМА СОБЫТИЯ БЕРЁТСЯ ИЗ ОДНОГО МЕСТА - из factory.js, а не заводится здесь второй копией.
122
- // Здесь она УЖЕ БЫЛА второй копией, и это дало настоящий дефект: после смены состава полей OrderCreated
123
- // копия осталась прежней, событие по ней не находилось, и адрес эскроу молча падал в предсказание - то
124
- // есть проверка "адрес из квитанции" перестала работать, не сказав об этом ни слова.
122
+ // The OrderCreated event: topic0 is computed from the actual signature (also checked by the watchtower);
123
+ // topics = [topic0, escrow, locker, claimer]; data carries both commitments, both points, the amount, readyBy, t1,
124
+ // the salt. THE EVENT TOPIC COMES FROM ONE PLACE - the factory module, not a second copy here. It USED to be a
125
+ // second copy here, which caused a real defect: after the OrderCreated field set changed, the stale copy failed to
126
+ // find the event and the escrow address silently fell back to the prediction - so the "address from the receipt"
127
+ // check stopped working without a word.
125
128
 
126
- // ФАКТИЧЕСКИЙ адрес эскроу из квитанции. Возвращает null, если события нет (транзакция не в блоке или
127
- // фабрика другая) - тогда вызывающий код обязан сказать об этом вслух, а не подставить догадку.
129
+ // The ACTUAL escrow address from the receipt. Returns null when the event is absent (the transaction is not in a
130
+ // block, or the factory is another) - then the caller must say so aloud rather than substitute a guess.
128
131
  export function escrowFromReceipt(receipt) {
129
132
  if (!receipt || !Array.isArray(receipt.logs)) return null;
130
133
  for (const log of receipt.logs) {
@@ -137,7 +140,7 @@ export function escrowFromReceipt(receipt) {
137
140
  return null;
138
141
  }
139
142
 
140
- // Ждём квитанцию: сразу после подписи её ещё нет, и это нормально. Ждать бесконечно нельзя - говорим честно.
143
+ // Wait for a receipt: right after signing there is none, which is normal. Waiting forever is not allowed - we say so honestly.
141
144
  async function waitReceipt(receiptReader, hash, { tries = 12, delayMs = 1500 } = {}) {
142
145
  for (let i = 0; i < tries; i += 1) {
143
146
  const r = await receiptReader({ hash }).catch(() => null);
@@ -147,113 +150,108 @@ async function waitReceipt(receiptReader, hash, { tries = 12, delayMs = 1500 } =
147
150
  return null;
148
151
  }
149
152
 
150
- // Полный путь фондирования. Шаги возвращаются наружу, чтобы интерфейс мог показать каждый:
151
- // predict - бесплатный вызов, createOrder и lock - две транзакции в кошельке пользователя.
153
+ // The full funding path. Steps are returned outward so the interface can show each one: predict is a free call,
154
+ // createOrder and lock are transactions in the user's wallet.
152
155
  export async function fundOrder({ factory, locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker, salt: providedSalt, amountWei, amountLabel, claimWindowSeconds, readyWindowSeconds, readyBy, t1, quote, depositorSignature = null, call, send, receipt, onStep }) {
153
- if (!factory) throw new Error("не задан адрес фабрики эскроу для этой сети");
154
- if (!locker) throw new Error("не подключён EVM-кошелёк: некому вносить средства");
155
- if (!claimer) throw new Error("не задан получатель ордера (claimer)");
156
+ if (!factory) throw new Error("no escrow factory address set for this network");
157
+ if (!locker) throw new Error("no EVM wallet connected: there is nobody to deposit");
158
+ if (!claimer) throw new Error("the order recipient (claimer) is not set");
156
159
  if (locker.toLowerCase() === claimer.toLowerCase()) {
157
- throw new Error("locker и claimer совпадать не могут: контракт такой ордер отклонит");
160
+ throw new Error("locker and claimer must not coincide: the contract rejects such an order");
158
161
  }
159
- // ПОДПИСАННАЯ КОТИРОВКА ОБЯЗАТЕЛЬНА: она несёт комиссию, адрес реестра и провайдера, без которых ордер
160
- // создать нельзя. Раньше комиссию спрашивали у фабрики - теперь у неё спрашивать нечего; отсутствие
161
- // котировки - это НАЗВАННЫЙ отказ, а не молчаливое фондирование не той суммы.
162
- if (!quote || typeof quote !== "object") throw new Error("нужна подписанная котировка: фабрика принимает котировку, а не голые условия ордера");
162
+ // A SIGNED QUOTE IS MANDATORY: it carries the fee, the registry address and the provider, without which the
163
+ // order cannot be created. The fee used to be asked of the factory - now there is nothing to ask; a missing
164
+ // quote is a NAMED refusal, not a silent funding of the wrong amount.
165
+ if (!quote || typeof quote !== "object") throw new Error("a signed quote is required: the factory takes a quote, not bare order terms");
163
166
 
164
- // ОБЯЗАТЕЛЬСТВА И ТОЧКИ ОБЯЗАТЕЛЬНЫ, и проверяются здесь, а не в глубине: без них ордер создастся, но
165
- // ни забрать по нему, ни доказать связь половины с Monero-адресом будет нельзя. Пусть падает громко.
166
- // Соль определяет адрес эскроу и обязана существовать ДО сборки условий: условия уходят и в predict.
167
- const salt = providedSalt || newSecret(); // соль CREATE2: детерминирует адрес эскроу
167
+ // COMMITMENTS AND POINTS ARE MANDATORY, and checked here and not deeper: without them the order is created but
168
+ // can neither be claimed nor have its half-to-Monero-address link proven. Let it fail loudly.
169
+ // The salt determines the escrow address and must exist BEFORE the terms are assembled: the terms also go to predict.
170
+ const salt = providedSalt || newSecret(); // CREATE2 salt: determines the escrow address
168
171
  const terms = testOrderTerms({
169
172
  locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker,
170
173
  amountWei, salt, claimWindowSeconds, readyWindowSeconds, readyBy, t1,
171
174
  });
172
- // СОЛЬ БЕРЁМ ИЗ СДЕЛКИ, ЕСЛИ ОНА ТАМ УЖЕ ЕСТЬ. Соль определяет адрес эскроу (CREATE2), поэтому при
173
- // повторном запуске фондирования новая соль создавала НОВЫЙ эскроу, а запись сделки оставалась с прежним
174
- // адресом - интерфейс, ссылка на обозреватель и экран прогресса показывали адрес, которого на цепи нет.
175
- // Побочная польза не меньше основной: повтор с той же солью попадает в ТОТ ЖЕ адрес и, если ордер уже
176
- // профинансирован, просто не проходит - вместо тихого создания второго эскроу.
177
- // Параметры здесь деструктурированы ПО ИМЕНАМ, поэтому обращаться к order нельзя: переменной order в
178
- // этой функции не существует. Первая редакция этой строки содержала order.salt - и фондирование падало
179
- // с "order is not defined", причём падало ПОСЛЕ моей же правки, которая спрятала причину в логе.
180
- // Локальную переменную назвал salt, входной параметр принят как providedSalt: иначе получилась бы
181
- // "мёртвая зона" const salt = salt || ... - обращение к себе до объявления.
182
- // 1) адрес эскроу виден ДО создания - это и есть смысл фабрики
175
+ // TAKE THE SALT FROM THE DEAL IF IT ALREADY HAS ONE. The salt determines the escrow address (CREATE2), so a
176
+ // re-run of funding with a new salt created a NEW escrow while the deal record kept the old address - the page,
177
+ // the explorer link and the progress screen showed an address not on chain. Re-using the salt also means a retry
178
+ // hits the SAME address and, if the order is already funded, simply fails instead of quietly creating a second
179
+ // escrow. The parameters are destructured, so there is no `order` variable here; the local is named `salt` and
180
+ // the input `providedSalt` to avoid a temporal-dead-zone self-reference.
181
+ // 1) the escrow address is visible BEFORE creation - that is the point of the factory
183
182
  onStep?.("computing the escrow address");
184
- // ПОЛЯ ПРЕДСКАЗАНИЯ ОБЯЗАНЫ СОВПАДАТЬ С ПОЛЯМИ СОЗДАНИЯ БАЙТ В БАЙТ - адрес есть хеш кода создания.
185
- // Здесь это гарантируется тем, что и predict, и createOrderAndFund собираются из ОДНИХ И ТЕХ ЖЕ terms:
186
- // разойтись они могут только если править одно из двух мест врозь. Раньше расхождение уже случалось
187
- // (в предсказание не попадала посылка), и лечится оно не аккуратностью, а общим источником полей.
188
- // ПОЛЯ ВНОСИВШЕГО ЕДУТ ОТДЕЛЬНЫМИ АРГУМЕНТАМИ ФАБРИКИ (их нет в подписанном наборе).
183
+ // THE PREDICTION FIELDS MUST MATCH THE CREATION FIELDS BYTE FOR BYTE - the address is a hash of the creation
184
+ // code. Here that is guaranteed because both predict and createOrderAndFund are built from THE SAME terms: they
185
+ // can diverge only if one of the two places is edited alone. They already did once (the preimage was missing from
186
+ // the prediction); the cure is a shared source of fields, not care.
187
+ // THE DEPOSITOR'S FIELDS GO AS SEPARATE FACTORY ARGUMENTS (they are not in the signed set).
189
188
  const quoteEnc = { ...quote, commitHalfLocker: terms.commitHalfLocker, edPointLocker: terms.edPointLocker, edViewPointLocker: terms.edViewPointLocker };
190
- // КОТИРОВКА И УСЛОВИЯ - ОБ ОДНОМ: расхождение уводит адрес эскроу и подписывает не тот ордер.
189
+ // QUOTE AND TERMS ARE ABOUT ONE THING: a divergence moves the escrow address and signs the wrong order.
191
190
  for (const name of ["locker", "claimer", "amount", "readyBy", "t1", "salt"]) {
192
191
  if (String(quote[name]).toLowerCase() !== String(terms[name]).toLowerCase()) {
193
- throw new Error("котировка и условия ордера расходятся по полю " + name);
192
+ throw new Error("the quote and the order terms diverge on field " + name);
194
193
  }
195
194
  }
196
195
  const predictedRaw = await call({ to: factory, data: encodePredict(quoteEnc) });
197
196
  const escrow = decodeAddress(predictedRaw);
198
197
  if (!/^0x[0-9a-fA-F]{40}$/.test(escrow) || /^0x0{40}$/i.test(escrow)) {
199
- throw new Error("фабрика вернула некорректный адрес эскроу: " + String(predictedRaw).slice(0, 40));
198
+ throw new Error("the factory returned an invalid escrow address: " + String(predictedRaw).slice(0, 40));
200
199
  }
201
200
 
202
- // 1б) СОСТОЯНИЕ ЭСКРОУ ПРОВЕРЯЕТСЯ ДО ПОДПИСИ, А НЕ ПОСЛЕ НЕЁ. Адрес уже известен (predict выше), и
203
- // если по нему лежит зафондированный, забранный или возвращённый ордер, подписывать нельзя: контракт
204
- // запрещает второе внесение (AlreadyFunded), а новая подпись (с новой солью) создала бы ВТОРОЙ ордер
205
- // вместо прежнего - деньги ушли бы в него, а не в тот, о котором человек думает (док 45).
201
+ // 1b) THE ESCROW STATE IS CHECKED BEFORE SIGNING, NOT AFTER. The address is known (predict above), and if it
202
+ // already holds a funded, claimed or refunded order, signing is not allowed: the contract forbids a second
203
+ // deposit (AlreadyFunded), while a new signature (with a new salt) would create a SECOND order instead of the old
204
+ // one - the money would go there, not where the person thinks.
206
205
  //
207
- // ПУСТОЙ ОТВЕТ - ЭТО "ОРДЕРА НЕТ", А НЕ "ПРОВЕРИТЬ НЕ УДАЛОСЬ": по адресу, которого ещё не существует,
208
- // eth_call возвращает пусто (так же это читается в sdk/src/swap-flow.mjs: orderDeadlines). А вот ОШИБКА
209
- // вызова - другое состояние, и она запирает: "не проверили" нельзя считать за "всё хорошо".
206
+ // AN EMPTY ANSWER MEANS "NO ORDER", NOT "COULD NOT CHECK": for an address that does not exist yet eth_call
207
+ // returns empty. A CALL ERROR is a different state and it blocks: "did not check" must not count as "all good".
210
208
  onStep?.("checking the order is not already on chain");
211
209
  let stateRead = null;
212
210
  try {
213
211
  const raw = await call({ to: escrow, data: encodeStatus() });
214
212
  const body = String(raw || "").replace(/^0x/, "");
215
- // Ответ escrow обязан быть четырьмя словами status(): короче - значит по этому адресу нет контракта,
216
- // исполняющего status(), то есть ордера там нет и быть не может.
213
+ // The escrow answer must be four status() words: shorter means no contract executing status() at this
214
+ // address, so there is no order there and cannot be one.
217
215
  stateRead = body.length >= 256 ? { status: decodeStatus(raw) } : { status: null };
218
216
  } catch (e) {
219
217
  stateRead = { error: String((e && e.message) || e) };
220
218
  }
221
219
  const escrowState = escrowStateVerdict({ status: stateRead.status, error: stateRead.error, address: escrow });
222
- if (escrowState.blocks) throw new Error("подпись не начата: " + escrowState.reason);
220
+ if (escrowState.blocks) throw new Error("signature not started: " + escrowState.reason);
223
221
 
224
- // 2) ОДНА ТРАНЗАКЦИЯ: создание ордера ВМЕСТЕ с внесением денег.
222
+ // 2) ONE TRANSACTION: creating the order TOGETHER with the deposit.
225
223
  //
226
- // Было двумя шагами (createOrder, затем lock), и у этого было два дефекта, оба наблюдались живьём:
227
- // кошелёк оценивал газ второй транзакции, когда кода по адресу ещё не было (лимит выходил как для
228
- // обычного перевода ETH, и она падала), а отказ фондирования оставлял на цепи пустой контракт.
229
- // Теперь отказ откатывает ВМЕСТО с созданием, а подпись нужна одна. Locker в этой функции передать
230
- // нельзя - им становится отправитель, поэтому адрес возврата всегда принадлежит тому, кто заплатил.
231
- // СУММУ ПОКАЗЫВАЕМ В ETH, А НЕ В WEI, и берём её готовой строкой от вызывающего: форматирование
232
- // сумм в проекте живёт в одном месте (fmt.amount), а второй форматтер здесь разошёлся бы с ним.
233
- // Надпись короткая намеренно: она выводится НА КНОПКУ и обязана помещаться в одну строку.
234
- // КОМИССИЯ ВХОДИТ В VALUE, И ЭТО ГЛАВНОЕ СЛЕДСТВИЕ ЕЁ ВВЕДЕНИЯ. Контракт требует внести РОВНО
235
- // `amount + fee` (NinseiEscrow.sol: fund и payable-конструктор), поэтому фондирование на сумму ордера
236
- // отвергается - причём отвергается уже после подписи, потому что проверка живёт в контракте.
237
- // Сумма ордера (`amount` в условиях) НЕ меняется: ровно её получит забирающий, ровно она входит в слепок
238
- // условий. Комиссия прибавляется сверху и удержится при заборе; при возврате вернётся вместе с суммой.
224
+ // It used to be two steps (createOrder, then lock) with two defects, both seen live: the wallet estimated the
225
+ // gas of the second transaction before code existed at the address (the limit came out as for a plain ETH
226
+ // transfer and it failed), and a funding failure left an empty contract on chain. Now a failure rolls back
227
+ // TOGETHER with creation, and only one signature is needed. Locker cannot be passed to this function - the sender
228
+ // becomes it, so the refund address always belongs to whoever paid.
229
+ // THE AMOUNT IS SHOWN IN ETH, NOT WEI, and taken as a ready string from the caller: amount formatting lives in
230
+ // one place in the project, and a second formatter here would diverge from it. The label is short on purpose: it
231
+ // is printed ON A BUTTON and must fit one line.
232
+ // THE FEE IS PART OF VALUE, AND THAT IS THE MAIN CONSEQUENCE OF ITS INTRODUCTION. The contract requires
233
+ // depositing EXACTLY `amount + fee`, so funding for the order amount alone is rejected - and rejected after
234
+ // signing, because the check lives in the contract. The order amount (`amount` in the terms) does NOT change: it
235
+ // is exactly what the claimer receives and what enters the terms hash. The fee is added on top, taken at claim,
236
+ // returned on refund.
239
237
  onStep?.("reading the escrow fee from the signed quote");
240
- // КОМИССИЮ БЕРЁМ ИЗ КОТИРОВКИ И СВЕРЯЕМ ЗЕРКАЛОМ. Направление уже учтено: покупка - amount + fee (сверху),
241
- // продажа - amount (комиссия вычтется из выплаты забирающему).
238
+ // THE FEE COMES FROM THE QUOTE AND IS MIRROR-CHECKED. Direction is already accounted for: a buy is amount + fee
239
+ // (on top), a sell is amount (the fee is deducted from the claimer's payout).
242
240
  const fee = feeTermsFromQuote(quote);
243
241
  const totalValueWei = fee.totalWei;
244
242
  const feeWords = fee.feeWei > 0n
245
243
  ? " (order " + amountWei + " + fee " + fee.feeWei + ")"
246
244
  : "";
247
245
  onStep?.("locking " + (amountLabel || "funds") + feeWords + " in one transaction - confirm in wallet");
248
- // КАКОЙ ДВЕРЬЮ ФАБРИКИ ИДЁТ ВНЕСЕНИЕ, И ЭТО НЕ УКРАШЕНИЕ. Подпись вносящего есть - вносим путём части A:
249
- // `createOrderAndFundByDepositor` восстанавливает вносившего ИЗ ПОДПИСИ над дайджестом котировки, поэтому
250
- // транзакцию вправе отправить кто угодно (релейщик, наш сервис), а роль `locker` (право вернуть, право
251
- // отметить готовность) остаётся у подписавшего. Подписи нет - прежний `createOrderAndFund`: вносит и
252
- // отправляет один и тот же кошелёк, прямой обмен работает ровно как раньше. Менять условия ордера
253
- // отправителю нельзя ни на одном пути - обе подписи покрывают ОДИН И ТОТ ЖЕ дайджест котировки.
246
+ // WHICH FACTORY DOOR THE DEPOSIT GOES THROUGH, AND IT IS NOT DECORATION. If a depositor signature is present, we
247
+ // deposit via part A: createOrderAndFundByDepositor recovers the depositor FROM THE SIGNATURE over the quote
248
+ // digest, so anyone may send the transaction (a relayer, our service) while the `locker` role (right to refund,
249
+ // right to mark readiness) stays with the signer. Without a signature, the old createOrderAndFund: one wallet
250
+ // deposits and sends, and the direct swap works as before. On neither path may the sender change the order terms
251
+ // - both signatures cover THE SAME quote digest.
254
252
  if (depositorSignature !== null && depositorSignature !== undefined && depositorSignature !== "") {
255
253
  const shape = depositorSignatureShape(depositorSignature);
256
- if (!shape.ok) throw new Error("подпись вносящего негодна: " + shape.reason);
254
+ if (!shape.ok) throw new Error("the depositor signature is unusable: " + shape.reason);
257
255
  }
258
256
  const relayedByDepositor = Boolean(depositorSignature);
259
257
  const createData = relayedByDepositor
@@ -266,14 +264,14 @@ export async function fundOrder({ factory, locker, claimer, commitHalfLocker, co
266
264
  gas: GAS_CREATE_ORDER_AND_FUND,
267
265
  });
268
266
 
269
- // 2б) АДРЕС ЭСКРОУ БЕРЁМ ИЗ КВИТАНЦИИ, А НЕ ИЗ ПРЕДСКАЗАНИЯ. Предсказание верно только пока совпадают
270
- // соль и код фабрики: повторный запуск фондирования создаёт эскроу по новому адресу, а запись сделки
271
- // осталась бы со старым - и сделка выглядела бы сломанной (сроки не читаются, кнопки возврата нет).
272
- // Ровно это и произошло. Расхождение с предсказанием называем вслух: это признак настоящей ошибки.
267
+ // 2b) THE ESCROW ADDRESS COMES FROM THE RECEIPT, NOT THE PREDICTION. The prediction is right only while the salt
268
+ // and the factory code match: a re-run of funding creates an escrow at a new address while the deal record keeps
269
+ // the old one - and the deal looks broken (deadlines unreadable, no refund button). That is exactly what
270
+ // happened. A divergence from the prediction is named aloud: it is a sign of a real error.
273
271
  let actualEscrow = null;
274
- // БЛОК СОЗДАНИЯ ЭСКРОУ - это высота, с которой сделка существует в цепи, и единственный НЕЗАВИСИМЫЙ
275
- // от ноды Monero источник высоты для файла восстановления. Объявлен снаружи ветки намеренно: внутри
276
- // блока объявленная переменная снаружи не видна, а возвращать её надо всегда.
272
+ // THE ESCROW'S BIRTH BLOCK is the height from which the deal exists on chain, and the only source of that height
273
+ // independent of the Monero node. Declared outside the branch on purpose: a variable declared inside a block is
274
+ // not visible outside, and it must always be returned.
277
275
  let birthBlock = null;
278
276
  if (typeof receipt === "function") {
279
277
  const rc = await waitReceipt(receipt, createHash);
@@ -290,20 +288,20 @@ export async function fundOrder({ factory, locker, claimer, commitHalfLocker, co
290
288
  }
291
289
  const escrowToUse = actualEscrow || escrow;
292
290
 
293
- // 3) ВТОРОЙ ТРАНЗАКЦИИ БОЛЬШЕ НЕТ: деньги уехали вместе с созданием ордера. Имя lockHash сохраняем
294
- // намеренно - на него смотрят вызывающие, и переименование задело бы их без пользы.
295
- // ДОКЛАДКИ НЕТ И НЕ БУДЕТ: отдельной функции внесения у эскроу нет, и добавление её означало бы возврат
296
- // пути «создать пустым, внести вторым вызовом» - того самого, которым деньги запирались в закрытом
297
- // ордере. Если ордер не зафондирован, это видно по receipt: создания ордера без денег не бывает.
291
+ // 3) THE SECOND TRANSACTION IS GONE: the money left together with order creation. The name lockHash is kept on
292
+ // purpose - callers look at it, and renaming would touch them with no benefit. THERE IS AND WILL BE NO TOP-UP:
293
+ // the escrow has no separate funding function, and adding one would restore the "create empty, fund by a second
294
+ // call" path that locked money inside a closed order. An unfunded order shows in the receipt: an order is never
295
+ // created without money.
298
296
  const lockHash = createHash;
299
297
 
300
- // escrow - ФАКТИЧЕСКИЙ адрес (из квитанции, если она была); predictedEscrow оставляем, чтобы расхождение
301
- // было видно снаружи, а не только в логе.
302
- // КОМИССИЯ НАРУЖУ: она ушла в транзакцию, и запись сделки обязана её сохранить - иначе по записи нельзя
303
- // будет посчитать, сколько человек внёс, и это разойдётся с тем, что видно в цепи.
298
+ // escrow is the ACTUAL address (from the receipt, if there was one); predictedEscrow is kept so a divergence is
299
+ // visible outside, not only in the log.
300
+ // THE FEE GOES OUTWARD: it went into the transaction and the deal record must keep it - otherwise the record
301
+ // cannot tell how much the person deposited, and it would diverge from what the chain shows.
304
302
  return { feeWei: fee.feeWei, feeTotalWei: totalValueWei, feeBps: fee.bps, feeWasLegacyFactory: Boolean(fee.legacy),
305
- // КАКОЙ ПУТЬ ФАБРИКИ СРАБОТАЛ: видно снаружи, а не только по calldata в кошельке. По нему
306
- // проверка и запись сделки понимают, будет ли ETH возвращаться подписавшему, а не отправителю.
303
+ // WHICH FACTORY PATH RAN: visible outside, not only in the wallet's calldata. By it the checks and the
304
+ // deal record know whether the ETH returns to the signer and not to the sender.
307
305
  depositorPath: relayedByDepositor,
308
306
  escrow: escrowToUse, predictedEscrow: escrow, escrowMismatch: Boolean(actualEscrow && actualEscrow !== String(escrow).toLowerCase()), salt, birthBlock, terms, createHash, lockHash };
309
307
  }