@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,208 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/escrow.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
+ // Вызовы контракта-эскроу ArrakisSwap (один экземпляр = один ордер).
7
+ //
8
+ // Библиотек для EVM в проекте нет: у эскроу ТРИ функции, которые двигают состояние (markReady, claim,
9
+ // refund), и несколько читаемых, поэтому calldata собирается селектором и словами по 32 байта.
10
+ // ФУНКЦИИ ВНЕСЕНИЯ У ЭСКРОУ НЕТ: ордер рождается зафондированным в транзакции создания, и двухшаговый
11
+ // путь («создать пустым, внести вторым вызовом») убран вместе с ней - он и был источником запирания
12
+ // денег в закрытом ордере. Отсюда и отсутствие селектора fund() в таблице ниже.
13
+ // Селекторы вычислены инструментом, не вручную - и это те подписи, которые у контракта ЕСТЬ:
14
+ // cast sig "markReady()" -> 0x1ca087e7
15
+ // cast sig "claim(bytes32)" -> 0xbd66528a
16
+ // cast sig "refund(bytes32)" -> 0x7249fbb6 (refund() БЕЗ аргумента у контракта нет: половина обязательна, :294)
17
+ // cast sig "status()" -> 0x200d2ed2
18
+ // Проверяются в tools/check-evm-abi.mjs сверкой с эталоном Foundry (cast sig / cast calldata).
19
+ //
20
+ // ПРО СОБЫТИЯ - ПРЕЖНЯЯ РЕДАКЦИЯ ЭТОГО КОММЕНТАРИЯ ВРАЛА, и врала дважды. У эскроу события ЕСТЬ, три:
21
+ // HalfRevealed(bytes32 half, bool byClaimer) - mvp/contracts/NinseiEscrow.sol:131
22
+ // OrderTerms(...) - :133-139
23
+ // Ready(address indexed by) - :141
24
+ // StrayRescued(address indexed token, ...) - :145
25
+ // Состояние всё же читается вызовом status(), и причина не в отсутствии событий, а в двух других вещах:
26
+ // 1) ВНЕСЕНИЕ ДЕНЕГ СОБЫТИЯ НЕ ИМЕЕТ ВООБЩЕ: флаг funded ставится в конструкторе (:232-237) вместе с
27
+ // эмиссией OrderTerms, но отдельного «внесено» в логах нет, а это первое из состояний ордера;
28
+ // 2) status() (:406-408) заведён в контракте именно «фронту и watchtower'у» и отдаёт все четыре
29
+ // величины ОДНИМ вызовом - внесено, забрано, возвращено, остаток - по адресу эскроу, который уже
30
+ // известен из записи сделки. Следить для этого за логами с блока создания не нужно.
31
+ // Событие HalfRevealed при этом несёт признак byClaimer, то есть из него видно и «забрано», и
32
+ // «возвращено»: тому, кто ведёт журнал ордеров, подписка на него полезна. Для чтения состояния
33
+ // конкретного ордера на месте нужен status().
34
+
35
+ export const ESCROW_METHODS = {
36
+ // ВНЕСЕНИЯ ЗДЕСЬ НЕТ НАМЕРЕННО. Прежний fund() кодировался как 0xb60d4288; функции у контракта больше
37
+ // нет, а её правила переехали в рождение ордера: ровно `amount + fee` проверяет конструктор, и платит
38
+ // тот, кто назван внёсшим, - других транзакций у ордера не бывает. Вернувшаяся строка с этим селектором
39
+ // краснит tools/check-evm-abi.mjs.
40
+ // отметить готовность: "XMR вижу". Без неё забор невозможен (:233) - это защита от забора ETH без XMR.
41
+ // Ставит её только внёсший, только на зафондированном ордере и только до readyBy (:218-222).
42
+ markReady: "0x1ca087e7",
43
+ // забрать, предъявив СВОЮ половину; её хеш (обязательство) зашит в ордер при создании.
44
+ // Вызвать вправе кто угодно - адрес отправителя контракт не проверяет (:232-235), - но деньги уходят
45
+ // не ему, а записанному claimer (:242), поэтому переадресовать их вызывающий не может.
46
+ claim: "0xbd66528a",
47
+ // вернуть средства после срока t1, ПРЕДЪЯВИВ ПОЛОВИНУ ВНЁСШЕГО: контракт сверяет её хеш с
48
+ // обязательством commitHalfLocker (:253, поле :79) и отвергает чужую половину (BadHalf).
49
+ // Отсюда уточнение к прежней формулировке "может позвать кто угодно": отправить транзакцию вправе
50
+ // любой (:246-249, роль watchtower), но провести её может лишь тот, у кого есть половина внёсшего.
51
+ // Деньги при этом идут ВСЕГДА внёсшему (:260), а половина публикуется - по ней он заберёт своё XMR.
52
+ refund: "0x7249fbb6",
53
+ // состояние: (заблокировано, забрано, возвращено, остаток)
54
+ status: "0x200d2ed2",
55
+ // СРОК ОДИН. До t1 ETH забирает только claimer по своей половине и только при отметке ready (:233),
56
+ // с t1 забор запрещён (:234), а вернуть вправе кто угодно, предъявив половину внёсшего (:250-253).
57
+ // Селектора t0() здесь больше нет: поля t0 в контракте не существует (решение
58
+ // 2026-09-16, док 26). У ордеров, созданных раньше, срок лежит в записи сделки, и правило для них
59
+ // прежнее - см. derive() в core/swap.js.
60
+ t1: "0xfb5343f3",
61
+ // до этого срока внёсший может отметить готовность; строго раньше t1
62
+ readyBy: "0x53fc2f40",
63
+ // отмечена ли готовность: без неё забор закрыт
64
+ ready: "0x6defbf80",
65
+ // раскрытая расчётом половина: из неё вторая сторона собирает ключ траты
66
+ revealedHalf: "0x3f267bb6",
67
+ // ТОЧКИ И ОБЯЗАТЕЛЬСТВА ОРДЕРА, прочитанные У САМОГО ЭСКРОУ. Нужны перед подписью: доказательство DLEQ
68
+ // связывает половину с точкой, но не мешает положить в ордер ДРУГИЕ точки - тогда общий адрес Monero
69
+ // соберётся не из тех половин, а узнать об этом можно, только сверив СЛОТЫ ЖИВОЙ ЦЕПИ с тем, что собрала
70
+ // страница. Селекторы посчитаны `cast sig` по фактическим подписям; сверка таблицы - в
71
+ // tools/check-evm-abi.mjs (отдельный зуб на каждое имя).
72
+ edPointLocker: "0x8c03382b", // точка траты внёсшего: половина общего адреса
73
+ edPointClaimer: "0xdd98c465", // и точка траты забирающего: сверяется с котировкой, а не берётся на слово
74
+ edViewPointLocker: "0x9a06a5fe", // и его точка просмотра: без неё адрес Monero не собрать
75
+ commitHalfLocker: "0x498b422e", // обязательство на половину внёсшего: по нему контракт проверит refund
76
+ commitHalfClaimer: "0x7d4c81f7", // обязательство на половину забирающего: по нему контракт проверит claim
77
+ // условия ордера одним хешем. Нужен, чтобы файл восстановления был ПРИВЯЗАН к ордеру на цепи: по нему
78
+ // владелец файла проверяет, что файл относится именно к этому эскроу, а не к похожему.
79
+ // ГРАНИЦА, КОТОРУЮ НАЗВАЛ АУДИТ, ЗАКРЫТА РЕШЕНИЕМ. Раньше точки просмотра внёсшего (edViewPointLocker) в хеше
80
+ // НЕ было, и сверку именно этой точки приходилось делать отдельно. Теперь поле входит в слепок (:346) на
81
+ // своё место - сразу после точки забирающего, как в событии OrderTerms, - поэтому ордер сверяется
82
+ // ЦЕЛИКОМ, и отдельная сверка точки просмотра больше не нужна. ЦЕНА НАЗВАНА ПРЯМО: слепок у ВСЕХ ордеров
83
+ // другой, значит фабрика и эскроу передеплоиваются, а адреса, записанные где-либо до этого (config.js,
84
+ // networks.json, записи сделок), к новым ордерам не относятся.
85
+ termsHash: "0xb311d9fd",
86
+ // КОМИССИЯ, РЕЕСТР И ПРОИСХОЖДЕНИЕ - ВСЁ ЧИТАЕТСЯ У САМОГО ЭСКРОУ. У фабрики этих полей больше нет
87
+ // (роутер удалён, решение #76): ставка, величина и получатель комиссии, адрес реестра, провайдер и ключ
88
+ // подписи приходят в подписанной котировке и лежат в ордере неизменяемыми полями. Селекторы - `cast sig`
89
+ // по фактическим подписям, как и всё остальное в этой таблице; сверка - в tools/check-evm-abi.mjs.
90
+ fee: "0xddca3f43", // величина комиссии в wei (посчитана при создании ордера)
91
+ feeBps: "0x24a9d853", // ставка в базисных пунктах (30 = 0.30%)
92
+ feeRecipient: "0x46904840", // кому комиссия зачисляется в кассу
93
+ feeOnTop: "0x2296cc23", // true - покупка (комиссия сверху), false - продажа (вычет из выплаты)
94
+ registry: "0x7b103999", // адрес реестра мейкеров ИЗ УСЛОВИЙ ОРДЕРА
95
+ cashier: "0xed740e97", // касса комиссии ИЗ УСЛОВИЙ ОРДЕРА (#103): фабрика её НЕ заводит
96
+ provider: "0x085d4883", // провайдер, заявленный в котировке (проверен по реестру)
97
+ quoteKey: "0x391d53ca", // ключ, которым подписана котировка
98
+ validUntil: "0xddac6654", // срок годности котировки (СЕКУНДЫ)
99
+ nonce: "0xaffed0e0", // нонс котировки
100
+ };
101
+
102
+ // Динамических аргументов у эскроу НЕТ, поэтому все вызовы ниже - голые селекторы. Кодировщика внесения
103
+ // здесь больше нет: у эскроу нет самой функции (см. заметку в ESCROW_METHODS).
104
+ // call markReady(): аргументов нет.
105
+ export function encodeMarkReady() {
106
+ return ESCROW_METHODS.markReady;
107
+ }
108
+
109
+ // call ready()/readyBy()/revealedHalf(): аргументов нет - calldata у них равен самому селектору,
110
+ // поэтому отдельных кодировщиков здесь нет, а селекторы лежат в ESCROW_METHODS выше.
111
+
112
+ // call claim(secret): секрет - bytes32. По контракту это половина ЗАБИРАЮЩЕГО (имя параметра -
113
+ // halfClaimer, :231), а "секретом" её называют потому, что она служит прообразом обязательства
114
+ // commitHalfClaimer: контракт сверяет keccak256 предъявленной половины со своим обязательством
115
+ // (:235) и ничего больше - связь половины с адресом Monero проверяет не он (док 27 §10).
116
+ export function encodeClaim(secret) {
117
+ return ESCROW_METHODS.claim + halfAsThirtyTwoBytes(secret, "секрет");
118
+ }
119
+
120
+ // ПОЛОВИНА - ЧИСЛО, А НЕ СТРОКА ФИКСИРОВАННОЙ ДЛИНЫ. В записи сделки и в файле восстановления половина
121
+ // хранится как есть, и ведущий ноль теряется: 63 знака вместо 64. Контракту нужны ровно 32 байта, поэтому
122
+ // добиваем слева (как это делает нода в своём заборе). Без этого возврат был невозможен в принципе:
123
+ // снаружи это выглядело как "половина должна быть 32 байтами" на живом нажатии кнопки возврата.
124
+ function halfAsThirtyTwoBytes(value, what) {
125
+ const hex = String(value || "").replace(/^0x/i, "").toLowerCase();
126
+ if (!/^[0-9a-f]{1,64}$/.test(hex)) throw new Error(what + " должен быть числом до 32 байт: " + value);
127
+ return hex.padStart(64, "0");
128
+ }
129
+
130
+ // call refund(bytes32 halfLocker): предъявляется половина ВНЁСШЕГО, а не половина вызывающего -
131
+ // контракт сверяет её хеш с обязательством commitHalfLocker (:253) и чужую отвергает (BadHalf).
132
+ // Отправителя контракт не проверяет: право на вызов даёт сама половина (:246-249).
133
+ export function encodeRefund(halfLocker) {
134
+ return ESCROW_METHODS.refund + halfAsThirtyTwoBytes(halfLocker, "половина");
135
+ }
136
+
137
+ // call status(): аргументов нет.
138
+ export function encodeStatus() {
139
+ return ESCROW_METHODS.status;
140
+ }
141
+
142
+ // call t1(): аргументов нет.
143
+ export function encodeT1() {
144
+ return ESCROW_METHODS.t1;
145
+ }
146
+
147
+ // call termsHash(): аргументов нет.
148
+ export function encodeTermsHash() {
149
+ return ESCROW_METHODS.termsHash;
150
+ }
151
+
152
+ // call к методу эскроу БЕЗ аргументов по имени. Нужен там, где чтений много (слоты ордера): отдельная
153
+ // функция на каждый селектор завела бы вторую таблицу тех же имён, и она разошлась бы с первой молча.
154
+ export function encodeRead(name) {
155
+ const sel = ESCROW_METHODS[name];
156
+ if (typeof sel !== "string" || !/^0x[0-9a-f]{8}$/.test(sel)) throw new Error("нет такого метода эскроу: " + name);
157
+ return sel;
158
+ }
159
+
160
+ // Разбор ответа bytes32: одно слово.
161
+ export function decodeBytes32(returnedHex, what = "значение") {
162
+ const hex = String(returnedHex || "").replace(/^0x/, "");
163
+ if (hex.length < 64) throw new Error("короткий ответ " + what + ": " + returnedHex);
164
+ return "0x" + hex.slice(0, 64).toLowerCase();
165
+ }
166
+
167
+ // Разбор ответа t1(): одно слово, uint64 внутри uint256.
168
+ export function decodeUint64(returnedHex, what = "значение") {
169
+ const hex = String(returnedHex || "").replace(/^0x/, "");
170
+ if (hex.length < 64) throw new Error("короткий ответ " + what + ": " + returnedHex);
171
+ const v = BigInt("0x" + hex.slice(0, 64));
172
+ if (v > 18446744073709551615n) throw new Error("ответ " + what + " не влезает в uint64: " + v);
173
+ return Number(v);
174
+ }
175
+
176
+ // Разбор ответа status(): четыре слова по 32 байта, три флага и остаток.
177
+ export function decodeStatus(returnedHex) {
178
+ const hex = String(returnedHex || "").replace(/^0x/, "");
179
+ if (hex.length < 256) throw new Error("короткий ответ status(): " + returnedHex);
180
+ const word = (i) => hex.slice(i * 64, (i + 1) * 64);
181
+ return {
182
+ isFunded: BigInt("0x" + word(0)) !== 0n,
183
+ isClaimed: BigInt("0x" + word(1)) !== 0n,
184
+ isRefunded: BigInt("0x" + word(2)) !== 0n,
185
+ balance: BigInt("0x" + word(3)),
186
+ };
187
+ }
188
+
189
+ // Какие условия ордера нужны эскроу при создании - одним местом, чтобы форма и проверка не разошлись.
190
+ // СРОК ОДИН. Поля t0 в условиях ордера больше нет: до t1 ETH забирает claimer по своей половине и
191
+ // только при отметке ready, с t1 забор запрещён, а вернуть вправе кто угодно, ПРЕДЪЯВИВ ПОЛОВИНУ
192
+ // ВНЁСШЕГО (NinseiEscrow.sol:250-253) - у сторожа её нет, поэтому "вернёт кто угодно" надо читать как
193
+ // "любой, у кого есть половина" (решение 2026-09-16, док 26; то же уточнение - док 24 §1).
194
+ // Проверка "t1 позже t0" потеряла смысл, но её место заняла не пустота: срок обязан быть В БУДУЩЕМ,
195
+ // иначе ордер создаётся уже закрытым - claim по нему невозможен, и деньги зависли бы до возврата.
196
+ // Верхних границ и требования "сроки в будущем" у КОНТРАКТА нет вовсе: конструктор проверяет только
197
+ // amount != 0 и t1 > readyBy (:170-171), а сроки в прошлом для него допустимы. Их держит эта функция -
198
+ // контракт защищает деньги, но не защищает от ордера с прошлыми сроками.
199
+ export function orderTerms({ locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, amount, readyBy, t1 }) {
200
+ for (const [k, v] of Object.entries({ locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, amount })) {
201
+ if (v === undefined || v === null || v === "") throw new Error("не задано условие ордера: " + k);
202
+ }
203
+ // Сроков два, и они НЕ должны совпадать: совпадение вернуло бы окно, где забор и возврат валидны
204
+ // одновременно, и спор решал бы газ. Первый срок - окно отметки готовности, второй - граница расчёта.
205
+ if (Number(readyBy) <= Math.floor(Date.now() / 1000)) throw new Error("readyBy должен быть в будущем: " + readyBy);
206
+ if (Number(t1) <= Number(readyBy)) throw new Error("t1 должен быть позже readyBy");
207
+ return { locker, claimer, commitHalfLocker, commitHalfClaimer, edPointLocker, edPointClaimer, amount, readyBy, t1 };
208
+ }
@@ -0,0 +1,149 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/factory.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
+ // Вызовы контракта-фабрики эскроу ArrakisSwap.
7
+ //
8
+ // Библиотек для EVM в проекте нет и не добавляем. ОБЕ ФУНКЦИИ ФАБРИКИ ТЕПЕРЬ ПРИНИМАЮТ ПОДПИСАННУЮ КОТИРОВКУ
9
+ // (OrderQuote.Quote), подпись тремя словами (r, s, v) и три поля вносившего. Комиссия и адрес реестра - ЧАСТЬ
10
+ // КОТИРОВКИ, а не настройка контракта: функций feeBps()/feeFor() у фабрики больше нет (роутер удалён,
11
+ // решение #76), и спрашивать их не у чего. Все аргументы статические, поэтому calldata - селектор плюс слова
12
+ // по 32 байта; собирается здесь, в одном месте, и проверяется tools/check-evm-abi.mjs.
13
+ //
14
+ // cast sig "predict((address,address,address,address,address,address,uint256,uint64,uint64,bytes32,bytes32,bytes32,uint256,uint256,uint256,address,uint64,uint256,uint256),bytes32,bytes32,uint8,bytes32,bytes32,bytes32)" -> 0x0824d205
15
+ // cast sig "createOrderAndFund(<тот же tuple>,...)" -> 0x4703a9d3
16
+
17
+ export const FACTORY_METHODS = {
18
+ // СОСТАВ КОТИРОВКИ ВЫРОС ПОЛЕМ xmrAmount (#110 шаг 3): XMR-сторона сделки входит в подписанный набор, поэтому
19
+ // селекторы обеих прежних функций сменились вторично (прежде - от поля cashier, #103).
20
+ predict: "0x0824d205",
21
+ // создать эскроу И внести деньги ОДНОЙ транзакцией (перевод value вместе с вызовом).
22
+ createOrderAndFund: "0x4703a9d3",
23
+ // создать эскроу и внести деньги, когда ВНОСЯЩИЙ восстановлен ИЗ ПОДПИСИ над дайджестом котировки
24
+ // (issue #97, часть A): транзакцию шлёт кто угодно, а `locker` записан подписавшим.
25
+ createOrderAndFundByDepositor: "0x2acd187b",
26
+ };
27
+
28
+ // ТЕМА СОБЫТИЯ OrderCreated. Она выросла адресом реестра, провайдером и ключом подписи:
29
+ // cast keccak "OrderCreated(address,address,address,bytes32,bytes32,bytes32,bytes32,bytes32,uint256,uint64,uint64,bytes32,address,address,address)"
30
+ // Тема живёт в ДВУХ местах - здесь и в watchtower/scan.mjs, - и обе обязаны совпадать: разойтись молча они
31
+ // не могут, сверку держит проверка ABI.
32
+ export const ORDER_CREATED_TOPIC =
33
+ "0xa22cdabaed3cce4f51dde71626a108ffe9c7bc4216fd14d5e719f892e2fd8ab0";
34
+
35
+ // ПОЛЯ КОТИРОВКИ В ПОРЯДКЕ КОНТРАКТА. Порядок - часть ABI: перестановка даёт другой вызов (чужую функцию
36
+ // или ордер с чужими условиями). Список ОДИН на обе функции: разойтись им нечем.
37
+ export const QUOTE_FIELDS = [
38
+ "provider", "quoteKey", "registry", "cashier", "locker", "claimer", "amount", "readyBy", "t1", "salt",
39
+ "commitHalfClaimer", "edPointClaimer", "chainId", "feeBps", "fee", "feeRecipient", "validUntil", "nonce", "xmrAmount",
40
+ ];
41
+ // Поля вносившего котировка НЕ несёт: они уходят отдельными аргументами (входят в init-код эскроу).
42
+ const LOCKER_HALF_FIELDS = ["commitHalfLocker", "edPointLocker", "edViewPointLocker"];
43
+ const ADDR_FIELDS = ["provider", "quoteKey", "registry", "cashier", "locker", "claimer", "feeRecipient"];
44
+ const B32_FIELDS = ["salt", "commitHalfClaimer", "edPointClaimer", "commitHalfLocker", "edPointLocker", "edViewPointLocker"];
45
+
46
+ // Слово calldata: значение, выровненное вправо до 32 байт. Числа приходят десятичными (wei, сроки), а в
47
+ // calldata обязаны быть шестнадцатеричными: дополнение нулями десятичных цифр кодировало бы их как hex и
48
+ // уводило сумму в другое число. Поймано сверкой с эталоном Foundry.
49
+ export function word(value, bytes = 32) {
50
+ const raw = String(value).trim();
51
+ let hex;
52
+ if (/^0x[0-9a-fA-F]*$/.test(raw)) {
53
+ hex = raw.slice(2).toLowerCase();
54
+ if (hex.length === 0) hex = "0";
55
+ } else if (/^[0-9]+$/.test(raw)) {
56
+ hex = BigInt(raw).toString(16);
57
+ } else {
58
+ throw new Error("не число и не hex: " + value);
59
+ }
60
+ if (!/^[0-9a-f]*$/.test(hex)) throw new Error("не hex: " + value);
61
+ if (hex.length > bytes * 2) throw new Error(`значение не влезает в ${bytes} байт: ${value}`);
62
+ return hex.padStart(bytes * 2, "0");
63
+ }
64
+
65
+ // ФОРМА ПРИВОДИТСЯ, А НЕ ТРЕБУЕТСЯ: точки и обязательства в записи движка лежат БЕЗ префикса 0x, а word()
66
+ // требует префикс. Байты одни и те же, поэтому оба вида кодируются одинаково.
67
+ const asHex32 = (v) => {
68
+ const x = String(v === undefined || v === null ? "" : v).trim();
69
+ return /^[0-9a-fA-F]{64}$/.test(x) ? "0x" + x.toLowerCase() : x;
70
+ };
71
+
72
+ // СЛОВА ПОДПИСАННОГО НАБОРА. Подпись - 65 байт r || s || v; v приводится к 27/28 (контракт принимает оба
73
+ // соглашения). r и s - ПОЛНЫЕ 32 байта, v - тоже ПОЛНОЕ слово из 32 байт (ABI uint8 не бывает короче слова:
74
+ // неполное слово сдвинуло бы хвост calldata и вызов ушёл бы на несуществующий селектор).
75
+ function quoteWords(quote) {
76
+ for (const name of QUOTE_FIELDS) {
77
+ if (quote[name] === undefined || quote[name] === null || quote[name] === "") throw new Error("в котировке нет поля: " + name);
78
+ }
79
+ // ФОРМА ПРОВЕРЯЕТСЯ, А НЕ ДОСТРАИВАЕТСЯ: короткий адрес или усечённый bytes32 молча выровнялся бы
80
+ // нулями и ушёл в контракт ДРУГИМ значением.
81
+ for (const name of ADDR_FIELDS) {
82
+ if (!/^(0x)?[0-9a-fA-F]{40}$/.test(String(quote[name]).trim())) throw new Error("поле котировки " + name + " - не адрес");
83
+ }
84
+ for (const name of B32_FIELDS) {
85
+ if (!/^(0x)?[0-9a-fA-F]{64}$/.test(String(quote[name]).trim())) throw new Error("поле котировки " + name + " - не bytes32");
86
+ }
87
+ const map = {
88
+ provider: word(quote.provider, 32), quoteKey: word(quote.quoteKey, 32), registry: word(quote.registry, 32),
89
+ cashier: word(quote.cashier, 32),
90
+ locker: word(quote.locker, 32), claimer: word(quote.claimer, 32),
91
+ amount: word(quote.amount, 32), readyBy: word(quote.readyBy, 32), t1: word(quote.t1, 32),
92
+ salt: word(asHex32(quote.salt), 32),
93
+ commitHalfClaimer: word(asHex32(quote.commitHalfClaimer), 32),
94
+ edPointClaimer: word(asHex32(quote.edPointClaimer), 32),
95
+ chainId: word(quote.chainId, 32), feeBps: word(quote.feeBps, 32), fee: word(quote.fee, 32),
96
+ feeRecipient: word(quote.feeRecipient, 32), validUntil: word(quote.validUntil, 32), nonce: word(quote.nonce, 32),
97
+ xmrAmount: word(quote.xmrAmount, 32),
98
+ };
99
+ return QUOTE_FIELDS.map((n) => map[n]).join("");
100
+ }
101
+
102
+ function signatureWords(signature) {
103
+ const hex = String(signature || "").replace(/^0x/, "");
104
+ if (!/^[0-9a-fA-F]{130}$/.test(hex)) throw new Error("подпись котировки должна быть 65 байт: " + String(signature).slice(0, 20));
105
+ const r = hex.slice(0, 64), s = hex.slice(64, 128);
106
+ let v = parseInt(hex.slice(128, 130), 16);
107
+ if (v < 27) v += 27;
108
+ return word("0x" + r, 32) + word("0x" + s, 32) + word(v, 32);
109
+ }
110
+
111
+ // ОБЩАЯ ЧАСТЬ predict и createOrderAndFund.
112
+ function lockerHalfWords(quote) {
113
+ return LOCKER_HALF_FIELDS.map((name) => {
114
+ if (quote[name] === undefined || quote[name] === null || quote[name] === "") throw new Error("в котировке нет поля вносившего: " + name);
115
+ return word(asHex32(quote[name]), 32);
116
+ }).join("");
117
+ }
118
+
119
+ function quoteCallArgs(quote) {
120
+ return quoteWords(quote) + signatureWords(quote.signature) + lockerHalfWords(quote);
121
+ }
122
+
123
+ // ПРЕДСКАЗАТЬ АДРЕС. Состав аргументов совпадает с createOrderAndFund БАЙТ В БАЙТ (адрес - хеш кода
124
+ // создания, и подпись в него входит).
125
+ export function encodePredict(quote) {
126
+ return FACTORY_METHODS.predict + quoteCallArgs(quote);
127
+ }
128
+
129
+ // СОЗДАТЬ И ВНЕСТИ. Вносившим становится msg.sender; его адрес уже внутри котировки, а фабрика сверяет его
130
+ // с отправителем.
131
+ export function encodeCreateOrderAndFund(quote) {
132
+ return FACTORY_METHODS.createOrderAndFund + quoteCallArgs(quote);
133
+ }
134
+
135
+ // СОЗДАТЬ И ВНЕСТИ, когда вносящий восстановлен ИЗ ЕГО ПОДПИСИ над ДАЙДЖЕСТОМ КОТИРОВКИ (#97, часть A).
136
+ // Транзакцию шлёт кто угодно, поэтому подписантов ДВОЕ: `quote.signature` - подпись провайдера (её
137
+ // проверяет эскроу), `depositorSignature` - подпись ВНОСЯЩЕГО (её проверяет фабрика и требует, чтобы
138
+ // восстановленный адрес совпал с `quote.locker`). Обе - над ОДНИМ дайджестом котировки.
139
+ export function encodeCreateOrderAndFundByDepositor(quote, depositorSignature) {
140
+ return FACTORY_METHODS.createOrderAndFundByDepositor + quoteWords(quote) +
141
+ signatureWords(quote.signature) + signatureWords(depositorSignature) + lockerHalfWords(quote);
142
+ }
143
+
144
+ // Ответ predict() - один адрес в 32-байтовом слове.
145
+ export function decodeAddress(returnedHex) {
146
+ const hex = String(returnedHex || "").replace(/^0x/, "");
147
+ if (hex.length < 64) throw new Error("короткий ответ: " + returnedHex);
148
+ return "0x" + hex.slice(24, 64);
149
+ }
@@ -0,0 +1,90 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/fees.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
+ // Комиссия ArrakisSwap: ЧАСТЬ ПОДПИСАННОЙ КОТИРОВКИ (OrderQuote v7), а не настройка контракта.
7
+ //
8
+ // ГДЕ ЖИВЁТ СТАВКА. Раньше её и получателя назначала ФАБРИКА (или роутер) в неизменяемых полях, и клиент
9
+ // спрашивал их у цепи (feeBps()/feeRecipient()/feeFor()). Теперь этих функций у контракта НЕТ: роутер удалён
10
+ // (решение #76), а ставка, величина и получатель комиссии входят в ПОДПИСАННЫЙ набор условий ордера. Значит
11
+ // источник правды ОДИН - котировка, и второго места быть не должно:
12
+ // * в настройках страницы ставки НЕТ; копия разошлась бы с подписанным набором;
13
+ // * у фабрики спрашивать нечего - она ничего о комиссии не знает;
14
+ // * величина в wei посчитана по формуле `amount * feeBps / 10000` (округление ВНИЗ) и здесь сверяется с
15
+ // этой же формулой ЗЕРКАЛОМ: расхождение обязано остановить подпись, а не превратиться в отказ эскроу
16
+ // (WrongAmount/BadFee) уже после неё.
17
+ //
18
+ // КТО ПЛАТИТ. Комиссию платит ПОЛЬЗОВАТЕЛЬ ИНТЕРФЕЙСА. Направление видно из ролей котировки: провайдер
19
+ // забирает ETH - покупка (комиссия СВЕРХУ взноса), провайдер вносит ETH - продажа (комиссия ВЫЧЕТОМ из
20
+ // выплаты, `amount - fee`). Провайдер не назван ни там, ни там - понять нечего, отказ.
21
+ //
22
+ // ОДНА ЕДИНИЦА ВРЕМЕНИ. Время в проекте - СЕКУНДЫ (сроки ордера, срок годности котировки, block.timestamp):
23
+ // пересчёт между шкалами и был единственным местом, где ошибка на тысячу проходит молча.
24
+
25
+ // Потолок ставки, зашитый в контракте (NinseiEscrow.MAX_FEE_BPS = 200 = 2%). Нужен здесь, чтобы отличить
26
+ // осмысленную ставку от испорченной: читать мусор как ставку и считать по нему комиссию нельзя.
27
+ export const MAX_FEE_BPS = 200;
28
+
29
+ // ЗЕРКАЛО ФОРМУЛЫ КОНТРАКТА. Одно место на всю страницу: вторая копия разошлась бы с первой ровно там,
30
+ // где это дороже всего - на величине, которую требует эскроу.
31
+ export function feeWeiFor(amountWei, feeBps) {
32
+ const amount = BigInt(amountWei === null || amountWei === undefined || amountWei === "" ? 0 : amountWei);
33
+ const bps = BigInt(Number(feeBps) || 0);
34
+ if (amount < 0n || bps < 0n) throw new Error("комиссия: отрицательная сумма или ставка");
35
+ return (amount * bps) / 10000n; // ВНИЗ, как в Solidity: amount * feeBps / 10000
36
+ }
37
+
38
+ const sameAddr = (a, b) => String(a || "").toLowerCase() === String(b || "").toLowerCase();
39
+ const zeroAddr = (a) => !a || /^0x0{40}$/i.test(String(a));
40
+
41
+ // ПАМЯТЬ О ПОСЛЕДНЕЙ ПОДПИСАННОЙ КОТИРОВКЕ. Ставка становится известна ТОЛЬКО с котировкой; до неё её нет,
42
+ // и показывать вместо неё ноль или число из конфига значило бы врать. Проверки подставляют свои значения
43
+ // через resetFeeCache + feeTermsFromQuote.
44
+ let lastTerms = null;
45
+
46
+ // СБРОС ПАМЯТИ. Нужен проверкам и любому повтору после смены котировки.
47
+ export function resetFeeCache() {
48
+ lastTerms = null;
49
+ }
50
+
51
+ // КОМИССИЯ ИЗ КОТИРОВКИ. Возвращает ставку, величину (сверенную зеркалом), получателя и направление; при
52
+ // ненулевой ставке без получателя - отказ (комиссия сгорела бы). Результат кладётся в память для экрана.
53
+ export function feeTermsFromQuote(quote) {
54
+ if (!quote || typeof quote !== "object") throw new Error("нет подписанной котировки: комиссию и адрес реестра взять неоткуда");
55
+ const amount = BigInt(String(quote.amount));
56
+ const bps = Number(quote.feeBps === undefined || quote.feeBps === null ? 0 : quote.feeBps);
57
+ if (!Number.isFinite(bps) || bps < 0 || bps > MAX_FEE_BPS) throw new Error("ставка комиссии вне потолка: " + quote.feeBps);
58
+ const fee = feeWeiFor(amount, bps);
59
+ if (BigInt(String(quote.fee === undefined || quote.fee === null ? 0 : quote.fee)) !== fee) {
60
+ throw new Error("подписанная комиссия " + quote.fee + " расходится с формулой amount * rate (" + fee + ") - подпись не начата");
61
+ }
62
+ if (bps !== 0 && zeroAddr(quote.feeRecipient)) throw new Error("при ненулевой ставке получатель комиссии обязан быть назван");
63
+ const providerIsLocker = sameAddr(quote.locker, quote.provider);
64
+ const providerIsClaimer = sameAddr(quote.claimer, quote.provider);
65
+ if (providerIsLocker === providerIsClaimer) throw new Error("не понять, кто платит комиссию: провайдер не назван ни вносившим, ни забирающим");
66
+ const feeOnTop = providerIsClaimer; // провайдер забирает ETH -> покупка -> комиссия сверху взноса
67
+ const terms = { bps, feeWei: fee, totalWei: feeOnTop ? amount + fee : amount, recipient: quote.feeRecipient || null, feeOnTop, legacy: false };
68
+ lastTerms = terms;
69
+ return terms;
70
+ }
71
+
72
+ // СИНХРОННЫЙ ДОСТУП К ПРОЧИТАННОМУ. Пусто - значит "ещё не читали либо не смогли": это НЕ ноль, и
73
+ // вызывающий обязан закрыть подпись, а не посчитать комиссию нулевой.
74
+ export function cachedFeeTerms() {
75
+ return lastTerms;
76
+ }
77
+
78
+ // СТАВКА ДЛЯ ПОКАЗА. Единственное место, откуда страница берёт число для строки разбивки цены. Ответа нет -
79
+ // null, и экран говорит, что ставка ещё неизвестна.
80
+ export function cachedFeeRate() {
81
+ if (!lastTerms || !Number.isFinite(Number(lastTerms.bps))) return null;
82
+ return Number(lastTerms.bps) / 10000;
83
+ }
84
+
85
+ // РАНЬШЕ ЭТА ФУНКЦИЯ ЧИТАЛА СТАВКУ У ЦЕПИ. Теперь читать не у чего: ставка - в подписанной котировке, и
86
+ // приходит она вместе с нею (feeTermsFromQuote). Функция оставлена ради вызывающих экранов: до котировки она
87
+ // честно возвращает то, что уже известно (обычно null - "ставка неизвестна"), и ничего не выдумывает.
88
+ export async function ensureFeeTerms() {
89
+ return lastTerms;
90
+ }