@ninsei-labs/sdk 0.39.3 → 0.40.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 (69) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/engine/atomic/dleq2.js +86 -111
  3. package/engine/atomic/halfenc.js +58 -72
  4. package/engine/atomic/halves.js +31 -53
  5. package/engine/atomic/order-client.js +15 -16
  6. package/engine/atomic/order.js +44 -62
  7. package/engine/atomic/orderContext.js +26 -50
  8. package/engine/atomic/orderHalfCommit.js +17 -18
  9. package/engine/core/chainSource.js +54 -69
  10. package/engine/core/clock.js +11 -12
  11. package/engine/core/config.js +561 -451
  12. package/engine/core/finality.js +34 -53
  13. package/engine/core/format.js +35 -36
  14. package/engine/core/preSignGuards.js +109 -117
  15. package/engine/core/rfqSource.js +57 -75
  16. package/engine/core/sellWindow.js +23 -24
  17. package/engine/core/store.js +20 -21
  18. package/engine/core/swap.js +242 -243
  19. package/engine/evm/amounts.js +50 -51
  20. package/engine/evm/asyncExec.js +577 -0
  21. package/engine/evm/asyncLegs.js +308 -0
  22. package/engine/evm/asyncRoute.js +179 -0
  23. package/engine/evm/auth.js +41 -48
  24. package/engine/evm/claimGas.js +28 -47
  25. package/engine/evm/depositor.js +39 -43
  26. package/engine/evm/dex.js +266 -267
  27. package/engine/evm/escrow.js +113 -129
  28. package/engine/evm/factory.js +71 -77
  29. package/engine/evm/fees.js +41 -42
  30. package/engine/evm/funding.js +160 -162
  31. package/engine/evm/gasReserve.js +67 -36
  32. package/engine/evm/index.js +46 -53
  33. package/engine/evm/injected.js +34 -42
  34. package/engine/evm/permit.js +129 -56
  35. package/engine/evm/prices.js +30 -45
  36. package/engine/evm/session.js +188 -189
  37. package/engine/mock/chain.js +17 -18
  38. package/engine/monero/address.js +33 -48
  39. package/engine/monero/halvesSweep.js +25 -43
  40. package/engine/monero/node.js +14 -15
  41. package/engine/monero/scan.js +82 -95
  42. package/engine/monero/swapKeys.js +30 -33
  43. package/engine/monero/unlock.js +17 -20
  44. package/engine/monero/wallet.js +32 -42
  45. package/engine/recovery/recoveryFile.js +154 -161
  46. package/engine/recovery/restore.js +50 -61
  47. package/engine/sdk/bridge.js +244 -234
  48. package/engine/ui/dom.js +4 -5
  49. package/engine/ui/walletModal.js +20 -31
  50. package/index.d.ts +8 -0
  51. package/package.json +1 -1
  52. package/src/adapters/evm-wallet.mjs +16 -0
  53. package/src/adapters/monero-wallet.mjs +1 -1
  54. package/src/adapters/monero.mjs +1 -1
  55. package/src/adapters/node-test-evm-wallet.mjs +20 -8
  56. package/src/config.mjs +1 -1
  57. package/src/engine.mjs +4 -0
  58. package/src/legs/cow-spec.mjs +254 -0
  59. package/src/legs/cow.mjs +315 -0
  60. package/src/legs/evm.mjs +86 -2
  61. package/src/legs/kyber-spec.mjs +265 -0
  62. package/src/legs/kyber.mjs +425 -0
  63. package/src/legs/shape.mjs +35 -0
  64. package/src/legs/uniswaprelay-spec.mjs +541 -0
  65. package/src/legs/uniswaprelay.mjs +422 -0
  66. package/src/legs/uniswapx-spec.mjs +329 -0
  67. package/src/legs/uniswapx.mjs +362 -0
  68. package/src/quotes.mjs +1 -1
  69. package/src/swaps.mjs +1 -1
@@ -1,39 +1,26 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/atomic/halfenc.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
- // Передача половины ключа: зашифрованная половина, пригодная для публикации в цепи.
7
- // Разбор и обоснование: .hermes/docs/20-key-shares.md, разделы 5, 12.3, 16, 18.
5
+ // Transferring a key half: an encrypted half fit for publishing on chain.
6
+ // WHAT THIS IS. A half x is sent so that: third parties cannot read it (the mask comes from an ECDH shared secret
7
+ // with the recipient); THE RECIPIENT can check that the ciphertext holds the half matching the published point X by
8
+ // the simple equality x*G == X, with no separate ZK proof; and the data fits on chain.
9
+ // THIS IS OUR COMPOSITION, not from the paper: instead of an ECDSA adaptor signature we use ECDH + mask + equality
10
+ // check. The primitives are @noble/curves; the composition is ours.
11
+ // THE ADAPTOR-SIGNATURE DECISION IS MADE: NOT USED. Revealing the half goes by keccak commit/reveal (commitments
12
+ // before money, the contract verifies the half hash), and here the half is sent encrypted.
13
+ // IMPORTANT: the primitives are audited, THIS implementation is not.
14
+ // LOCKED UNTIL EXPLICIT PERMISSION: the encrypted-half envelope is not used in the live flow, and a dead key path
15
+ // nobody tested in a swap is locked. SEALING AND OPENING (seal/open) ARE LOCKED, NOT THE CONSTRUCTOR: the
16
+ // constructor is called by the live flow, so locking it would break the working path.
8
17
  //
