@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,98 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/mock/chain.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
+ // Мок-чейн: escrow-контракт на EVM и Monero-нога.
7
+ //
8
+ // Здесь НЕТ реальных транзакций. Всё, что возвращается, помечено как mock, а хеши
9
+ // детерминированы от id сделки - чтобы демо было воспроизводимо.
10
+ // Точка подмены: когда появится контракт, эти функции заменяются на вызовы ethers.js,
11
+ // а интерфейс (fund/lock/claim/refund/sweep) сохраняется.
12
+
13
+ import { randomHex } from "../core/format.js";
14
+ import * as node from "../monero/node.js";
15
+
16
+ function hashOf(...parts) {
17
+ const payload = parts.join("|");
18
+ let hh = 0x811c9dc5;
19
+ for (let i = 0; i < payload.length; i++) {
20
+ hh ^= payload.charCodeAt(i);
21
+ hh = (hh * 0x01000193) >>> 0;
22
+ }
23
+ return "0x" + hh.toString(16).padStart(8, "0") + randomHex(28);
24
+ }
25
+
26
+ // CREATE2-адрес per-order escrow (litepaper §8: свой контракт на сделку, без admin).
27
+ export function escrowAddress(swapId) {
28
+ return "0x" + hashOf("escrow", swapId).slice(2, 42);
29
+ }
30
+
31
+ // Адрес совместного Monero-выхода (S_a + S_b, V_a + V_b в реальном протоколе).
32
+ export function moneroLockAddress(swapId) {
33
+ return "8" + hashOf("xmrlock", swapId).slice(2, 96);
34
+ }
35
+
36
+ export function fundEscrow({ swapId, ethAmountUsd, ethUsd }) {
37
+ return {
38
+ mock: true,
39
+ txHash: hashOf("fund", swapId),
40
+ escrow: escrowAddress(swapId),
41
+ ethAmount: ethUsd > 0 ? ethAmountUsd / ethUsd : 0,
42
+ valueUsd: ethAmountUsd,
43
+ gasUsd: 0.21,
44
+ // ИМЯ МЕТОДА НЕ ВЫДУМЫВАЕМ. Здесь стояло lockAndSwap(bytes32 quoteHash, bytes makerSig) - такого
45
+ // метода в контракте нет вовсе, но именно эта строка уезжала в запись сделки и показывалась на экране
46
+ // как "Escrow method". Демо-путь денег не двигает, поэтому честная подпись для него - эта, без
47
+ // выдуманной сигнатуры. Настоящее имя вызова живой путь записывает сам, из фактического вызова.
48
+ method: "simulated funding (no real transaction)",
49
+ confirmations: 1,
50
+ };
51
+ }
52
+
53
+ export function lockXmr({ swapId, xmrAmount }) {
54
+ return {
55
+ mock: true,
56
+ txid: hashOf("xmrtx", swapId),
57
+ address: moneroLockAddress(swapId),
58
+ amount: xmrAmount,
59
+ viewKeyVerified: true,
60
+ unlocked: "1 of 2 spend key shares revealed (DLEQ bound)",
61
+ };
62
+ }
63
+
64
+ export function sweepXmr({ swapId }) {
65
+ return { mock: true, txid: hashOf("sweep", swapId) };
66
+ }
67
+
68
+ export function refundEscrow({ swapId, by }) {
69
+ return { mock: true, txHash: hashOf("refund", swapId, by), by };
70
+ }
71
+
72
+ // Подтверждения Monero-ноды: 10 блоков по 2 минуты (litepaper §5.0).
73
+ //
74
+ // chainMode=mock - подтверждения идут по sim-времени (быстро, для показа потока).
75
+ // chainMode=live - подтверждения считаются по реальному росту высоты mainnet-нода:
76
+ // сделка тогда честно длится ~20 минут независимо от скорости демки. Если нод
77
+ // недоступен или высота на момент лока не зафиксирована, откатываемся на sim-счётчик.
78
+ export function confirmations(swap, simElapsedMs) {
79
+ const target = swap.timeline.confTarget;
80
+
81
+ if (swap.chainMode === "live") {
82
+ const lockHeight = swap.xmr && swap.xmr.lockHeight;
83
+ const snap = node.snapshot();
84
+ if (lockHeight && snap && typeof snap.height === "number" && snap.height >= lockHeight) {
85
+ return Math.max(0, Math.min(target, snap.height - lockHeight));
86
+ }
87
+ return swap.xmr ? 0 : 0;
88
+ }
89
+
90
+ const started = swap.timeline.lockAtSim;
91
+ if (started === null || simElapsedMs < started) return 0;
92
+ const blocks = Math.floor((simElapsedMs - started) / swap.timeline.blockSim);
93
+ return Math.max(0, Math.min(target, blocks));
94
+ }
95
+
96
+ export function explorerTx(hash) {
97
+ return "https://arbiscan.io/tx/" + hash;
98
+ }
@@ -0,0 +1,265 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/monero/address.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
+ // Проверка и сборка Monero-адресов: формат + настоящий checksum + префиксы всех сетей.
7
+ //
8
+ // Важная деталь, на которой легко ошибиться: у Monero СВОЙ base58 - блочный
9
+ // (8 байт кодируются 11 символами, остаток идёт в конце). Он НЕ совместим с обычным
10
+ // биткойн-base58: там ведущий символ '1' означает нулевой байт, а у Monero блок кодируется
11
+ // как ЧИСЛО, и '1' может быть просто цифрой внутри числа. Поэтому ни bs58.decode, ни
12
+ // bs58.encode здесь не годятся - base58 реализован ниже численно (см. base58ToBlock).
13
+ //
14
+ // Зачем это нужно в демке: без проверки checksum опечатка в адресе получения
15
+ // выглядит валидной, а в atomic-свопе ошибка в адресе означает потерянные деньги.
16
+ //
17
+ // Префиксы сетей (из src/cryptonote_config.h, официальные значения):
18
+ // mainnet: 18 обычный, 19 с payment id, 42 суб-адрес
19
+ // stagenet: 24 / 25 / 36
20
+ // testnet: 53 / 54 / 63
21
+ // Вендоренная monero-js умеет ТОЛЬКО mainnet (у неё в address.js жёстко 18/19/42),
22
+ // поэтому здесь свой энкодер: библиотека даёт публичные ключи, а строку адреса собираем мы.
23
+
24
+ const BLOCK_CHARS = 11;
25
+
26
+ // encoded bytes -> chars (из monero-js/src/base58.js, ENC_BLOCK_BYTE_LENS)
27
+ const CHARS_BY_BYTES = { 1: 2, 2: 3, 3: 5, 4: 6, 5: 7, 6: 9, 7: 10, 8: 11 };
28
+ const BYTES_BY_CHARS = { 2: 1, 3: 2, 5: 3, 6: 4, 7: 5, 9: 6, 10: 7, 11: 8 };
29
+
30
+ const PREFIXES = {
31
+ 18: { network: "mainnet", kind: "primary" },
32
+ 19: { network: "mainnet", kind: "integrated" },
33
+ 42: { network: "mainnet", kind: "subaddress" },
34
+ 24: { network: "stagenet", kind: "primary" },
35
+ 25: { network: "stagenet", kind: "integrated" },
36
+ 36: { network: "stagenet", kind: "subaddress" },
37
+ 53: { network: "testnet", kind: "primary" },
38
+ 54: { network: "testnet", kind: "integrated" },
39
+ 63: { network: "testnet", kind: "subaddress" },
40
+ };
41
+
42
+ // обратная таблица: сеть + вид -> байт префикса (для сборки адреса)
43
+ export const PREFIX_BY_NETWORK = {
44
+ mainnet: { primary: 18, integrated: 19, subaddress: 42 },
45
+ stagenet: { primary: 24, integrated: 25, subaddress: 36 },
46
+ testnet: { primary: 53, integrated: 54, subaddress: 63 },
47
+ };
48
+
49
+ export function prefixFor(network, kind = "primary") {
50
+ const byKind = PREFIX_BY_NETWORK[network];
51
+ if (!byKind) throw new Error("Unknown network: " + network);
52
+ const prefix = byKind[kind];
53
+ if (prefix === undefined) throw new Error("Unknown address kind: " + kind);
54
+ return prefix;
55
+ }
56
+
57
+ // Синхронная проверка формата (для UI до загрузки библиотеки).
58
+ export function formatCheck(addr) {
59
+ const a = String(addr || "").trim();
60
+ if (![95, 106].includes(a.length)) return false;
61
+ if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(a)) return false;
62
+ // первая буква адреса определяется префиксом сети: 4/8 mainnet, 5/7 stagenet, 9/A/B testnet
63
+ return ["4", "5", "7", "8", "9", "A", "B"].includes(a[0]);
64
+ }
65
+
66
+ // Первая буква адреса прямо следует из префикса: 4/8 - mainnet, 5/7 - stagenet, 9/A/B - testnet
67
+ export function networkFromShape(addr) {
68
+ const a = String(addr || "").trim();
69
+ if (!formatCheck(a)) return null;
70
+ if (a[0] === "4" || a[0] === "8") return "mainnet";
71
+ if (a[0] === "5" || a[0] === "7") return "stagenet";
72
+ if (["9", "A", "B"].includes(a[0])) return "testnet";
73
+ return null;
74
+ }
75
+
76
+ export function isStagenetShaped(addr) {
77
+ return networkFromShape(addr) === "stagenet";
78
+ }
79
+
80
+ let lib = null;
81
+ let libFailed = false;
82
+
83
+ async function loadLib() {
84
+ if (lib) return lib;
85
+ if (libFailed) return null;
86
+ try {
87
+ const mod = await import("../../assets/vendors/monero/monero-js-browser.js?v=05c682ea");
88
+ lib = mod.default || mod;
89
+ return lib;
90
+ } catch {
91
+ libFailed = true;
92
+ return null;
93
+ }
94
+ }
95
+
96
+ // Алфавит Monero-base58 (тот же, что у Bitcoin, но правила кодирования другие).
97
+ const B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
98
+ const B58_VALUES = (() => {
99
+ const m = new Map();
100
+ [...B58_ALPHABET].forEach((c, i) => m.set(c, BigInt(i)));
101
+ return m;
102
+ })();
103
+
104
+ // Блок байтов -> base58-строка (числом: никакого «ведущий '1' = нулевой байт»).
105
+ function blockToBase58(block) {
106
+ let num = 0n;
107
+ for (const b of block) num = (num << 8n) | BigInt(b);
108
+ let s = "";
109
+ while (num > 0n) {
110
+ s = B58_ALPHABET[Number(num % 58n)] + s;
111
+ num /= 58n;
112
+ }
113
+ return s;
114
+ }
115
+
116
+ // base58-строка блока -> ровно `bytes` байтов, выравнивание по ПРАВОМУ краю.
117
+ //
118
+ // Почему не bs58 из npm: его декодер трактует каждый ведущий символ '1' как нулевой БАЙТ
119
+ // (биткойн-семантика). В Monero блок кодируется как число, и '1' может быть просто цифрой
120
+ // внутри числа - тогда bs58 выдаёт лишний нулевой байт, payload разъезжается, и checksum
121
+ // ложно не сходится. На этом ловились: 18 адресов из 120 (15%) проверялись как «с опечаткой».
122
+ function base58ToBlock(str, bytes) {
123
+ let num = 0n;
124
+ for (const ch of str) {
125
+ const v = B58_VALUES.get(ch);
126
+ if (v === undefined) throw new Error("Invalid Monero base58 character: " + ch);
127
+ num = num * 58n + v;
128
+ }
129
+ const out = new Uint8Array(bytes);
130
+ for (let i = bytes - 1; i >= 0; i--) {
131
+ out[i] = Number(num & 0xffn);
132
+ num >>= 8n;
133
+ }
134
+ if (num !== 0n) throw new Error("Base58 block does not fit in " + bytes + " bytes");
135
+ return out;
136
+ }
137
+
138
+ // Блочный Monero-base58: строка -> байты.
139
+ //
140
+ // Порядок блоков важен: Monero сначала кодирует полные 8-байтовые блоки (по 11 символов),
141
+ // а НЕПОЛНЫЙ остаток идёт В КОНЦЕ и занимает меньше символов (см. monero-js/src/base58.js:
142
+ // сначала tot_block_cnt блоков, затем last_block_enc_len). Поэтому остаток берём с конца строки.
143
+ export function decodeMoneroBase58(str) {
144
+ const s = String(str);
145
+ const L = s.length;
146
+ const rem = L % BLOCK_CHARS;
147
+ const lastChars = rem === 0 ? BLOCK_CHARS : rem;
148
+ const lastBytes = BYTES_BY_CHARS[lastChars];
149
+ if (!lastBytes) throw new Error("Invalid Monero base58 length");
150
+
151
+ const chunks = [];
152
+ const headLen = L - lastChars;
153
+ for (let i = 0; i < headLen; i += BLOCK_CHARS) chunks.push(base58ToBlock(s.slice(i, i + BLOCK_CHARS), 8));
154
+ chunks.push(base58ToBlock(s.slice(headLen), lastBytes));
155
+
156
+ const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
157
+ let offset = 0;
158
+ for (const c of chunks) { out.set(c, offset); offset += c.length; }
159
+ return out;
160
+ }
161
+
162
+ // Блочный Monero-base58: байты -> строка (обратная к decode выше).
163
+ // Полные 8-байтовые блоки кодируются по 11 символов (слева дополняются '1'), остаток - в конце.
164
+ export function encodeMoneroBase58(bytes) {
165
+ const arr = Array.from(bytes);
166
+ const L = arr.length;
167
+ const rem = L % 8;
168
+ const lastBytes = rem === 0 ? 8 : rem;
169
+ const headLen = L - lastBytes;
170
+
171
+ let out = "";
172
+ for (let i = 0; i < headLen; i += 8) {
173
+ const s = blockToBase58(arr.slice(i, i + 8));
174
+ out += "1".repeat(Math.max(0, CHARS_BY_BYTES[8] - s.length)) + s;
175
+ }
176
+ const t = blockToBase58(arr.slice(headLen));
177
+ out += "1".repeat(Math.max(0, CHARS_BY_BYTES[lastBytes] - t.length)) + t;
178
+ return out;
179
+ }
180
+
181
+ // Полезная нагрузка адреса (без checksum): префикс + публичный spend + публичный view [+ payment id]
182
+ export function addressPayload({ network, kind = "primary", spendPub, viewPub, paymentId }) {
183
+ const prefix = prefixFor(network, kind);
184
+ const spend = hexToBytes(spendPub, 32, "public spend key");
185
+ const view = hexToBytes(viewPub, 32, "public view key");
186
+ const out = [prefix, ...spend, ...view];
187
+ if (kind === "integrated") {
188
+ const pid = hexToBytes(paymentId, 8, "payment id");
189
+ out.push(...pid);
190
+ }
191
+ return Uint8Array.from(out);
192
+ }
193
+
194
+ function hexToBytes(hex, expectedBytes, label) {
195
+ const s = String(hex || "").trim();
196
+ if (!new RegExp("^[0-9a-fA-F]{" + expectedBytes * 2 + "}$").test(s)) {
197
+ throw new Error("Bad " + label + ": expected " + expectedBytes * 2 + " hex characters");
198
+ }
199
+ const out = new Uint8Array(expectedBytes);
200
+ for (let i = 0; i < expectedBytes; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
201
+ return out;
202
+ }
203
+
204
+ // Сборка адреса из публичных ключей: deps = { keccak256 } (в браузере - из бандла monero-js).
205
+ // bs58 здесь не нужен: base58 у Monero блочный и числовой, см. комментарий у base58ToBlock.
206
+ export function addressFromKeys({ network, kind = "primary", spendPub, viewPub, paymentId }, deps) {
207
+ if (!deps?.keccak256) throw new Error("addressFromKeys needs { keccak256 }");
208
+ const payload = addressPayload({ network, kind, spendPub, viewPub, paymentId });
209
+ const digest = Uint8Array.from(deps.keccak256(deps.Buffer ? deps.Buffer.from(payload) : payload));
210
+ const full = new Uint8Array(payload.length + 4);
211
+ full.set(payload, 0);
212
+ full.set(digest.slice(0, 4), payload.length);
213
+ return encodeMoneroBase58(full);
214
+ }
215
+
216
+ // Полная проверка: формат + блочный декод + keccak256-checksum + сеть.
217
+ // expectNetwork (необязательно): если задан, адрес чужой сети считается ошибкой -
218
+ // в свапе на stagenet mainnet-адрес это потерянные деньги, а не придирка.
219
+ export async function checkAddress(addr, opts = {}) {
220
+ const a = String(addr || "").trim();
221
+ if (!formatCheck(a)) {
222
+ return { ok: false, reason: "Not a Monero address (expected 95 or 106 base58 characters)" };
223
+ }
224
+ const shape = networkFromShape(a);
225
+ if (opts.expectNetwork && shape && shape !== opts.expectNetwork) {
226
+ return { ok: false, reason: `This is a ${shape} address, but the swap runs on ${opts.expectNetwork}` };
227
+ }
228
+ const l = await loadLib();
229
+ if (!l) {
230
+ return { ok: true, verified: false, network: shape, reason: "Format looks valid (checksum check unavailable: monero-js bundle is not built)" };
231
+ }
232
+ try {
233
+ const bytes = decodeMoneroBase58(a);
234
+ const payloadLen = bytes.length - 4;
235
+ if (payloadLen < 65) throw new Error("Address is too short");
236
+ const payload = bytes.slice(0, payloadLen);
237
+ const checksum = bytes.slice(payloadLen);
238
+ // keccak256 из бандла требует Buffer: Uint8Array он не принимает
239
+ const digest = Uint8Array.from(l.keccak256(l.Buffer.from(payload)));
240
+ const match = checksum.length === 4 && checksum.every((b, i) => b === digest[i]);
241
+ if (!match) {
242
+ // Мок-адреса демки (wallet.js, режим mock) намеренно без checksum: они помечены в UI как
243
+ // фиктивные. Если такой адрес попадёт сюда, «опечатка» - не единственная версия, и текст
244
+ // должен это говорить, иначе отладка уходит не туда.
245
+ return { ok: false, reason: "Checksum mismatch: the address has a typo, is corrupted, or is a demo mock address (mock addresses have no checksum)" };
246
+ }
247
+
248
+ const prefix = payload[0];
249
+ const meta = PREFIXES[prefix] || { network: "unknown", kind: "unknown" };
250
+ if (meta.network === "unknown") return { ok: false, reason: `Unsupported address prefix ${prefix}` };
251
+ if (opts.expectNetwork && meta.network !== opts.expectNetwork) {
252
+ return { ok: false, reason: `This is a ${meta.network} address, but the swap runs on ${opts.expectNetwork}` };
253
+ }
254
+ return { ok: true, verified: true, network: meta.network, kind: meta.kind, bytes: bytes.length };
255
+ } catch (e) {
256
+ return { ok: false, reason: "Address decode failed: " + e.message };
257
+ }
258
+ }
259
+
260
+ // Хелпер для кода, который уже загрузил бандл: собрать адрес нужной сети из ключей кошелька.
261
+ export async function addressForNetwork({ network, kind = "primary", spendPub, viewPub }) {
262
+ const l = await loadLib();
263
+ if (!l) throw new Error("monero-js bundle is not available");
264
+ return addressFromKeys({ network, kind, spendPub, viewPub }, { keccak256: l.keccak256, Buffer: l.Buffer });
265
+ }
@@ -0,0 +1,68 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/monero/halvesSweep.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
+ // потребителя: страница вывода (sweep.js) и проверка (tools/check-sweep-halves.mjs). Пока код жил прямо на
8
+ // странице, проверить его можно было только в браузере, а вторая копия в проверке неизбежно разошлась бы с
9
+ // первой - и заметил бы это не тот, кто ошибся.
10
+ //
11
+ // ПОЧЕМУ ЭТО ГЛАВНАЯ ФУНКЦИЯ ВСЕГО ВЫВОДА. Контракт убеждается лишь в том, что ХЕШ раскрытой половины совпал
12
+ // с обязательством. Связь «раскрытая половина и точка в адресе» проверить он не может: ed25519 в EVM нет.
13
+ // Значит её проверяем мы, и ДО всякой синхронизации: (s_a + s_b)*G обязана совпасть с публичным ключом траты
14
+ // ордера. Не совпало - не метём и говорим прямо.
15
+ //
16
+ // Секреты здесь не создаются: обе половины уже есть у стороны (своя - в файле, чужая - раскрыта в цепи).
17
+ export function spendKeysFromRevealedHalf({
18
+ halves, ownSpendHalf, ownViewHalf, otherSpendPoint, otherViewHalf, revealedHalf,
19
+ }) {
20
+ if (!halves) throw new Error("нужна математика половин");
21
+ for (const [name, v] of Object.entries({ ownSpendHalf, ownViewHalf, otherSpendPoint, otherViewHalf, revealedHalf })) {
22
+ if (v === undefined || v === null || v === "") throw new Error("не задано: " + name);
23
+ }
24
+ const sum = halves.combineHalves(BigInt(ownSpendHalf), BigInt(revealedHalf));
25
+ const sumPub = halves.publicHalves(sum).ed;
26
+ // Ожидаемый ключ траты собирается ИЗ СВОЕЙ ПОЛОВИНЫ И ЧУЖОЙ ТОЧКИ - то есть из того, что было в ордере.
27
+ // ПОЛОВИНА ИЗ ФАЙЛА ПРИХОДИТ СТРОКОЙ, И ЭТО НАДО УЧЕСТЬ КАЖДЫЙ РАЗ. Строкой она была и в сумме, и здесь -
28
+ // здесь забыли, и вывод XMR падал на живом стенде с «invalid field element: expected bigint, got string»
29
+ // уже ПОСЛЕ чтения раскрытой половины из цепи: до этой строки дело доходило, а дальше нет.
30
+ const expected = halves.combinePublic(halves.publicHalves(BigInt(ownSpendHalf)).ed, otherSpendPoint);
31
+ const norm = (h) => String(h).replace(/^0x/i, "").toLowerCase();
32
+ if (norm(sumPub) !== norm(expected)) {
33
+ throw new Error(
34
+ "раскрытая половина не соответствует адресу: (s_a + s_b)*G != ключ траты ордера. " +
35
+ "Половина из другого ордера или подменена - метать нельзя"
36
+ );
37
+ }
38
+ // Полный ключ ПРОСМОТРА - тоже сумма: своей половины из файла и половины контрагента. Тратить он не даёт,
39
+ // а без него кошелёк не найдёт наши XMR в цепи: ключи просмотра и траты в Monero независимы.
40
+ const viewSum = halves.combineHalves(BigInt(ownViewHalf), BigInt(otherViewHalf));
41
+ // ПОРЯДОК БАЙТ - ЭТО ФОРМАТ, А НЕ КОСМЕТИКА, И ЗДЕСЬ ОН БЫЛ НЕВЕРЕН ДО 28.09.2026.
42
+ // Прежняя редакция писала скаляр обычной записью числа: старший байт первым. Библиотека ждёт ЗАПИСЬ
43
+ // MONERO - младшим байтом вперёд, как скаляр лежит в памяти.
44
+ //
45
+ // ЗАМЕР 28.09.2026, ОБЕ СБОРКИ monero-ts 0.11.16 (нативная в узле и вендорная в браузере), ОДНИ И ТЕ ЖЕ
46
+ // ключи: своя же строка (createWalletKeys().getPrivateSpendKey()) принимается КАК ЕСТЬ и публичный ключ
47
+ // совпадает, а развёрнутая отвергается словами "failed to verify secret spend key". Обе сборки ведут себя
48
+ // ОДИНАКОВО - значит расхождение было нашим, а не библиотеки: прежняя запись числа давала развёрнутый
49
+ // порядок, и обе точки входа чинили это обходным путём (страница вывода - reversedKey, инструмент узла -
50
+ // flip). Обходные пути оставлены КАК СЕТЬ для чужого файла, но срабатывать не должны, и срабатывание
51
+ // видно в журнале.
52
+ //
53
+ // ПРЕФИКСА 0x ЗДЕСЬ НЕТ И БЫТЬ НЕ ДОЛЖНО: библиотека выдаёт ключ 64 hex без префикса и такой принимает,
54
+ // а вид "0x"+64 hex отвергает на разборе - "failed to parse secret spend key".
55
+ const be32 = (v) => BigInt(v).toString(16).padStart(64, "0"); // обычная запись числа, старший байт первым
56
+ const le32 = (v) => be32(v).match(/../g).reverse().join(""); // запись Monero: младший байт первым
57
+ return { privateSpendKey: le32(sum), privateViewKey: le32(viewSum) };
58
+ }
59
+
60
+ // ОБРАТНЫЙ ПЕРЕХОД - ТОЖЕ ОДНА ФУНКЦИЯ НА ВСЕХ. Кто читает выданный ключ как ЧИСЛО, обязан развернуть порядок:
61
+ // обычная запись числа (BigInt("0x" + ключ)) прочтёт запись Monero задом наперёд и даст другой скаляр - молча,
62
+ // без всякой ошибки. Ровно на этом мы уже стояли: и проверка, и инструмент пары разбирали ключ напрямую, и
63
+ // после исправления порядка байт оба обязаны были бы покраснеть - если бы их не поправили вместе с модулем.
64
+ export function scalarFromMoneroHex(h) {
65
+ const hex = String(h).replace(/^0x/i, "");
66
+ if (!/^[0-9a-f]{64}$/.test(hex)) throw new Error("ключ не в записи Monero: ожидались 64 hex");
67
+ return BigInt("0x" + hex.match(/../g).reverse().join(""));
68
+ }
@@ -0,0 +1,110 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/monero/node.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
+ // Живой Monero-нод: единственная реальная внешняя зависимость демки.
7
+ //
8
+ // Адрес ноды - константа MONERO.node в www/js/core/config.js: наш прокси
9
+ // https://xmr.arrakisswap.trade (конфиг .hermes/deploy/arrakis-xmr-rpc.conf).
10
+ // Никаких параметров и переопределений у демки нет: адрес один и виден в коде.
11
+ // Используем ноду для двух честных вещей:
12
+ // 1) статус сети в шапке: реальная высота, nettype, доступность нода;
13
+ // 2) режим "live chain": подтверждения Monero считаются по реальному росту высоты,
14
+ // а не по таймеру (медленно - блок ~2 минуты, зато по-настоящему).
15
+ //
16
+ // Балансы, outputs и ключи через этот RPC не запрашиваются: для этого нужен кошелёк
17
+ // (wallet2 в WASM + view-ключ), это следующий шаг, а не то, что делает демка.
18
+
19
+ import { MONERO } from "../core/config.js";
20
+
21
+ const RPC = MONERO.node;
22
+ const TIMEOUT_MS = 8000;
23
+ const CACHE_MS = 15_000;
24
+
25
+ let cache = { at: 0, data: null, error: null };
26
+ let inflight = null;
27
+
28
+ async function rpc(method, params = {}, path = "/json_rpc") {
29
+ const ctl = new AbortController();
30
+ const timer = setTimeout(() => ctl.abort(), TIMEOUT_MS);
31
+ try {
32
+ const res = await fetch(RPC + path, {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: JSON.stringify({ jsonrpc: "2.0", id: "arrakis-demo", method, params }),
36
+ signal: ctl.signal,
37
+ });
38
+ if (!res.ok) throw new Error("HTTP " + res.status);
39
+ const json = await res.json();
40
+ if (json.error) throw new Error(json.error.message || "RPC error");
41
+ return json.result;
42
+ } finally {
43
+ clearTimeout(timer);
44
+ }
45
+ }
46
+
47
+ export function snapshot() {
48
+ return cache.data;
49
+ }
50
+
51
+ export function lastError() {
52
+ return cache.error;
53
+ }
54
+
55
+ // Высота сети на момент вызова (из кеша, если он свежий).
56
+ export async function height() {
57
+ const info = await getInfo();
58
+ return info ? info.height : null;
59
+ }
60
+
61
+ export async function getInfo({ force = false } = {}) {
62
+ const fresh = cache.data && Date.now() - cache.at < CACHE_MS;
63
+ if (fresh && !force) return cache.data;
64
+ if (inflight) return inflight;
65
+
66
+ inflight = rpc("get_info")
67
+ .then((info) => {
68
+ cache = { at: Date.now(), data: normalize(info), error: null };
69
+ return cache.data;
70
+ })
71
+ .catch((e) => {
72
+ cache = { at: Date.now(), data: null, error: e.message };
73
+ return null;
74
+ })
75
+ .finally(() => {
76
+ inflight = null;
77
+ });
78
+ return inflight;
79
+ }
80
+
81
+ function normalize(info) {
82
+ return {
83
+ height: info.height,
84
+ nettype: info.nettype || (info.mainnet ? "mainnet" : info.stagenet ? "stagenet" : "testnet"),
85
+ offline: !!info.offline,
86
+ busySyncing: !!info.busy_syncing,
87
+ version: info.version,
88
+ target: info.target,
89
+ txPoolSize: info.tx_pool_size,
90
+ outgoing: info.outgoing_connections_count,
91
+ incoming: info.incoming_connections_count,
92
+ databaseSizeGb: info.database_size ? Math.round(info.database_size / 1024 ** 3) : null,
93
+ fetchedAt: Date.now(),
94
+ };
95
+ }
96
+
97
+ // Опрос раз в 30 секунд, пока страница открыта: шапка показывает живую высоту.
98
+ let poller = null;
99
+ export function startPolling(onUpdate, intervalMs = 30_000) {
100
+ if (poller) clearInterval(poller);
101
+ const tick = async () => {
102
+ const info = await getInfo({ force: true });
103
+ if (onUpdate) onUpdate(info, lastError());
104
+ };
105
+ tick();
106
+ poller = setInterval(tick, intervalMs);
107
+ return () => clearInterval(poller);
108
+ }
109
+
110
+ export const nodeUrl = RPC;