@ninsei-labs/sdk 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +866 -0
  2. package/LICENSE +21 -0
  3. package/README.md +187 -0
  4. package/engine/atomic/dleq2.js +330 -0
  5. package/engine/atomic/halfenc.js +205 -0
  6. package/engine/atomic/halves.js +119 -0
  7. package/engine/atomic/order-client.js +57 -0
  8. package/engine/atomic/order.js +132 -0
  9. package/engine/atomic/orderContext.js +70 -0
  10. package/engine/atomic/orderHalfCommit.js +29 -0
  11. package/engine/core/chainSource.js +306 -0
  12. package/engine/core/clock.js +40 -0
  13. package/engine/core/config.js +885 -0
  14. package/engine/core/finality.js +93 -0
  15. package/engine/core/format.js +168 -0
  16. package/engine/core/preSignGuards.js +495 -0
  17. package/engine/core/rfqSource.js +193 -0
  18. package/engine/core/sellWindow.js +43 -0
  19. package/engine/core/store.js +100 -0
  20. package/engine/core/swap.js +1153 -0
  21. package/engine/evm/amounts.js +131 -0
  22. package/engine/evm/auth.js +156 -0
  23. package/engine/evm/claimGas.js +87 -0
  24. package/engine/evm/depositor.js +127 -0
  25. package/engine/evm/dex.js +836 -0
  26. package/engine/evm/escrow.js +208 -0
  27. package/engine/evm/factory.js +149 -0
  28. package/engine/evm/fees.js +90 -0
  29. package/engine/evm/funding.js +309 -0
  30. package/engine/evm/gasReserve.js +88 -0
  31. package/engine/evm/index.js +123 -0
  32. package/engine/evm/injected.js +175 -0
  33. package/engine/evm/permit.js +125 -0
  34. package/engine/evm/prices.js +179 -0
  35. package/engine/evm/session.js +777 -0
  36. package/engine/mock/chain.js +98 -0
  37. package/engine/monero/address.js +265 -0
  38. package/engine/monero/halvesSweep.js +68 -0
  39. package/engine/monero/node.js +110 -0
  40. package/engine/monero/scan.js +325 -0
  41. package/engine/monero/swapKeys.js +69 -0
  42. package/engine/monero/unlock.js +46 -0
  43. package/engine/monero/wallet.js +168 -0
  44. package/engine/recovery/recoveryFile.js +363 -0
  45. package/engine/recovery/restore.js +176 -0
  46. package/engine/sdk/bridge.js +609 -0
  47. package/engine/ui/dom.js +90 -0
  48. package/engine/ui/walletModal.js +132 -0
  49. package/index.d.ts +1417 -0
  50. package/package.json +74 -0
  51. package/src/actions.mjs +72 -0
  52. package/src/adapters/evm-wallet.mjs +126 -0
  53. package/src/adapters/index.mjs +21 -0
  54. package/src/adapters/local-storage.mjs +32 -0
  55. package/src/adapters/monero-wallet.mjs +185 -0
  56. package/src/adapters/monero.mjs +51 -0
  57. package/src/adapters/node-test-evm-wallet.mjs +345 -0
  58. package/src/codes.mjs +188 -0
  59. package/src/config.mjs +277 -0
  60. package/src/engine.mjs +53 -0
  61. package/src/errors.mjs +24 -0
  62. package/src/http.mjs +72 -0
  63. package/src/index.mjs +256 -0
  64. package/src/legs/evm.mjs +118 -0
  65. package/src/legs/index.mjs +50 -0
  66. package/src/levelsWalkSpec.mjs +183 -0
  67. package/src/lock.mjs +140 -0
  68. package/src/order.mjs +212 -0
  69. package/src/preflight.mjs +137 -0
  70. package/src/primitives.mjs +47 -0
  71. package/src/quoteEip712.mjs +35 -0
  72. package/src/quoteEip712Spec.mjs +214 -0
  73. package/src/quotes.mjs +465 -0
  74. package/src/record.mjs +65 -0
  75. package/src/recovery.mjs +40 -0
  76. package/src/sim.mjs +37 -0
  77. package/src/swap-flow.mjs +492 -0
  78. package/src/swaps.mjs +935 -0
  79. package/src/sweep.mjs +222 -0
  80. package/src/wallet.mjs +164 -0
  81. package/src/xmr-arrival.mjs +289 -0