9
- // ЧТО ЭТО. Половина x передаётся получателю так, что:
10
- // 1) третьи лица прочитать её не могут (маска выводится из общего секрета ECDH с получателем);
11
- // 2) ПОЛУЧАТЕЛЬ может проверить, что в шифртексте лежит именно та половина, которой соответствует
12
- // опубликованная точка X - простым равенством (x*G == X), без отдельного ZK-доказательства;
13
- // 3) данные помещаются в цепь: точка-получателя + эфемерная точка + замаскированный скаляр.
14
18
  //
15
- // ЭТО НАША КОМПОЗИЦИЯ, а не перенос из статьи. В документах протокола половина передаётся адаптерной
16
- // подписью ECDSA (Binks, раздел про ecdsa adaptor verify/decrypt). Мы делаем проще и проверяемо:
17
- // ECDH + маска + проверка равенством. Стойкость примитивов - на @noble/curves, но сам состав решения - наш.
18
19
  //
19
- // РЕШЕНИЕ ПО АДАПТЕРНОЙ ПОДПИСИ ПРИНЯТО: ОНА НЕ ПРИМЕНЯЕТСЯ, и это выбор, а не недоделка. Раскрытие половины
20
- // идёт механизмом keccak commit/reveal (обязательства объявляются ДО денег, контракт сверяет хеш половины),
21
- // а здесь половина передаётся зашифрованной. Разбор, выгоды, цена и причина - .hermes/docs/28-adaptor-note.md,
22
- // раздел 8.
23
20
  //
24
- // ВАЖНО: примитивы аудированы, ЭТА реализация - нет.
25
21
 
26
- // ЗАПЕРТО ДО ЯВНОГО РАЗРЕШЕНИЯ (вывод аудитора, review_01 §P3: «удалить или запереть мёртвую ветку, чтобы
27
- // её не вызвали „на всякий случай“»). Посылка зашифрованной половины в живом потоке не участвует: раскрытие
28
- // идёт механизмом keccak commit/reveal, а половину вторая сторона получает из цепи. Мёртвая ветка, которую
29
- // можно позвать случайно, - это путь ключей, который никто не проверял в сделке, поэтому он заперт.
30
22
  //
31
- // ЗАПЕРТО ИМЕННО СОЗДАНИЕ И ВСКРЫТИЕ КОНВЕРТА (seal/open), а НЕ КОНСТРУКТОР. Разница измерена, а не
32
- // придумана: конструктор зовёт ЖИВОЙ поток (www/js/atomic/order.js:26 создаёт механизм, :43 берёт у него
33
- // ключ получателя), поэтому замок на конструкторе сломал бы рабочий путь. Мёртвое здесь - сам конверт:
34
- // запечатать половину и вскрыть её.
35
- // РАЗРЕШЕНИЕ ДАЁТСЯ ЯВНО: globalThis.ARRAKIS_ALLOW_HALFENC = true (так делает только проверка
36
- // tools/check-halfenc.mjs, у которой на запертость есть отдельный случай).
23
+ // PERMISSION IS GIVEN EXPLICITLY: globalThis.ARRAKIS_ALLOW_HALFENC = true (only a check sets it, with a separate locked case).
37
24
  export const HALFENC_LOCK_REASON =
38
25
  "halfenc is locked: the sealed-half envelope is not part of the live flow (keccak commit/reveal replaced it). " +
39
26
  "Set globalThis.ARRAKIS_ALLOW_HALFENC = true to work with it in a test or a spike.";
@@ -45,7 +32,7 @@ export function halfencUnlocked() {
45
32
  export function createHalfEnc({ secp256k1, ed25519, keccak256, randomBytes }) {
46
33
  const ED_ORDER = ed25519.Point.Fn.ORDER;
47
34
  const SEC_ORDER = secp256k1.Point.Fn.ORDER;
48
- const LIMIT = ED_ORDER < SEC_ORDER ? ED_ORDER : SEC_ORDER; // маскируем в меньшем порядке
35
+ const LIMIT = ED_ORDER < SEC_ORDER ? ED_ORDER : SEC_ORDER; // mask within the smaller order
49
36
 
50
37
  function bytesToBig(b) { let v = 0n; for (const x of b) v = (v << 8n) | BigInt(x); return v; }
51
38
  function randScalar(limit) {
@@ -53,18 +40,18 @@ export function createHalfEnc({ secp256k1, ed25519, keccak256, randomBytes }) {
53
40
  const v = bytesToBig(randomBytes(32));
54
41
  if (v > 0n && v < limit) return v;
55
42
  }
56
- throw new Error("randScalar: не удалось за 256 попыток");
43
+ throw new Error("randScalar: failed in 256 attempts");
57
44
  }
58
45
  const bigToHex = (v) => v.toString(16).padStart(64, "0");
59
46
 
60
- // Ключ шифрования получателя: публикуется заранее и не связан с половиной ключа траты.
47
+ // The encryption recipient key: published in advance and unrelated to the spend half.
61
48
  function recipientKeyPair() {
62
49
  const priv = randScalar(SEC_ORDER);
63
50
  return { priv, pub: secp256k1.Point.BASE.multiply(priv).toHex() };
64
51
  }
65
52
 
66
- // Маска выводится из общего секрета ECDH: у отправителя через свою эфемерную пару и ключ получателя,
67
- // у получателя - через свой приватный ключ и эфемерную точку. Формула одна и та же.
53
+ // The mask comes from the ECDH shared secret: the sender via its ephemeral pair and the recipient key, the
54
+ // recipient via its private key and the ephemeral point. Same formula.
68
55
  function maskFrom(sharedPointHex, context) {
69
56
  const h = keccak256(Uint8Array.from(Buffer.concat([
70
57
  Buffer.from(sharedPointHex.replace(/^0x/, ""), "hex"),
@@ -73,48 +60,47 @@ export function createHalfEnc({ secp256k1, ed25519, keccak256, randomBytes }) {
73
60
  return bytesToBig(h) % LIMIT;
74
61
  }
75
62
 
76
- // ЗАКРЫТИЕ: половина x -> публикуемая посылка для получателя.
63
+ // SEAL: half x -> a publishable envelope for the recipient.
77
64
  function seal(x, recipientPubHex, context) {
78
- // ЗАМОК НА САМОМ КОНВЕРТЕ: сюда попадает только мёртвая ветка, и «на всякий случай» позвать её нельзя.
65
+ // LOCK ON THE ENVELOPE ITSELF: only the dead branch reaches it, so it cannot be called "just in case".
79
66
  if (!halfencUnlocked()) throw new Error(HALFENC_LOCK_REASON);
80
- if (!(x > 0n && x < LIMIT)) throw new Error("seal: половина вне границ min(l, n)");
67
+ if (!(x > 0n && x < LIMIT)) throw new Error("seal: half outside min(l, n)");
81
68
  const recipient = secp256k1.Point.fromHex(recipientPubHex.replace(/^0x/, ""));
82
69
  const ephPriv = randScalar(SEC_ORDER);
83
70
  const ephPub = secp256k1.Point.BASE.multiply(ephPriv);
84
- const shared = recipient.multiply(ephPriv); // ECDH у отправителя
71
+ const shared = recipient.multiply(ephPriv); // ECDH on the sender
85
72
  const mask = maskFrom(shared.toHex(), context);
86
- // Все hex-поля приводятся к одному виду: с префиксом 0x и в нижнем регистре. Иначе объект-посылка
87
- // и результат распаковки различались бы представлением одних и тех же байт (на этом и споткнулась
88
- // проверка: seal отдавал точки без префикса, а распаковка добавляла его).
73
+ // All hex fields are normalised: 0x prefix, lowercase. Else the envelope and the unpacked result would differ
74
+ // in representation of the same bytes.
89
75
  const canon = (h) => "0x" + String(h).replace(/^0x/i, "").toLowerCase();
90
76
  return {
91
- recipient: canon(recipientPubHex), // кому адресовано
92
- ephemeral: canon(ephPub.toHex()), // эфемерная точка, нужна получателю
93
- masked: "0x" + bigToHex((x + mask) % LIMIT), // замаскированный скаляр
77
+ recipient: canon(recipientPubHex), // to whom it is addressed
78
+ ephemeral: canon(ephPub.toHex()), // ephemeral point, needed by the recipient
79
+ masked: "0x" + bigToHex((x + mask) % LIMIT), // masked scalar
94
80
  context: context || "",
95
81
  };
96
82
  }
97
83
 
98
- // ВСКРЫТИЕ: получатель восстанавливает половину и ПРОВЕРЯЕТ её по опубликованной точке.
84
+ // OPEN: the recipient restores the half and CHECKS it against the published point.
99
85
  function open(sealed, recipientPriv, publicPointHex) {
100
- // ЗАМОК И НА ВСКРЫТИИ: вторая половина той же мёртвой ветки.
86
+ // LOCK ON OPEN TOO: the second half of the same dead branch.
101
87
  if (!halfencUnlocked()) return { ok: false, reason: HALFENC_LOCK_REASON };
102
- if (!sealed || !sealed.ephemeral || !sealed.masked) return { ok: false, reason: "посылка неполная" };
88
+ if (!sealed || !sealed.ephemeral || !sealed.masked) return { ok: false, reason: "incomplete envelope" };
103
89
  const eph = secp256k1.Point.fromHex(sealed.ephemeral.replace(/^0x/, ""));
104
- const shared = eph.multiply(recipientPriv); // ECDH у получателя
90
+ const shared = eph.multiply(recipientPriv); // ECDH on the recipient
105
91
  const mask = maskFrom(shared.toHex(), sealed.context);
106
92
  const x = (BigInt(sealed.masked) - mask + LIMIT) % LIMIT;
107
- if (x === 0n) return { ok: false, reason: "восстановлен ноль" };
93
+ if (x === 0n) return { ok: false, reason: "restored zero" };
108
94
  if (!publicPointHex) return { ok: true, x };
109
- // Проверка связки "в шифртексте именно эта половина": x*G обязан совпасть с опубликованной точкой.
95
+ // Check the binding "the ciphertext holds exactly this half": x*G must equal the published point.
110
96
  const expect = ed25519.Point.BASE.multiply(x).toHex();
111
97
  const got = ed25519.Point.fromHex((publicPointHex.startsWith("0x") ? publicPointHex.slice(2) : publicPointHex)).toHex();
112
- if (expect !== got) return { ok: false, reason: "в шифртексте не та половина" };
98
+ if (expect !== got) return { ok: false, reason: "the ciphertext holds a different half" };
113
99
  return { ok: true, x };
114
100
  }
115
101
 
116
- // Обязательство для контракта: keccak256 тех самых байтов посылки. Живёт внутри фабрики, потому что
117
- // хеш-функция передаётся ей снаружи (как и все зависимости), и снаружи видно только через этот объект.
102
+ // The commitment for the contract: keccak256 of the envelope bytes. It lives inside the factory, with the hash
103
+ // passed in from outside.
118
104
  function sealedCommitmentOf(hexBytes) {
119
105
  const h = keccak256(hexToBytesArr(hexBytes));
120
106
  return "0x" + bytesToHexStr(h);
@@ -123,21 +109,21 @@ export function createHalfEnc({ secp256k1, ed25519, keccak256, randomBytes }) {
123
109
  return { LIMIT, recipientKeyPair, seal, open, packSealed, unpackSealed, sealedCommitmentOf };
124
110
  }
125
111
 
126
- // --- Байтовый формат посылки ---------------------------------------------------------------
112
+ // --- Envelope byte format ---------------------------------------------------------------
127
113
  //
128
- // Контракту нужны БАЙТЫ: их keccak256 идёт в sealedCommitment ордера, и те же байты уходят в
129
- // calldata при lock(). Формат фиксированный и однозначный, потому что от него зависит обязательство:
130
- // при любой другой раскладке хеш не совпадёт и блокировка будет отвергнута.
114
+ // The contract needs BYTES: their keccak256 goes into the order's sealedCommitment and the same bytes go into
115
+ // calldata at lock(). The format is fixed, because the commitment depends on it: any other layout changes the
116
+ // hash and the lock is rejected.
131
117
  //
132
- // 0 : версия формата (1)
133
- // 1..33 : публичный ключ получателя шифрования (33 байта, сжатая точка secp256k1)
134
- // 34..66 : эфемерная точка отправителя (33 байта, сжатая)
135
- // 67..98 : замаскированный скаляр (32 байта, big-endian)
136
- // 99 : длина контекста в байтах (0..255)
137
- // 100.. : контекст ордера в UTF-8
118
+ // 0 : format version (1)
119
+ // 1..33 : encryption recipient public key (33 bytes, compressed secp256k1)
120
+ // 34..66 : sender ephemeral point (33 bytes, compressed)
121
+ // 67..98 : masked scalar (32 bytes, big-endian)
122
+ // 99 : context length in bytes (0..255)
123
+ // 100.. : order context in UTF-8
138
124
  //
139
- // Итого 100 + длина контекста байт. JSON здесь намеренно не используется: порядок ключей и форматирование
140
- // чисел дали бы разные байты для одного и того же содержимого - а обязательство требует однозначности.
125
+ // Total 100 + context length bytes. JSON is deliberately not used: key order and number formatting would give
126
+ // different bytes for the same content, and the commitment requires uniqueness.
141
127
  const FORMAT_VERSION = 1;
142
128
 
143
129
  function bytesToHexStr(b) {
@@ -173,28 +159,28 @@ const hexToBytesArr = (h) => {
173
159
  return out;
174
160
  };
175
161
 
176
- // Посылка -> байты (hex). Именно эти байты хешируются в обязательство и уходят в calldata.
162
+ // Envelope -> bytes (hex). These bytes are hashed into the commitment and go into calldata.
177
163
  export function packSealed(sealed) {
178
- if (!sealed || !sealed.recipient || !sealed.ephemeral || !sealed.masked) throw new Error("packSealed: посылка неполная");
164
+ if (!sealed || !sealed.recipient || !sealed.ephemeral || !sealed.masked) throw new Error("packSealed: incomplete envelope");
179
165
  const recipient = hexToBytesArr(sealed.recipient);
180
166
  const ephemeral = hexToBytesArr(sealed.ephemeral);
181
- if (recipient.length !== 33) throw new Error("packSealed: ключ получателя должен быть 33 байта (сжатая точка)");
182
- if (ephemeral.length !== 33) throw new Error("packSealed: эфемерная точка должна быть 33 байта (сжатая)");
167
+ if (recipient.length !== 33) throw new Error("packSealed: recipient key must be 33 bytes (compressed point)");
168
+ if (ephemeral.length !== 33) throw new Error("packSealed: ephemeral point must be 33 bytes (compressed)");
183
169
  const masked = BigInt(sealed.masked).toString(16).padStart(64, "0");
184
170
  const ctx = utf8(sealed.context);
185
- if (ctx.length > 255) throw new Error("packSealed: контекст длиннее 255 байт");
171
+ if (ctx.length > 255) throw new Error("packSealed: context longer than 255 bytes");
186
172
  return "0x" + bytesToHexStr(Uint8Array.of(FORMAT_VERSION))
187
173
  + bytesToHexStr(recipient) + bytesToHexStr(ephemeral) + masked
188
174
  + ctx.length.toString(16).padStart(2, "0") + bytesToHexStr(ctx);
189
175
  }
190
176
 
191
- // Байты -> посылка. Проверяет версию и длины; повреждённые данные не разбираются молча.
177
+ // Bytes -> envelope. Checks the version and lengths; corrupted data is not parsed silently.
192
178
  export function unpackSealed(hexBytes) {
193
179
  const b = hexToBytesArr(hexBytes);
194
- if (b.length < 100) throw new Error("unpackSealed: посылка короче 100 байт");
195
- if (b[0] !== FORMAT_VERSION) throw new Error("unpackSealed: неизвестная версия формата: " + b[0]);
180
+ if (b.length < 100) throw new Error("unpackSealed: envelope shorter than 100 bytes");
181
+ if (b[0] !== FORMAT_VERSION) throw new Error("unpackSealed: unknown format version: " + b[0]);
196
182
  const ctxLen = b[99];
197
- if (b.length !== 100 + ctxLen) throw new Error("unpackSealed: длина не совпадает с указанной в контексте");
183
+ if (b.length !== 100 + ctxLen) throw new Error("unpackSealed: length does not match the one in the context");
198
184
  return {
199
185
  recipient: "0x" + bytesToHexStr(b.slice(1, 34)),
200
186
  ephemeral: "0x" + bytesToHexStr(b.slice(34, 67)),
@@ -1,35 +1,24 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/atomic/halves.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
- // Половина ключа для атомарного свопа XMR <-> EVM.
7
- // См. .hermes/docs/18-xmr-swap-protocol.md, разделы 2, 3, 12, 13.
8
- //
9
- // ЧТО ЭТО. Ключ траты Monero собирается как сумма двух половин: ks = ks_a + ks_b (mod l). Потратить
10
- // выход можно, только зная обе половины. Одна и та же половина должна работать на двух кривых:
11
- // edwards25519 (Monero) и secp256k1 (EVM) - поэтому скаляр берётся МЕНЬШЕ меньшего из порядков групп
12
- // (статья, §4.3, формула 5: ks_i < min(l, n)). Это не деталь: именно из-за этого неравенства
13
- // доказательство равенства логарифмов сходится в обеих группах.
14
- //
15
- // ВАЖНО ПРО БЕЗОПАСНОСТЬ. Аудированы примитивы (@noble/curves), а НЕ этот код. Наш слой поверх них
16
- // проверки не проходил, и документация это фиксирует (§13). Не использовать для настоящих денег
17
- // до отдельного разбора.
18
- //
19
- // Зависимости передаются снаружи, как в www/js/monero/address.js: в браузере - вендоренный бандл,
20
- // в проверках - тот же пакет из node_modules. Модуль не тянет ничего сам.
5
+ // Half of a Monero key for the XMR <-> EVM atomic swap.
6
+ // The Monero spend key is the sum of two halves: ks = ks_a + ks_b (mod l); spending needs both.
7
+ // One half must work on edwards25519 (Monero) and secp256k1 (EVM), so the scalar stays below the smaller
8
+ // group order (min(l, n)) - that is what makes the equal-log proof converge on both groups.
9
+ // SECURITY: the audited primitives are @noble/curves, NOT this layer. Do not use with real money before a separate review.
10
+ // Dependencies are passed in from outside (browser: the vendored bundle; checks: node_modules). The module pulls nothing itself.
21
11
 
22
12
  export function createHalves({ ed25519, secp256k1, keccak256, randomBytes }) {
23
13
  for (const [name, v] of [["ed25519", ed25519], ["secp256k1", secp256k1], ["keccak256", keccak256], ["randomBytes", randomBytes]]) {
24
- if (!v) throw new Error("createHalves: не передано " + name);
14
+ if (!v) throw new Error("createHalves: not passed " + name);
25
15
  }
26
16
  const ED_ORDER = ed25519.Point.Fn.ORDER;
27
17
  const SECP_ORDER = secp256k1.Point.Fn.ORDER;
28
- // Меньший из порядков: половина обязана помещаться в обе группы (статья §4.3).
18
+ // The smaller group order: a half must fit into both groups (paper section 4.3).
29
19
  const LIMIT = ED_ORDER < SECP_ORDER ? ED_ORDER : SECP_ORDER;
30
20
 
31
- // Отбор с отбраковкой: даёт равномерное распределение, в отличие от приведения по модулю,
32
- // которое перекашивает вероятности в сторону малых значений.
21
+ // Rejection sampling gives a uniform distribution; a modulo reduction would skew it towards small values.
33
22
  function newHalf() {
34
23
  for (let i = 0; i < 256; i++) {
35
24
  const bytes = randomBytes(32);
@@ -37,34 +26,28 @@ export function createHalves({ ed25519, secp256k1, keccak256, randomBytes }) {
37
26
  for (const b of bytes) v = (v << 8n) | BigInt(b);
38
27
  if (v > 0n && v < LIMIT) return v;
39
28
  }
40
- throw new Error("newHalf: не удалось получить скаляр за 256 попыток");
29
+ throw new Error("newHalf: could not get a scalar in 256 attempts");
41
30
  }
42
31
 
43
- // ВЫВОД ПОЛОВИНЫ ИЗ СЕКРЕТА, А НЕ ИЗ СЛУЧАЙНОСТИ. Нужен ноде провайдера: она не хранит половину под
44
- // каждый ордер, а выводит её заново из своего ключа и привязки к ордеру. Один и тот же ордер даёт одну
45
- // и ту же половину, разные ордера - разные, посчитать может только владелец секрета.
32
+ // DERIVE THE HALF FROM A SECRET, NOT RANDOMNESS: the provider node derives it again from its key and the
33
+ // order binding, so the same order always gives the same half, and only the secret holder can compute it.
46
34
  //
47
- // ОТБОР С ОТБРАКОВКОЙ, А НЕ ПРИВЕДЕНИЕ ПО МОДУЛЮ - по той же причине, что и в newHalf выше: приведение
48
- // перекашивает распределение в сторону малых значений, а половина идёт в адрес Monero, где
49
- // распределение обязано быть равномерным. Поэтому берём хеш, и если он не меньше порядка группы -
50
- // меняем счётчик и хешируем снова. Счётчик входит в хеш, значит весь путь воспроизводим: тот же ордер
51
- // при том же секрете даёт ту же половину и на той же попытке.
35
+ // Rejection sampling, not modulo, as in newHalf: the half feeds a Monero address and must be uniform.
36
+ // The attempt counter is part of the hash, so the same order and secret give the same half.
52
37
  //
53
- // ПОЧЕМУ keccak, А НЕ HMAC. keccak - губка, к удлинению сообщения она невосприимчива, поэтому порядок
54
- // «домен, секрет, привязка, счётчик» безопасен и не даёт из одного вывода получить другой. Так модуль
55
- // остаётся без зависимости от node:crypto и работает и в браузере.
56
- // Домены половин. Отсюда их берут И котирование, И выплата: разойдись строки - и нода выведет половину,
57
- // которой не соответствует ни её обязательство, ни собранный адрес, и обнаружит это только отказом.
58
- const HALF_DOMAIN = "arrakis-order-half-v1"; // половина ТРАТЫ
59
- const VIEW_HALF_DOMAIN = "arrakis-order-view-v1"; // половина ПРОСМОТРА
38
+ // WHY keccak, NOT HMAC: keccak is a sponge, so the order domain/secret/binding/counter is safe, and the
39
+ // module needs no node:crypto (it runs in the browser too).
40
+ // Half domains. Quoting and payout both take them from here: if the strings diverge, the node derives a
41
+ // half that matches neither its commitment nor the assembled address.
42
+ const HALF_DOMAIN = "ninsei-order-half-v1"; // spend half
43
+ const VIEW_HALF_DOMAIN = "ninsei-order-view-v1"; // view half
60
44
 
61
45
  function halfFromSeed(seedBytes, binding, domain = HALF_DOMAIN) {
62
- if (!seedBytes || seedBytes.length < 16) throw new Error("halfFromSeed: секрет короче 16 байт");
63
- if (!binding) throw new Error("halfFromSeed: не задана привязка к ордеру");
46
+ if (!seedBytes || seedBytes.length < 16) throw new Error("halfFromSeed: secret shorter than 16 bytes");
47
+ if (!binding) throw new Error("halfFromSeed: order binding not set");
64
48
  const dom = new TextEncoder().encode(domain + "|" + String(binding) + "|");
65
49
  for (let counter = 0; counter < 256; counter++) {
66
- // Счётчик попытки лежит ЯВНО и четырьмя байтами, а не подменой соседнего байта: раньше он был
67
- // вписан трюком (инкремент последнего байта строки домена), и такой код читается как загадка.
50
+ // The attempt counter is an explicit 4-byte field, not a byte tweak.
68
51
  const input = new Uint8Array(dom.length + seedBytes.length + 4);
69
52
  input.set(dom, 0);
70
53
  input.set(seedBytes, dom.length);
@@ -77,30 +60,26 @@ export function createHalves({ ed25519, secp256k1, keccak256, randomBytes }) {
77
60
  for (const b of bytes) v = (v << 8n) | BigInt(b);
78
61
  if (v > 0n && v < LIMIT) return v;
79
62
  }
80
- throw new Error("halfFromSeed: не удалось получить скаляр за 256 попыток");
63
+ throw new Error("halfFromSeed: could not get a scalar in 256 attempts");
81
64
  }
82
65
 
83
66
  function pointFromHex(P, hex) { return P.fromHex(hex.startsWith("0x") ? hex.slice(2) : hex); }
84
67
 
85
- // ПОЛОВИНА МОЖЕТ ПРИЙТИ СТРОКОЙ, И ЭТО НОРМАЛЬНО. Файл восстановления хранит половины hex-строками
86
- // (BigInt нельзя положить в JSON - на этом уже один раз молча терялась запись сделки). Значит любой
87
- // вызывающий, который читает файл, обязан получить половину строкой, а арифметика кривой требует
88
- // bigint. Раньше это место принимало только bigint, и восстановление из НАСТОЯЩЕГО файла падало с
89
- // "expected bigint, got string" - а проверка восстановления этого не видела, потому что подавала
90
- // свежие половины. Приводим здесь, в одном месте, чтобы покрыть всех вызывающих сразу.
68
+ // A HALF MAY COME AS A STRING, AND THAT IS NORMAL: the recovery file stores halves as hex (BigInt cannot
69
+ // go into JSON). Convert here to cover all callers at once.
91
70
  function toScalar(v) {
92
71
  if (typeof v === "bigint") return v;
93
72
  if (typeof v === "string") {
94
73
  const t = v.trim().replace(/^0x/, "");
95
- if (!/^[0-9a-fA-F]{1,64}$/.test(t)) throw new Error("половина не похожа на число: " + v.slice(0, 12));
74
+ if (!/^[0-9a-fA-F]{1,64}$/.test(t)) throw new Error("half does not look like a number: " + v.slice(0, 12));
96
75
  return BigInt("0x" + t);
97
76
  }
98
77
  if (typeof v === "number") return BigInt(v);
99
- throw new Error("половина неизвестного вида: " + typeof v);
78
+ throw new Error("half of unknown type: " + typeof v);
100
79
  }
101
80
  function hex(P) { return P.toHex(); }
102
81
 
103
- // Публичные половины на обеих кривых: Ks_i = ks_i*G (edwards25519), Bs_i = ks_i*H (secp256k1).
82
+ // Public halves on both curves: Ks_i = ks_i*G (edwards25519), Bs_i = ks_i*H (secp256k1).
104
83
  function publicHalves(half) {
105
84
  return {
106
85
  ed: hex(ed25519.Point.BASE.multiply(toScalar(half))),
@@ -108,8 +87,7 @@ export function createHalves({ ed25519, secp256k1, keccak256, randomBytes }) {
108
87
  };
109
88
  }
110
89
 
111
- // Сумма половин: и приватно (mod l), и публично (сложение точек). Второе равенство - то,
112
- // на чём держится сборка общего адреса.
90
+ // Sum of halves: privately (mod l) and publicly (point addition). The second equality is what the joint address rests on.
113
91
  function combineHalves(a, b) { return (toScalar(a) + toScalar(b)) % ED_ORDER; }
114
92
  function combinePublic(edHexA, edHexB) {
115
93
  return hex(pointFromHex(ed25519.Point, edHexA).add(pointFromHex(ed25519.Point, edHexB)));
@@ -1,14 +1,13 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/atomic/order-client.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
+ // Worker client: turns message passing into an ordinary call with a progress callback.
7
6
  //
8
- // Отдельный модуль нужен, чтобы страница не знала ничего про postMessage и идентификаторы запросов:
9
- // она вызывает buildSide(...) и получает обещание плюс этапы.
7
+ // A separate module is needed so the page knows nothing about postMessage and request ids:
8
+ // it calls buildSide(...) and gets a promise plus the stages.
10
9
  export function createOrderWorker() {
11
- if (typeof Worker === "undefined") throw new Error("воркеры недоступны в этом окружении");
10
+ if (typeof Worker === "undefined") throw new Error("workers are unavailable in this environment");
12
11
  const worker = new Worker(new URL("./order-worker.js", import.meta.url), { type: "module" });
13
12
  const pending = new Map();
14
13
  let seq = 0;
@@ -20,10 +19,10 @@ export function createOrderWorker() {
20
19
  if (msg.kind === "progress") { entry.onProgress?.(msg.stage, msg.detail); return; }
21
20
  pending.delete(msg.id);
22
21
  if (msg.kind === "done") entry.resolve(msg.result);
23
- else entry.reject(new Error(msg.message || "воркер вернул ошибку"));
22
+ else entry.reject(new Error(msg.message || "worker returned an error"));
24
23
  };
25
24
  worker.onerror = (event) => {
26
- const err = new Error("воркер упал: " + (event.message || "без сообщения"));
25
+ const err = new Error("worker crashed: " + (event.message || "no message"));
27
26
  for (const [, entry] of pending) entry.reject(err);
28
27
  pending.clear();
29
28
  };
@@ -40,17 +39,17 @@ export function createOrderWorker() {
40
39
  buildSide: (orderContext, onProgress) => call("newSide", { orderContext }, onProgress),
41
40
  sealHalf: (orderContext, ownHalf, encPub, onProgress) => call("seal", { orderContext, ownHalf, counterparty: { encPub } }, onProgress),
42
41
  verifyCounterparty: (orderContext, counterparty, sealedBytes, onProgress) => call("verify", { orderContext, counterparty, sealedBytes }, onProgress),
43
- // Обязательство на половину. Считается в воркере, потому что там же живёт криптография: наружу
44
- // уходит только хеш, половина остаётся внутри и в главный поток не попадает.
42
+ // Commitment to a half. Computed in the worker, because that is where the cryptography lives:
43
+ // only the hash goes out, the half stays inside and never reaches the main thread.
45
44
  halfCommitment: (ownHalf) => call("commit", { ownHalf }),
46
- // Точка по половине: половина контрагента приходит из котировки, а для адреса нужна точка.
45
+ // Point from a half: the counterparty's half comes from the quote, and the address needs a point.
47
46
  pointOf: (ownHalf) => call("pointOf", { ownHalf }),
48
- // Общий адрес Monero из половин - в воркере, потому что там же криптография.
47
+ // Joint Monero address from the halves - in the worker, because that is where the cryptography lives.
49
48
  jointAddress: (joint) => call("jointAddress", { joint }),
50
- // МАТЕРИАЛ МЕЙКЕРА ОБРАТНОЙ ЗАЯВКИ: обе половины, их точки и обязательство. Доказательства здесь нет и
51
- // быть не может: в обратном направлении привязку ордера назначает нода, и до билета доказывать нечего
52
- // (order-worker.js, newMakerMaterial). Публичная часть уходит ноде в заявке, приватная остаётся у
53
- // мейкера - ею забирают выплату и видят приход XMR.
49
+ // MAKER MATERIAL OF THE REVERSE REQUEST: both halves, their points and the commitment. There is no proof
50
+ // here and there cannot be: in the reverse direction the node assigns the order binding, and there is
51
+ // nothing to prove before the ticket (order-worker.js, newMakerMaterial). The public part goes to the node
52
+ // in the request, the private part stays with the maker - it is used to claim the payout and see XMR arrival.
54
53
  makerMaterial: () => call("newMakerMaterial"),
55
54
  stop: () => worker.terminate(),
56
55
  };