@@ -0,0 +1,309 @@
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.
4
+ // Everything below this header is identical to the source.
5
+
6
+ // Фондирование ордера в контракте-эскроу: настоящие транзакции, а не имитация.
7
+ //
8
+ // Роли в этом потоке и почему так:
9
+ // - ордер создаёт и ETH вносит тот, кого в условиях записали `locker` (контракт требует ровно его);
10
+ // - забирает по секрету `claimer` - адрес задан в условиях, вызывающий на получателя не влияет;
11
+ // - секрет на этом шаге генерируется ЗДЕСЬ и помечен как тестовый: в бою он рождается при трате XMR
12
+ // (HTLC), и тогда эта же функция просто получит его снаружи. Больше ничего в форме менять не нужно.
13
+ //
14
+ // keccak256 передаётся зависимостью, как в www/js/monero/address.js: библиотеку ключей грузит тот,
15
+ // кто вызвал, а не этот модуль. Монеро и Ethereum используют один и тот же Keccak-256, поэтому хеш,
16
+ // посчитанный бандлом, совпадает с тем, что проверяет Solidity.
17
+
18
+ import { encodeCreateOrderAndFund, encodeCreateOrderAndFundByDepositor, encodePredict, decodeAddress, ORDER_CREATED_TOPIC } from "./factory.js";
19
+ // ВНОСЯЩИЙ ПО ПОДПИСИ (issue #97, части A/C): форма подписи вносящего - из одного места (evm/depositor.js),
20
+ // рядом с формой протокола. Заведя проверку формы здесь, мы получили бы вторую запись того же правила.
21
+ import { depositorSignatureShape } from "./depositor.js";
22
+ import { encodeStatus, decodeStatus } from "./escrow.js";
23
+ // КОМИССИЯ: ставка, величина и получатель - ЧАСТЬ ПОДПИСАННОЙ КОТИРОВКИ. У фабрики их больше нет (роутер
24
+ // удалён), поэтому величина берётся из котировки и сверяется зеркалом формулы. Без котировки фондировать
25
+ // нечем: фабрика принимает подписанный набор, а не голые условия ордера.
26
+ import { feeTermsFromQuote } from "./fees.js";
27
+ // ЗАЩИТЫ ПЕРЕД ПОДПИСЬЮ (док 45): сроки ордера и состояние эскроу - чистые функции, потому что проверять
28
+ // их на числах, которые их ЛОМАЮТ, можно только тогда, когда они не знают ни про сеть, ни про кошелёк.
29
+ import { deadlineVerdict, escrowStateVerdict } from "../core/preSignGuards.js";
30
+ import { SIGNING_GUARDS } from "../core/config.js";
31
+
32
+ const ATOMIC = 1_000_000_000_000_000_000n; // 1 ETH = 10^18 wei
33
+
34
+ // Тестовый секрет: 32 байта из криптографического генератора браузера.
35
+ export function newSecret() {
36
+ const bytes = new Uint8Array(32);
37
+ crypto.getRandomValues(bytes);
38
+ return "0x" + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
39
+ }
40
+
41
+ // Условия ордера. Время - абсолютное (unix), как требует контракт.
42
+ //
43
+ // ПОСЫЛКИ ЗДЕСЬ БОЛЬШЕ НЕТ (док 27): вместо неё в ордер идут ОБА обязательства на половины и ОБЕ
44
+ // публичные точки ed25519. Функция ТРЕБУЕТ их и падает, если их нет, - и это осознанно: молча
45
+ // отправить ордер с отсутствующими полями значило бы создать эскроу, по которому нельзя ни забрать,
46
+ // ни доказать связь половины с адресом. Пусть лучше откажет здесь и громко.
47
+ //
48
+ // СРОКА ДВА, И ОНИ НЕ СОВПАДАЮТ: readyBy - до него внёсший отмечает готовность ("XMR вижу"), t1 - с
49
+ // него забор закрыт, а вернуть может кто угодно. Совпадение вернуло бы окно, где оба пути валидны
50
+ // одновременно, и спор решал бы газ (док 26).
51
+ export function testOrderTerms({ locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker, amountWei, salt, claimWindowSeconds = 3600, readyWindowSeconds = 14400, readyBy, t1 }) {
52
+ // ТОЧКА ПРОСМОТРА В СПИСКЕ ОБЯЗАТЕЛЬНЫХ: без неё адрес Monero не собрать из полей ордера, и узнал бы
53
+ // об этом не тот, кто ошибся, а забирающий - уже после фондирования.
54
+ for (const [k, v] of Object.entries({ commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker })) {
55
+ if (!v) throw new Error("не задано условие ордера: " + k);
56
+ }
57
+ const now = Math.floor(Date.now() / 1000);
58
+ // СРОКИ МОЖНО ЗАДАТЬ ГОТОВЫМИ, И ЭТО НЕ УДОБСТВО, А НЕОБХОДИМОСТЬ. Они входят в канонический контекст
59
+ // ордера, к которому привязано доказательство, а доказательство считается ДО фондирования. Если сроки
60
+ // считать здесь и сейчас, они окажутся посчитаны ПОСЛЕ, то есть доказательство будет привязано к одним
61
+ // срокам, а в контракт уйдут другие. Поэтому вызывающий решает их заранее и передаёт сюда.
62
+ const hasReady = readyBy !== undefined && readyBy !== null && readyBy !== "";
63
+ const hasT1 = t1 !== undefined && t1 !== null && t1 !== "";
64
+ const readyByValue = hasReady ? Number(readyBy) : now + readyWindowSeconds;
65
+ // t1 - ОКНО ПОСЛЕ readyBy, а не "от сейчас" (issue #88): плоская формула при 4-часовом окне готовности
66
+ // давала t1 раньше readyBy. Тот же смысл, что в sdk/src/swap-flow.mjs и rfq/reverseOrder.mjs.
67
+ 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), здесь - отказ словами.
74
+ const deadlines = deadlineVerdict({ nowSec: now, readyBy: readyByValue, t1: t1Value, ...SIGNING_GUARDS });
75
+ if (deadlines.blocks) throw new Error("сроки ордера не годны: " + deadlines.reason);
76
+ return {
77
+ locker,
78
+ claimer,
79
+ commitHalfLocker, // обязательство половины внёсшего
80
+ commitHalfClaimer, // обязательство половины забирающего
81
+ edPointLocker, // P_a: по ней вторая сторона собирает общий адрес
82
+ edPointClaimer, // P_b
83
+ edViewPointLocker, // V_a: половина ПРОСМОТРА внёсшего (нужна для адреса Monero)
84
+ amount: String(amountWei),
85
+ // ОКНО ГОТОВНОСТИ - ЭТО ПРО MONERO, А НЕ ПРО EVM. Мейкер залочит XMR только после фондирования, и
86
+ // отметить готовность осмысленно лишь тогда, когда XMR реально подтверждён: блоки Monero идут по
87
+ // Пуассону, среднее до первого подтверждения около 2,5 минут, но один медленный блок легко даёт
88
+ // 10-20. Плюс шаги самого мейкера: кошелёк, комиссия, отправка. На пятнадцати минутах хвост не
89
+ // помещался, поэтому 30 - по замеру, а не «на всякий случай». Замеры и источники: док 26.
90
+ readyBy: String(readyByValue),
91
+ t1: String(t1Value), // граница расчёта: час по умолчанию
92
+ // СОЛЬ ВХОДИТ В УСЛОВИЯ, А НЕ ДОБАВЛЯЕТСЯ К НИМ. Она определяет адрес эскроу (CREATE2), и predict
93
+ // обязан считать адрес ПО ТЕМ ЖЕ полям, что уедут в создание. Раньше её добавляли только в вызов
94
+ // создания, а predict шёл без соли - и падал на её проверке в кодировщике calldata. Принцип файла
95
+ // («один источник полей») нарушался ровно на этом поле.
96
+ salt,
97
+ };
98
+ }
99
+
100
+ // Сумма в wei из человеческого значения (строка) и decimals токена.
101
+ export function toWei(human, decimals = 18) {
102
+ const [intPart, fracPart = ""] = String(human).split(".");
103
+ const frac = (fracPart + "0".repeat(decimals)).slice(0, decimals);
104
+ return BigInt(intPart || "0") * 10n ** BigInt(decimals) + BigInt(frac || "0");
105
+ }
106
+
107
+ // ГАЗ ВЫСТАВЛЯЕМ САМИ И С ЗАПАСОМ - ЭТО НЕ ПЕРЕСТРАХОВКА, А ЛЕЧЕНИЕ КОНКРЕТНОЙ ОШИБКИ.
108
+ // createOrder и lock уходят двумя транзакциями подряд. Кошелёк оценивает вторую в момент подписи, когда
109
+ // эскроу ещё НЕ создан, поэтому считает по пустому адресу обычный перевод: 21000 + стоимость данных. На
110
+ // живом контракте это дало лимит 24167 при реальной надобности 50294 - и lock упал по газу, хотя вызов
111
+ // был полностью верный (та же сумма, тот же отправитель, тот же адрес). Оценку кошелька здесь доверять
112
+ // нельзя, поэтому лимиты задаём явно, с запасом от измеренных значений.
113
+ const GAS_CREATE_ORDER = 1_200_000n; // измерено 612567 - запас ~2x: создаётся целый контракт
114
+ // Создание И взнос в одной транзакции: создание контракта плюс запись взноса, запас намеренно большой -
115
+ // недобор лимита стоит человеку подписи, а лишний газ в лимите на Arbitrum почти ничего не стоит.
116
+ const GAS_CREATE_ORDER_AND_FUND = 1_500_000n;
117
+ // GAS_LOCK УБРАН вместе с вызовом lock(bytes): взнос идёт в той же транзакции, что и создание ордера.
118
+
119
+ // СОБЫТИЕ OrderCreated: topic0 посчитан по фактической подписи (и сверяется проверкой watchtower),
120
+ // topics = [topic0, escrow, locker, claimer]; в data - оба обязательства, обе точки, сумма, readyBy, t1, соль.
121
+ // ТЕМА СОБЫТИЯ БЕРЁТСЯ ИЗ ОДНОГО МЕСТА - из factory.js, а не заводится здесь второй копией.
122
+ // Здесь она УЖЕ БЫЛА второй копией, и это дало настоящий дефект: после смены состава полей OrderCreated
123
+ // копия осталась прежней, событие по ней не находилось, и адрес эскроу молча падал в предсказание - то
124
+ // есть проверка "адрес из квитанции" перестала работать, не сказав об этом ни слова.
125
+
126
+ // ФАКТИЧЕСКИЙ адрес эскроу из квитанции. Возвращает null, если события нет (транзакция не в блоке или
127
+ // фабрика другая) - тогда вызывающий код обязан сказать об этом вслух, а не подставить догадку.
128
+ export function escrowFromReceipt(receipt) {
129
+ if (!receipt || !Array.isArray(receipt.logs)) return null;
130
+ for (const log of receipt.logs) {
131
+ const topics = log.topics || [];
132
+ if (String(topics[0]).toLowerCase() !== ORDER_CREATED_TOPIC) continue;
133
+ if (topics.length < 2) continue;
134
+ const addr = "0x" + String(topics[1]).slice(26).toLowerCase();
135
+ if (/^0x[0-9a-f]{40}$/.test(addr) && !/^0x0{40}$/.test(addr)) return addr;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ // Ждём квитанцию: сразу после подписи её ещё нет, и это нормально. Ждать бесконечно нельзя - говорим честно.
141
+ async function waitReceipt(receiptReader, hash, { tries = 12, delayMs = 1500 } = {}) {
142
+ for (let i = 0; i < tries; i += 1) {
143
+ const r = await receiptReader({ hash }).catch(() => null);
144
+ if (r) return r;
145
+ await new Promise((res) => setTimeout(res, delayMs));
146
+ }
147
+ return null;
148
+ }
149
+
150
+ // Полный путь фондирования. Шаги возвращаются наружу, чтобы интерфейс мог показать каждый:
151
+ // predict - бесплатный вызов, createOrder и lock - две транзакции в кошельке пользователя.
152
+ 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 (locker.toLowerCase() === claimer.toLowerCase()) {
157
+ throw new Error("locker и claimer совпадать не могут: контракт такой ордер отклонит");
158
+ }
159
+ // ПОДПИСАННАЯ КОТИРОВКА ОБЯЗАТЕЛЬНА: она несёт комиссию, адрес реестра и провайдера, без которых ордер
160
+ // создать нельзя. Раньше комиссию спрашивали у фабрики - теперь у неё спрашивать нечего; отсутствие
161
+ // котировки - это НАЗВАННЫЙ отказ, а не молчаливое фондирование не той суммы.
162
+ if (!quote || typeof quote !== "object") throw new Error("нужна подписанная котировка: фабрика принимает котировку, а не голые условия ордера");
163
+
164
+ // ОБЯЗАТЕЛЬСТВА И ТОЧКИ ОБЯЗАТЕЛЬНЫ, и проверяются здесь, а не в глубине: без них ордер создастся, но
165
+ // ни забрать по нему, ни доказать связь половины с Monero-адресом будет нельзя. Пусть падает громко.
166
+ // Соль определяет адрес эскроу и обязана существовать ДО сборки условий: условия уходят и в predict.
167
+ const salt = providedSalt || newSecret(); // соль CREATE2: детерминирует адрес эскроу
168
+ const terms = testOrderTerms({
169
+ locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker,
170
+ amountWei, salt, claimWindowSeconds, readyWindowSeconds, readyBy, t1,
171
+ });
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) адрес эскроу виден ДО создания - это и есть смысл фабрики
183
+ onStep?.("computing the escrow address");
184
+ // ПОЛЯ ПРЕДСКАЗАНИЯ ОБЯЗАНЫ СОВПАДАТЬ С ПОЛЯМИ СОЗДАНИЯ БАЙТ В БАЙТ - адрес есть хеш кода создания.
185
+ // Здесь это гарантируется тем, что и predict, и createOrderAndFund собираются из ОДНИХ И ТЕХ ЖЕ terms:
186
+ // разойтись они могут только если править одно из двух мест врозь. Раньше расхождение уже случалось
187
+ // (в предсказание не попадала посылка), и лечится оно не аккуратностью, а общим источником полей.
188
+ // ПОЛЯ ВНОСИВШЕГО ЕДУТ ОТДЕЛЬНЫМИ АРГУМЕНТАМИ ФАБРИКИ (их нет в подписанном наборе).
189
+ const quoteEnc = { ...quote, commitHalfLocker: terms.commitHalfLocker, edPointLocker: terms.edPointLocker, edViewPointLocker: terms.edViewPointLocker };
190
+ // КОТИРОВКА И УСЛОВИЯ - ОБ ОДНОМ: расхождение уводит адрес эскроу и подписывает не тот ордер.
191
+ for (const name of ["locker", "claimer", "amount", "readyBy", "t1", "salt"]) {
192
+ if (String(quote[name]).toLowerCase() !== String(terms[name]).toLowerCase()) {
193
+ throw new Error("котировка и условия ордера расходятся по полю " + name);
194
+ }
195
+ }
196
+ const predictedRaw = await call({ to: factory, data: encodePredict(quoteEnc) });
197
+ const escrow = decodeAddress(predictedRaw);
198
+ if (!/^0x[0-9a-fA-F]{40}$/.test(escrow) || /^0x0{40}$/i.test(escrow)) {
199
+ throw new Error("фабрика вернула некорректный адрес эскроу: " + String(predictedRaw).slice(0, 40));
200
+ }
201
+
202
+ // 1б) СОСТОЯНИЕ ЭСКРОУ ПРОВЕРЯЕТСЯ ДО ПОДПИСИ, А НЕ ПОСЛЕ НЕЁ. Адрес уже известен (predict выше), и
203
+ // если по нему лежит зафондированный, забранный или возвращённый ордер, подписывать нельзя: контракт
204
+ // запрещает второе внесение (AlreadyFunded), а новая подпись (с новой солью) создала бы ВТОРОЙ ордер
205
+ // вместо прежнего - деньги ушли бы в него, а не в тот, о котором человек думает (док 45).
206
+ //
207
+ // ПУСТОЙ ОТВЕТ - ЭТО "ОРДЕРА НЕТ", А НЕ "ПРОВЕРИТЬ НЕ УДАЛОСЬ": по адресу, которого ещё не существует,
208
+ // eth_call возвращает пусто (так же это читается в sdk/src/swap-flow.mjs: orderDeadlines). А вот ОШИБКА
209
+ // вызова - другое состояние, и она запирает: "не проверили" нельзя считать за "всё хорошо".
210
+ onStep?.("checking the order is not already on chain");
211
+ let stateRead = null;
212
+ try {
213
+ const raw = await call({ to: escrow, data: encodeStatus() });
214
+ const body = String(raw || "").replace(/^0x/, "");
215
+ // Ответ escrow обязан быть четырьмя словами status(): короче - значит по этому адресу нет контракта,
216
+ // исполняющего status(), то есть ордера там нет и быть не может.
217
+ stateRead = body.length >= 256 ? { status: decodeStatus(raw) } : { status: null };
218
+ } catch (e) {
219
+ stateRead = { error: String((e && e.message) || e) };
220
+ }
221
+ const escrowState = escrowStateVerdict({ status: stateRead.status, error: stateRead.error, address: escrow });
222
+ if (escrowState.blocks) throw new Error("подпись не начата: " + escrowState.reason);
223
+
224
+ // 2) ОДНА ТРАНЗАКЦИЯ: создание ордера ВМЕСТЕ с внесением денег.
225
+ //
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
+ // условий. Комиссия прибавляется сверху и удержится при заборе; при возврате вернётся вместе с суммой.
239
+ onStep?.("reading the escrow fee from the signed quote");
240
+ // КОМИССИЮ БЕРЁМ ИЗ КОТИРОВКИ И СВЕРЯЕМ ЗЕРКАЛОМ. Направление уже учтено: покупка - amount + fee (сверху),
241
+ // продажа - amount (комиссия вычтется из выплаты забирающему).
242
+ const fee = feeTermsFromQuote(quote);
243
+ const totalValueWei = fee.totalWei;
244
+ const feeWords = fee.feeWei > 0n
245
+ ? " (order " + amountWei + " + fee " + fee.feeWei + ")"
246
+ : "";
247
+ onStep?.("locking " + (amountLabel || "funds") + feeWords + " in one transaction - confirm in wallet");
248
+ // КАКОЙ ДВЕРЬЮ ФАБРИКИ ИДЁТ ВНЕСЕНИЕ, И ЭТО НЕ УКРАШЕНИЕ. Подпись вносящего есть - вносим путём части A:
249
+ // `createOrderAndFundByDepositor` восстанавливает вносившего ИЗ ПОДПИСИ над дайджестом котировки, поэтому
250
+ // транзакцию вправе отправить кто угодно (релейщик, наш сервис), а роль `locker` (право вернуть, право
251
+ // отметить готовность) остаётся у подписавшего. Подписи нет - прежний `createOrderAndFund`: вносит и
252
+ // отправляет один и тот же кошелёк, прямой обмен работает ровно как раньше. Менять условия ордера
253
+ // отправителю нельзя ни на одном пути - обе подписи покрывают ОДИН И ТОТ ЖЕ дайджест котировки.
254
+ if (depositorSignature !== null && depositorSignature !== undefined && depositorSignature !== "") {
255
+ const shape = depositorSignatureShape(depositorSignature);
256
+ if (!shape.ok) throw new Error("подпись вносящего негодна: " + shape.reason);
257
+ }
258
+ const relayedByDepositor = Boolean(depositorSignature);
259
+ const createData = relayedByDepositor
260
+ ? encodeCreateOrderAndFundByDepositor(quoteEnc, depositorSignature)
261
+ : encodeCreateOrderAndFund(quoteEnc);
262
+ const createHash = await send({
263
+ to: factory,
264
+ data: createData,
265
+ value: "0x" + totalValueWei.toString(16),
266
+ gas: GAS_CREATE_ORDER_AND_FUND,
267
+ });
268
+
269
+ // 2б) АДРЕС ЭСКРОУ БЕРЁМ ИЗ КВИТАНЦИИ, А НЕ ИЗ ПРЕДСКАЗАНИЯ. Предсказание верно только пока совпадают
270
+ // соль и код фабрики: повторный запуск фондирования создаёт эскроу по новому адресу, а запись сделки
271
+ // осталась бы со старым - и сделка выглядела бы сломанной (сроки не читаются, кнопки возврата нет).
272
+ // Ровно это и произошло. Расхождение с предсказанием называем вслух: это признак настоящей ошибки.
273
+ let actualEscrow = null;
274
+ // БЛОК СОЗДАНИЯ ЭСКРОУ - это высота, с которой сделка существует в цепи, и единственный НЕЗАВИСИМЫЙ
275
+ // от ноды Monero источник высоты для файла восстановления. Объявлен снаружи ветки намеренно: внутри
276
+ // блока объявленная переменная снаружи не видна, а возвращать её надо всегда.
277
+ let birthBlock = null;
278
+ if (typeof receipt === "function") {
279
+ const rc = await waitReceipt(receipt, createHash);
280
+ actualEscrow = escrowFromReceipt(rc);
281
+ if (rc && Number.isFinite(Number(rc.blockNumber))) birthBlock = Number(rc.blockNumber);
282
+ if (!rc) {
283
+ onStep?.("no receipt yet - using the predicted address");
284
+ } else if (!actualEscrow) {
285
+ onStep?.("no OrderCreated in the receipt - using the predicted address");
286
+ } else if (actualEscrow !== String(escrow).toLowerCase()) {
287
+ console.warn("[escrow] receipt address " + actualEscrow + " != predicted " + escrow + " - using the receipt one");
288
+ onStep?.("receipt address differs from predicted - using the receipt one");
289
+ }
290
+ }
291
+ const escrowToUse = actualEscrow || escrow;
292
+
293
+ // 3) ВТОРОЙ ТРАНЗАКЦИИ БОЛЬШЕ НЕТ: деньги уехали вместе с созданием ордера. Имя lockHash сохраняем
294
+ // намеренно - на него смотрят вызывающие, и переименование задело бы их без пользы.
295
+ // ДОКЛАДКИ НЕТ И НЕ БУДЕТ: отдельной функции внесения у эскроу нет, и добавление её означало бы возврат
296
+ // пути «создать пустым, внести вторым вызовом» - того самого, которым деньги запирались в закрытом
297
+ // ордере. Если ордер не зафондирован, это видно по receipt: создания ордера без денег не бывает.
298
+ const lockHash = createHash;
299
+
300
+ // escrow - ФАКТИЧЕСКИЙ адрес (из квитанции, если она была); predictedEscrow оставляем, чтобы расхождение
301
+ // было видно снаружи, а не только в логе.
302
+ // КОМИССИЯ НАРУЖУ: она ушла в транзакцию, и запись сделки обязана её сохранить - иначе по записи нельзя
303
+ // будет посчитать, сколько человек внёс, и это разойдётся с тем, что видно в цепи.
304
+ return { feeWei: fee.feeWei, feeTotalWei: totalValueWei, feeBps: fee.bps, feeWasLegacyFactory: Boolean(fee.legacy),
305
+ // КАКОЙ ПУТЬ ФАБРИКИ СРАБОТАЛ: видно снаружи, а не только по calldata в кошельке. По нему
306
+ // проверка и запись сделки понимают, будет ли ETH возвращаться подписавшему, а не отправителю.
307
+ depositorPath: relayedByDepositor,
308
+ escrow: escrowToUse, predictedEscrow: escrow, escrowMismatch: Boolean(actualEscrow && actualEscrow !== String(escrow).toLowerCase()), salt, birthBlock, terms, createHash, lockHash };
309
+ }
@@ -0,0 +1,88 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/gasReserve.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.
4
+ // Everything below this header is identical to the source.
5
+
6
+ // ЗАПАС ГАЗА ПОКУПАТЕЛЯ НА ДЕЙСТВИЯ ОРДЕРА (issue #97, часть B): ОТМЕТКА ГОТОВНОСТИ И ВОЗМОЖНЫЙ ВОЗВРАТ.
7
+ //
8
+ // СУТЬ. В прямом обмене человек вносит средства в эскроу, а после прихода XMR сам зовёт markReady (когда XMR
9
+ // подтверждены) либо refund (когда сделка не состоялась). Оба вызова стоят газа в НАТИВНОЙ монете сети, и у
10
+ // человека, пришедшего из USDC, своего натива нет. Поэтому ДО котировки решается, СКОЛЬКО НАТИВА ОСТАВИТЬ под
11
+ // эти два вызова; решение показывается и в ETH, и в долларах, а строкой входит в разбивку цены.
12
+ //
13
+ // ПОЧЕМУ ЧИСТЫЕ ФУНКЦИИ. Решение стоит денег и обязано проверяться без сети (tools/check-buy-gas-reserve.mjs)
14
+ // и считаться ОДНИМ И ТЕМ ЖЕ кодом на экране и в прогоне. Сеть касается только чтения цены газа - его
15
+ // подставляет вызывающий (www/js/evm/prices.js кладёт gasPriceWei в живые цены, экран передаёт его сюда).
16
+ //
17
+ // ЧИСЛА НЕ ВЫДУМАНЫ. Пределы газа - те же, с которыми НАШ ЖЕ код отправляет обе транзакции
18
+ // (движок сделки теперь в пакете: sdk/src/swap-flow.mjs - markReadyOrder - gas 120000, refundOrder - gas 250000). Проверка
19
+ // tools/check-buy-gas-reserve.mjs сверяет обе константы со строками того файла, поэтому правка одного места
20
+ // без другого краснит прогон - ровно так же, как это сделано для газа забора в rfq/reverseGas.mjs.
21
+ //
22
+ // ПОЧЕМУ ЗАПАС ФИКСИРУЕТСЯ ДО КОТИРОВКИ. `amount` ордера входит в ПОДПИСАННУЮ котировку, и решение о запасе
23
+ // меняет итоговую сумму взноса; значит оно принимается ДО запроса котировки и дальше не пересчитывается.
24
+ // Экран кладёт готовый план в состояние формы (www/js/ui/views/swapForm.js), а экран подписи пользуется тем
25
+ // же зафиксированным планом - второй пересчёт разошёлся бы с первым.
26
+
27
+ // ПРЕДЕЛЫ ГАЗА - ИЗ НАШЕГО ЖЕ КОДА. Строки-источники: sdk/src/swap-flow.mjs (gas: 120_000n у отметки и
28
+ // gas: 250_000n у возврата). Заниженный предел - это отказ уже после раскрытия половины; неиспользованный
29
+ // газ в лимите не списывается, поэтому запас берётся с запасом.
30
+ export const ORDER_READY_GAS_LIMIT = 120_000n; // markReady - sdk/src/swap-flow.mjs markReadyOrder
31
+ export const ORDER_REFUND_GAS_LIMIT = 250_000n; // refund - sdk/src/swap-flow.mjs refundOrder
32
+ export const ORDER_GAS_LIMIT = ORDER_READY_GAS_LIMIT + ORDER_REFUND_GAS_LIMIT;
33
+
34
+ const toBig = (v) => {
35
+ if (v === null || v === undefined || v === "") return null;
36
+ try { return BigInt(typeof v === "bigint" ? v : String(v)); } catch { return null; }
37
+ };
38
+
39
+ // СКОЛЬКО ВЕЙ НУЖНО НА ОБА ВЫЗОВА. Цену газа даёт цепь, пределы - наши. Ноль цены и нечитаемое число
40
+ // означают «не измерено», и это НЕ ноль газа: неизвестное требование обязано называться, а не выдаваться за
41
+ // ноль (то же правило, что у газа забора: rfq/reverseGas.mjs).
42
+ export function orderGasReserveWei({ gasPriceWei, readyGasLimit = ORDER_READY_GAS_LIMIT, refundGasLimit = ORDER_REFUND_GAS_LIMIT } = {}) {
43
+ const gp = toBig(gasPriceWei);
44
+ if (gp === null || gp <= 0n) return null;
45
+ const ready = toBig(readyGasLimit);
46
+ const refund = toBig(refundGasLimit);
47
+ if (ready === null || refund === null || ready <= 0n || refund <= 0n) return null;
48
+ return gp * (ready + refund);
49
+ }
50
+
51
+ // ПОКАЗ В ОБЕИХ ВЕЛИЧИНАХ. У человека, пришедшего из USDC, нет чувства масштаба ETH, поэтому запас показан и
52
+ // в ETH, и в долларах. Доллар - оценка по живому курсу натива; нет курса - нет и оценки (null), а не выдумка.
53
+ export function gasReserveDisplay({ reserveWei, nativeUsd, decimals = 18 } = {}) {
54
+ const w = toBig(reserveWei);
55
+ if (w === null || w <= 0n) return null;
56
+ const native = Number(w) / 10 ** decimals;
57
+ const usd = Number.isFinite(Number(nativeUsd)) && Number(nativeUsd) > 0 ? native * Number(nativeUsd) : null;
58
+ return { native, usd };
59
+ }
60
+
61
+ // РЕШЕНИЕ ПО ГАЛОЧКЕ. Галочка НУЖНА, только когда своего натива НЕ ХВАТАЕТ на оба вызова; при достатке ETH
62
+ // она не показывается вовсе - человек платит своим газом, и просить у него нечего. Нечитаемый баланс натива -
63
+ // отдельное состояние: запас предлагаем (лучше предложить, чем промолчать о нехватке), и это названо полем.
64
+ export function gasReserveChoice({ nativeBalanceWei, reserveWei } = {}) {
65
+ const need = toBig(reserveWei);
66
+ if (need === null || need <= 0n) return { measured: false, enough: null, show: false, balanceUnread: false };
67
+ const have = toBig(nativeBalanceWei);
68
+ if (have === null) return { measured: true, enough: false, show: true, balanceUnread: true };
69
+ return { measured: true, enough: have >= need, show: have < need, balanceUnread: false };
70
+ }
71
+
72
+ // ПОЛНОЕ РЕШЕНИЕ ОДНИМ ОБЪЕКТОМ - именно оно фиксируется в состоянии формы до котировки. Все числовые поля -
73
+ // строки либо null: так план переживает запись и чтение, и BigInt не превращается в «[object BigInt]».
74
+ export function orderGasReservePlan({ gasPriceWei = null, nativeBalanceWei = null, nativeUsd = null, decimals = 18 } = {}) {
75
+ const reserveWei = orderGasReserveWei({ gasPriceWei });
76
+ const display = reserveWei === null ? null : gasReserveDisplay({ reserveWei, nativeUsd, decimals });
77
+ const choice = gasReserveChoice({ nativeBalanceWei, reserveWei });
78
+ return {
79
+ measured: reserveWei !== null,
80
+ gasPriceWei: gasPriceWei === null || gasPriceWei === undefined ? null : String(gasPriceWei),
81
+ reserveWei: reserveWei === null ? null : reserveWei.toString(),
82
+ native: display ? display.native : null,
83
+ usd: display ? display.usd : null,
84
+ enough: choice.enough,
85
+ show: choice.show,
86
+ balanceUnread: Boolean(choice.balanceUnread),
87
+ };
88
+ }
@@ -0,0 +1,123 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/index.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.
4
+ // Everything below this header is identical to the source.
5
+
6
+ // Кошелёк, которым пользуется интерфейс демки.
7
+ //
8
+ // Реализация одна: WalletConnect (www/js/evm/walletconnect.js) - настоящий кошелёк
9
+ // пользователя, настоящие балансы. Раньше здесь был выбор режима (WalletConnect или
10
+ // встроенный демо-кошелёк) для Demo controls; демо-кошелёк убран вместе с переключателем,
11
+ // потому что мы движемся к настоящему продукту, а не к макету.
12
+ //
13
+ // Чего в демке по-прежнему НЕТ: переводов. Контракта escrow ещё не существует, адрес escrow
14
+ // генерируется моком (www/js/mock/chain.js), поэтому шаг фондирования остаётся имитацией -
15
+ // simulateEscrowFunding() ниже. Когда контракт появится, на его месте будет
16
+ // provider.sendTransaction/eth_sendTransaction, а имитация исчезнет вместе с моком цепочки.
17
+
18
+ import {
19
+ address,
20
+ balanceOf,
21
+ chainIdOf,
22
+ connect,
23
+ disconnect,
24
+ isConnected,
25
+ refreshBalances,
26
+ restore,
27
+ status,
28
+ subscribe,
29
+ switchChain,
30
+ currentProvider,
31
+ tokensWithBalance,
32
+ connectInjected,
33
+ restoreInjected,
34
+ } from "./session.js";
35
+ import { discoverWallets } from "./injected.js";
36
+ // fundOrder нужен ЛОКАЛЬНЫМ именем: export * из модуля ./funding.js« ниже только реэкспортирует, но не
37
+ // создаёт переменную в этом модуле. Из-за этого fundOrderLive вызывал несуществующее имя и падал.
38
+ import { fundOrder } from "./funding.js";
39
+ export * from "./factory.js";
40
+ export * from "./escrow.js";
41
+ export * from "./funding.js";
42
+ // ВНОСЯЩИЙ ПО ПОДПИСИ И РАЗРЕШЕНИЕ НА ТОКЕН. Форма подписи вносящего (evm/depositor.js) и вердикт о
43
+ // разрешении по подписи (evm/permit.js) выходят через ТОТ ЖЕ шов движка: страница и SDK пользуются одним
44
+ // кодом протокола, а не двумя похожими.
45
+ export * from "./depositor.js";
46
+ export * from "./permit.js";
47
+
48
+ // currentProvider - ПРОВАЙДЕР КОШЕЛЬКА НАРУЖУ, ЯДРУ: склейник отдаёт его адаптеру SDK (evm-wallet.mjs), чтобы
49
+ // подпись шла тем же ключом, что и у страницы. Экранам он не нужен: они читают status()/isConnected().
50
+ export { address, balanceOf, chainIdOf, connect, disconnect, isConnected, refreshBalances, restore, status, subscribe, switchChain, currentProvider };
51
+ export { connectInjected, restoreInjected };
52
+ // ВАЖНО: это импорт, а не реэкспорт. Конструкция export { X } из модуля ...« только реэкспортирует имя и НЕ
53
+ // создаёт его в этом модуле, а fundOrderLive ниже вызывает sendTransaction и readContract напрямую.
54
+ // Ровно на этом уже падало дважды: сначала fundOrder, затем readContract - оба имени были доступны
55
+ // снаружи, но не внутри файла.
56
+ import { sendTransaction, readContract, readReceipt, readCode } from "./session.js";
57
+ export { sendTransaction, readContract, readReceipt, readCode };
58
+
59
+ // Список браузерных кошельков в этом браузере (EIP-6963, с фолбэком на window.ethereum).
60
+ // Пустой список - обычное дело: значит расширения нет, и остаётся WalletConnect.
61
+ export function wallets() {
62
+ return discoverWallets();
63
+ }
64
+
65
+ // Балансы всех принимаемых токенов сети (нативный первым) - для экранов, которым нужен список.
66
+
67
+ // ИМИТАЦИЯ: настоящей транзакции не отправляем, потому что отправлять некуда - escrow-контракта
68
+ // нет. Возвращаем поддельный хеш, чтобы состояние сделки осталось выводимым (см. docs/03).
69
+ // В UI это сказано прямо: демка не двигает реальные деньги.
70
+ export async function simulateEscrowFunding(tx) {
71
+ await new Promise((r) => setTimeout(r, 900));
72
+ const bytes = new Uint8Array(32);
73
+ crypto.getRandomValues(bytes);
74
+ const hash = "0x" + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
75
+ return { hash, ...tx, simulated: true };
76
+ }
77
+
78
+ // БОЕВОЙ ПУТЬ ФОНДИРОВАНИЯ: настоящие транзакции в кошельке пользователя.
79
+ //
80
+ // Заменяет simulateEscrowFunding. Отличие принципиальное: симуляция выдумывала хеш, здесь уходят две
81
+ // настоящие транзакции - createOrder в фабрику и lock(sealedHalf) в созданный эскроу.
82
+ //
83
+ // Разделение обязанностей намеренное: криптография (половины, DLEQ, посылка) живёт в www/js/atomic/ и
84
+ // сюда НЕ заглядывает. Эта функция получает уже готовые sealedCommitment и sealedHalf - то есть ровно то,
85
+ // что уходит в контракт. Порядок вычислений задаёт вызывающий: сначала проверить сторону контрагента
86
+ // (раздел 20.5), и только потом лочить.
87
+ //
88
+ // factory передаётся снаружи: адрес берётся из конфига сети тем, кто вызывает, чтобы здесь не заводить
89
+ // второй источник правды об адресах.
90
+ // ПОДПИСЬ ПРИВЕДЕНА К НОВОЙ СХЕМЕ, и это была не косметика: здесь оставались хешлок, обязательство и
91
+ // посылка от прежнего устройства, а обязательств на половины и точек ed25519 - которые fundOrder ТРЕБУЕТ
92
+ // и без которых падает - не было вовсе. То есть живой путь фондирования не мог пройти даже первый шаг:
93
+ // вызов уходил дальше без четырёх обязательных полей. Ни синтаксис, ни проверки этого не видели, потому
94
+ // что живой путь никем не запускается: он требует настоящего кошелька и подписи.
95
+ // ЧЕТЫРЕ ПОЛЯ И ДВА СРОКА ЯВНО. Сроки приходят готовыми (readyBy, t1), а не считаются здесь от текущего
96
+ // времени: они входят в контекст, к которому привязано доказательство, значит должны быть решены ДО него.
97
+ export async function fundOrderLive({ factory, locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, edViewPointLocker, salt, amountWei, amountLabel, claimWindowSeconds = 3600, readyWindowSeconds = 14400, readyBy, t1, quote, depositorSignature, onStep }) {
98
+ return fundOrder({
99
+ factory,
100
+ quote, // подписанная котировка: несёт комиссию, адрес реестра и провайдера
101
+ // ПОДПИСЬ ВНОСЯЩЕГО НАД ДАЙДЖЕСТОМ КОТИРОВКИ (issue #97, части A/C). Есть - фондирование идёт путём
102
+ // createOrderAndFundByDepositor, и транзакцию вправе отправить кто угодно. Нет - прежний путь.
103
+ depositorSignature,
104
+ locker,
105
+ claimer,
106
+ commitHalfLocker,
107
+ commitHalfClaimer,
108
+ edPointLocker,
109
+ edPointClaimer,
110
+ edViewPointLocker,
111
+ salt, // повтор фондирования обязан попасть в тот же адрес эскроу
112
+ amountLabel, // "0.03 ETH" вместо числа в wei на кнопке
113
+ amountWei,
114
+ claimWindowSeconds,
115
+ readyWindowSeconds,
116
+ readyBy, // сроки, к которым уже привязано доказательство
117
+ t1,
118
+ call: readContract,
119
+ send: sendTransaction,
120
+ receipt: readReceipt, // адрес эскроу берём из квитанции, а не из предсказания
121
+ onStep,
122
+ });
123
+ }