@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,193 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/core/rfqSource.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
+ // ЖИВЫЕ ИНДИКАТИВНЫЕ КОТИРОВКИ ОТ НАШЕГО ПРОВАЙДЕРА - ЧЕРЕЗ ЯДРО SDK.
7
+ //
8
+ // ПОЧЕМУ ЧЕРЕЗ ЯДРО, А НЕ СВОИМ ЗАПРОСОМ. Раньше этот модуль САМ ходил в /api/rate и САМ разбирал книгу:
9
+ // правила чтения (ноль = "направления нет", просрочка по СОБСТВЕННОЙ метке котировки, строка, противоречащая
10
+ // себе) жили здесь, а рядом - в ядре (sdk/src/quotes.mjs). Две редакции одних правил расходятся молча
11
+ // (док 41/54), поэтому книга теперь берётся у ЯДРА: опрос - quotes.watch, снимок - quotes.snapshot, а
12
+ // ПЕРЕВОД ФОРМЫ делает склейник (www/js/sdk/bridge.js). Здесь не осталось НИ ОДНОГО правила отбора: что
13
+ // предложение, что отказ, что лучший - решает ядро. Молчание или отказ ядра даёт ПУСТУЮ книгу: прежняя
14
+ // (движковая) книга на экран не подставляется.
15
+ //
16
+ // ЧТО ЗНАЧИТ ЭТА ФОРМА СНИМКА (полный текст формы - .hermes/docs/41-quote-protocol-v4.md; действующие
17
+ // правила приёма - .hermes/docs/54-quote-protocol-v5.md), если коротко:
18
+ // - СТУПЕНЕЙ НЕТ. min/max/step/ttlMs описывают ДИАПАЗОН целиком, а цена одна на весь диапазон: объём либо
19
+ // попадает в диапазон провайдера, либо он за него не берётся;
20
+ // - ВИД КОТИРОВКИ ОДИН. quoteKind (live/fixed) ушёл: твёрдая котировка ровно одна - ордерная;
21
+ // - rate: 0 ЗНАЧИТ "НАПРАВЛЕНИЕ ОТКЛЮЧЕНО" и рядом стоит why. Это ЕДИНСТВЕННЫЙ сигнал отсутствия
22
+ // предложения, поэтому ноль НИКОГДА не становится ценой и НИКОГДА не делится;
23
+ // - СЕТЬ - ЧАСТЬ ЛИЧНОСТИ РЫНКА: assetNetwork/currencyNetwork сверяются со своей сетью (quoteNetworksFit);
24
+ // - seq (номер запроса) и at (метка времени) едут вместе с котировкой: по ним читается свежесть;
25
+ // - ОБЪЁМ МЕРЯЕТСЯ В ЕДИНИЦАХ ASSET ПАРЫ (первого элемента): у XMR/<токен> это XMR, у <токен>/XMR - токен.
26
+ import { API, DEFAULT_CHAIN } from "./config.js";
27
+
28
+ // КОТИРОВКА ПОД КОНКРЕТНЫЙ ОРДЕР (orderQuote). Запрос идёт ЧЕРЕЗ НАШ БЭКЕНД, а не напрямую в ноду
29
+ // провайдера: прямой запрос потребовал бы от ноды заголовков CORS, то есть открытой наружу ноды. Наш
30
+ // бэкенд маршрутизирует запрос и заодно проверяет подпись провайдера по списку разрешённых ключей
31
+ // (док 21, D5.1; док 22, версия 4). Тело котировки бэкенд не хранит и не логирует.
32
+ export async function requestOrderQuote({ providerId, order }) {
33
+ if (!providerId) throw new Error("не выбран провайдер: запрашивать котировку под ордер не у кого");
34
+ // ПУТЬ БЕЗ ВТОРОГО "api": apiBase() уже возвращает "/api", и лишний префикс давал /api/api/order-quote -
35
+ // 404 на каждом запросе. Снаружи это выглядело как «котировка под ордер не запрашивается вовсе», хотя
36
+ // запрос уходил и получал отказ: сбивало с толку и разбор, и поиск причины.
37
+ const res = await fetch(apiBase() + "/order-quote", {
38
+ method: "POST",
39
+ headers: { "content-type": "application/json" },
40
+ body: JSON.stringify({ providerId, order }),
41
+ });
42
+ let body = null;
43
+ try { body = await res.json(); } catch { body = null; }
44
+ if (!res.ok || !body || body.ok !== true || !body.quote) {
45
+ const why = (body && (body.error || body.why)) || ("HTTP " + res.status);
46
+ throw new Error("провайдер не дал котировку под ордер: " + why);
47
+ }
48
+ // ПОДПИСЬ ПРОВЕРЯЕТ БЭКЕНД, НО РЕЗУЛЬТАТ ПРИХОДИТ ПОЛЕМ, А НЕ МОЛЧАНИЕМ. Непроверенная подпись -
49
+ // это не «наверное всё хорошо»: котировка под ордер несёт обязательство по ключам, и принимать её без
50
+ // подтверждения нельзя. Отсутствие флага тоже считаем непроверенной: обещать больше, чем сделано, нельзя.
51
+ if (body.signatureVerified !== true) {
52
+ throw new Error("подпись провайдера не подтверждена: " + String(body.signatureWhy || "бэкенд не проверил"));
53
+ }
54
+ return body.quote;
55
+ }
56
+
57
+ // ПРОВЕРКА КОТИРОВКИ ПОД ОРДЕР - то, ради чего она вообще запрашивается. Проверяющий код тот же, что и
58
+ // у своей стороны: доказательство в контексте ордера, и доказанная точка ed25519 обязана совпасть с той,
59
+ // которую мы отдадим в контракт. Второе без первого бессмысленно, первое без второго не защищает.
60
+ export function checkOrderQuoteShape(quote) {
61
+ // ПОЛОВИНА ПРОСМОТРА В СПИСКЕ ОБЯЗАТЕЛЬНЫХ: без неё по котировке не собрать адрес Monero (док 21, D14.3).
62
+ const missing = ["providerId", "amount", "claimer", "edPointClaimer", "commitHalfClaimer", "viewHalfClaimer", "proof", "context", "expiresAt"]
63
+ .filter((f) => quote[f] === undefined || quote[f] === null || quote[f] === "");
64
+ if (missing.length) throw new Error("в котировке под ордер не хватает полей: " + missing.join(", "));
65
+ // ЕДИНСТВЕННЫЙ ТВЁРДЫЙ ВИД - ОРДЕРНЫЙ. В версии 4 второго вида (fixed) в продукте нет, поэтому проверка
66
+ // не "какой это вид", а "это вообще котировка под ордер или нет": рассылочная котировка сюда не годится.
67
+ if (quote.quoteKind !== "orderQuote") throw new Error("пришла не котировка под ордер, а " + String(quote.quoteKind));
68
+ if (Number(quote.expiresAt) <= Date.now()) throw new Error("котировка под ордер просрочена");
69
+ }
70
+
71
+ let baseOverride = null;
72
+ const apiBase = () => baseOverride || API.base;
73
+
74
+ // ЧТО СПРАШИВАЕМ У ЯДРА. Сторона и РАЗМЕР - в единицах ASSET пары (см. формулировку выше); сеть и токен
75
+ // нужны ядру, чтобы выбрать СВОЙ экземпляр (у каждой сети свой опрос). Своих чисел здесь не появляется:
76
+ // размер приходит от экрана, который выровнял его по сетке провайдера.
77
+ let query = { size: 1, side: "buy", chain: DEFAULT_CHAIN, token: null };
78
+ export function setRfqQuery(next) {
79
+ if (!next) return;
80
+ if (next.side === "buy" || next.side === "sell") query.side = next.side;
81
+ // ВИДА КОТИРОВКИ В ЗАПРОСЕ БОЛЬШЕ НЕТ: второго вида не существует, и поле mode ушло вместе с ним.
82
+ if (next.size !== null && next.size !== undefined && Number(next.size) > 0) query.size = Number(next.size);
83
+ else query.size = 1; // сумма не введена - спрашиваем единицу ASSET, а не "ничего"
84
+ if (typeof next.chain === "string" && next.chain) query.chain = next.chain;
85
+ if (typeof next.token === "string" && next.token) query.token = next.token;
86
+ }
87
+
88
+ // Текущий запрос: экрану нужно знать, под какой объём и сторону спрашивали - иначе он не отличит "провайдер
89
+ // молчит" от "провайдер не берётся за такой объём".
90
+ export function rfqQuery() {
91
+ return { ...query };
92
+ }
93
+
94
+ // СНИМОК, КОТОРЫЙ ЧИТАЮТ ЭКРАНЫ. Форма та же, что была: { ok, error, data, at }. В data - references
95
+ // (справочный курс) и best, а предложения и отказы отдаёт rfqOffers(). Ядро не опрошено ни разу или
96
+ // отказало - данных нет, и это видно по ok. Прежние числа как "живые" не подставляются.
97
+ const snapshot = { ok: false, error: null, data: null, at: 0, fetching: false };
98
+ export function rfqSnapshot() {
99
+ return snapshot;
100
+ }
101
+
102
+ // Пара, которую пользователь ВИДИТ: он покупает XMR, значит смотрит на пары XMR/<актив>; продаёт - на обратные.
103
+ export function pairFacingUser(side = query.side, asset = "ETH") {
104
+ return side === "sell" ? asset.toUpperCase() + "/XMR" : "XMR/" + asset.toUpperCase();
105
+ }
106
+
107
+ // СТОРОНЫ ПАРЫ ЧИТАЮТСЯ ПО МЕСТУ (док 41, §1): первый элемент - ASSET (в нём же меряется объём), второй -
108
+ // CURRENCY (то, чем платят). Отдельные функции, потому что это правило конвенции, а не разбор строки:
109
+ // "XMR/ETH" значит "сколько ETH за 1 XMR" и ничего другого.
110
+ export function pairAsset(pair) {
111
+ return String(pair || "").split("/")[0].toUpperCase();
112
+ }
113
+ export function pairCurrency(pair) {
114
+ return String(pair || "").split("/")[1] ? String(pair).split("/")[1].toUpperCase() : "";
115
+ }
116
+
117
+ // ПРЕДЛОЖЕНИЯ И ОТКАЗЫ - ИЗ СНИМКА ЯДРА, БЕЗ ВТОРОГО ОТБОРА. Ядро уже разделило строки книги на
118
+ // предложения и отказы (sdk/src/quotes.mjs, readBook: ноль/противоречие/просрочка) и назвало лучшего.
119
+ // Здесь они ТОЛЬКО переносятся: своего отсева, своей сортировки и своих порогов не появляется. Пустой
120
+ // снимок - пустая книга; прежняя книга на экране не остаётся.
121
+ export function rfqOffers() {
122
+ const data = snapshot.ok && snapshot.data ? snapshot.data : null;
123
+ if (!data) return { offers: [], refused: [], seq: null, at: null };
124
+ return {
125
+ offers: data.offers || [],
126
+ refused: data.refused || [],
127
+ seq: data.seq === undefined ? null : data.seq,
128
+ at: data.at === undefined ? null : data.at,
129
+ };
130
+ }
131
+
132
+ // СВЕРКА СЕТИ КОТИРОВКИ СО СВОЕЙ (док 41, §3). Сеть - часть личности рынка, а не украшение: arbitrum и
133
+ // arbitrum-sepolia - это ОДНО СЛОВО ДЛЯ РАЗНЫХ ДЕНЕГ, и кошелёк между ними монеты не переводит. Сторона,
134
+ // где стоит XMR, обязана говорить на языке Monero, вторая - на языке реестра сетей EVM (его id).
135
+ // Возвращаем вердикт, а не throw: отказ одной котировки не закрывает рынок (см. market.js).
136
+ export function quoteNetworksFit(quote, { monero, evm }) {
137
+ const xmrOnAsset = quote.asset === "XMR";
138
+ const xmrOnCurrency = quote.currency === "XMR";
139
+ if (!xmrOnAsset && !xmrOnCurrency) return { ok: false, stated: quote.networkStated, why: "pair " + quote.pair + " has no XMR on either side" };
140
+ const moneroNet = xmrOnAsset ? quote.assetNetwork : quote.currencyNetwork;
141
+ const evmNet = xmrOnAsset ? quote.currencyNetwork : quote.assetNetwork;
142
+ const moneroSide = xmrOnAsset ? "assetNetwork" : "currencyNetwork";
143
+ const evmSide = xmrOnAsset ? "currencyNetwork" : "assetNetwork";
144
+ if (moneroNet && moneroNet !== monero) {
145
+ return { ok: false, stated: true, why: quote.pair + ": " + moneroSide + " is " + moneroNet + ", but this page pays XMR on " + monero };
146
+ }
147
+ if (evmNet && evmNet !== evm) {
148
+ return { ok: false, stated: true, why: quote.pair + ": " + evmSide + " is " + evmNet + ", but this page settles on " + evm };
149
+ }
150
+ // НЕ НАЗВАНА - НЕ ЗНАЧИТ "НАША". Сверять нечего, и это надо сказать вслух (в лог), но отказом за это не
151
+ // закрываем рынок: правило §3 ловит СЕТЬ, НАЗВАННУЮ НЕВЕРНО, а не поле, которого котировка ещё не несёт.
152
+ return { ok: true, stated: quote.networkStated, moneroNet: moneroNet || null, evmNet: evmNet || null, why: null };
153
+ }
154
+
155
+ // ОПРОС ИДЁТ ЧЕРЕЗ ЯДРО. Склейник (www/js/sdk/bridge.js) отдаёт ядру то, что нужно ему снаружи (сеть, размер,
156
+ // направление), и возвращает ПЕРЕВЕДЁННЫЙ снимок. Ядро грузится ДИНАМИЧЕСКИ: этот модуль читают и без
157
+ // браузера (проверки рынка), и тогда книга просто не запрашивается - динамический импорт в Node не тянет
158
+ // страничные модули. Отказ загрузки ядра - ЭТО ОТКАЗ, а не повод взять книгу где-то ещё.
159
+ export async function refreshRfq() {
160
+ if (snapshot.fetching) return snapshot;
161
+ snapshot.fetching = true;
162
+ try {
163
+ const bridge = await import("../sdk/bridge.js");
164
+ await bridge.sdkBook({ chain: query.chain, direction: query.side, token: query.token, size: query.size });
165
+ const next = bridge.sdkBookSnapshot();
166
+ snapshot.ok = next.ok;
167
+ snapshot.error = next.error;
168
+ snapshot.data = next.data;
169
+ snapshot.at = next.at || Date.now();
170
+ } catch (error) {
171
+ // ОТКАЗ НЕ ПОДМЕНЯЕМ ПРОШЛЫМИ ЧИСЛАМИ: показать устаревшую цену как живую - хуже, чем сказать, что
172
+ // связи нет. Данных не остаётся вовсе: книги, которую можно было бы показать, больше нет.
173
+ snapshot.ok = false;
174
+ snapshot.error = String((error && error.message) || error);
175
+ snapshot.data = null;
176
+ snapshot.at = Date.now();
177
+ } finally {
178
+ snapshot.fetching = false;
179
+ }
180
+ return snapshot;
181
+ }
182
+
183
+ let timer = null;
184
+
185
+ // ОПРАШИВАЕМ ВСЕГДА, как и раньше. Опрос здесь не для того, чтобы "поймать новую цену": он подтверждает,
186
+ // что провайдер на месте. Сам опрос держит ЯДРО (на время подписки), а этот такт заставляет книгу
187
+ // обновляться вместе с экраном. Молчание ноды видно как отсутствие свежей метки, а не как "цена та же".
188
+ export function startRfqPolling(chain = null, ms = API.pollMs) {
189
+ if (typeof chain === "string" && chain) query.chain = chain;
190
+ if (timer) return;
191
+ refreshRfq();
192
+ timer = setInterval(() => refreshRfq(), ms);
193
+ }
@@ -0,0 +1,43 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/core/sellWindow.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
+ // ОКНО ПРОДАЖИ XMR (issue #111): ОДНО ПРАВИЛО НА ДВЕ СТОРОНЫ, А НЕ ДВА ЧИСЛА.
7
+ //
8
+ // ЗАЧЕМ ОТДЕЛЬНЫЙ МОДУЛЬ. У окна продажи XMR (человек отдаёт XMR, получает натив сети) два числа, и они
9
+ // разной природы:
10
+ // * ЖЁСТКОЕ (hard) - срок отметки готовности ИЗ КОНТРАКТА. Позже него отметку не примет никто, и он входит
11
+ // в ПРИВЯЗКУ ордера (termsHash), поэтому после выдачи билета не меняется. В контракт ВСЕГДА идёт максимум:
12
+ // 10 подтверждений Monero - это примерно 1200 с, и меньше контрактного окна быть не может, иначе отметить
13
+ // готовность было бы нечем (отметку контракт после readyBy запрещает, XMR заперлись бы для обоих).
14
+ // * МЯГКОЕ (soft) - ОБЕЩАНИЕ НА ЭКРАНЕ: сколько провайдер держит цену, пока человек отправляет XMR.
15
+ // "Десять минут" владельца и партнёра.
16
+ // ПРАВИЛО ПРОДЛЕНИЯ ОДНО. Пока в пуле НЕ видно ПОЛНОЙ ожидаемой суммы, обещание продлевается до контрактного
17
+ // максимума: человек мог отправить, и ему нужен первый блок. Как только полная сумма видна в пуле, окно
18
+ // мягкое - обещание сдержано, и продлевать нечего.
19
+ //
20
+ // ПОЧЕМУ ФУНКЦИЯ, А НЕ ДВА ЧИСЛА В ДВУХ МЕСТАХ. Это правило решает НОДА (rfq/reverseArrival.mjs - отметка,
21
+ // тревога, ранний возврат) и показывает СТРАНИЦА (www/js/core/swap.js - отсчёт на экране). Вторая редакция
22
+ // разошлась бы с первой молча: человек видел бы одно окно, а нода жила бы по другому. Направление импорта
23
+ // rfq -> www в проекте уже принято (образец: rfq/reverseGas.mjs импортирует ../www/js/evm/claimGas.js).
24
+ export const SELL_WINDOW = {
25
+ softSec: 600, // "десять минут": обещание провайдера на экране
26
+ hardSec: 1200, // контрактное окно: 10 подтверждений Monero, максимум, попадающий в termsHash
27
+ };
28
+
29
+ const positiveSec = (v, dflt) => {
30
+ const n = Number(v);
31
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : dflt;
32
+ };
33
+
34
+ // СКОЛЬКО СЕКУНД ИДЁТ ОКНО ПРОДАЖИ при названном состоянии пула. poolSeen - в пуле видна ПОЛНАЯ ожидаемая
35
+ // сумма, и только она (не "что-то видно" и не недоплата: сделка повисла бы на чужой копейке - см. poolCoverage
36
+ // в rfq/reverseArrival.mjs). Контрактное окно - ПОТОЛОК: мягкое обещание не может быть длиннее контрактного,
37
+ // иначе мы обещали бы то, чего цепь не даст.
38
+ export function sellWindowSec({ poolSeen, soft = SELL_WINDOW.softSec, hard = SELL_WINDOW.hardSec } = {}) {
39
+ const s = positiveSec(soft, SELL_WINDOW.softSec);
40
+ const h = positiveSec(hard, SELL_WINDOW.hardSec);
41
+ const promised = Math.min(s, h);
42
+ return poolSeen ? promised : h;
43
+ }
@@ -0,0 +1,100 @@
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/core/store.js for the SDK package.
2
+ // Edit the source under www/js, then run: node tools/build-sdk-engine.mjs
3
+ // tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
4
+ // Everything below this header is identical to the source.
5
+
6
+ // Хранилище состояния демки.
7
+ //
8
+ // Зачем оно вообще: литпепер §5.1.7 - «вкладку можно закрыть, браузер восстановит состояние». Состояние сделки
9
+ // выводится из сохранённых таймстемпов и событий, поэтому вкладку действительно можно закрыть.
10
+ //
11
+ // ХРАНИЛИЩЕ ПРИХОДИТ СНАРУЖИ. Раньше здесь стоял `localStorage` прямо в коде, и это делало модуль непригодным
12
+ // ни в Node (проверки репозитория), ни в SDK (пакет обещает «без DOM»). Теперь хранилище - адаптер с тремя
13
+ // действиями (get/set/remove), а по умолчанию берётся localStorage, ЕСЛИ он есть в среде, иначе память
14
+ // процесса: молча не сохранять нельзя - сделка после перезагрузки потерялась бы.
15
+
16
+ import { APP, DEMO } from "./config.js";
17
+
18
+ const EMPTY = {
19
+ version: 1,
20
+ // payToken - выбранный актив оплаты. Хранится рядом с сетью: иначе форма при перезагрузке возвращала
21
+ // нативный токен и выбор пользователя терялся.
22
+ settings: { speed: DEMO.defaultSpeed, scenario: DEMO.defaultScenario, chainMode: DEMO.defaultChainMode, chain: DEMO.defaultChain, payToken: "" },
23
+ wallet: { connected: false, address: null, balances: {} },
24
+ swaps: {},
25
+ activeSwapId: null,
26
+ };
27
+
28
+ // АДАПТЕР ПО УМОЛЧАНИЮ: web storage, если он есть, иначе память. Проверка среды - через typeof, а не
29
+ // try/catch вокруг обращения: обращение к несуществующему имени в Node - это ReferenceError, и ловить его
30
+ // как «нет хранилища» значит прятать опечатки.
31
+ export function memoryStorage(map = new Map()) {
32
+ return {
33
+ get: (key) => (map.has(key) ? map.get(key) : null),
34
+ set: (key, value) => map.set(key, String(value)),
35
+ remove: (key) => map.delete(key),
36
+ };
37
+ }
38
+
39
+ const webStorage = (name) => {
40
+ const store = typeof globalThis !== "undefined" ? globalThis[name] : undefined;
41
+ if (!store || typeof store.getItem !== "function") return null;
42
+ return {
43
+ get: (key) => store.getItem(key),
44
+ set: (key, value) => store.setItem(key, String(value)),
45
+ remove: (key) => store.removeItem(key),
46
+ };
47
+ };
48
+
49
+ let adapter = webStorage("localStorage") || memoryStorage();
50
+
51
+ /** Подменить хранилище. Проверяется форма, а не происхождение: годится и localStorage, и память, и SDK. */
52
+ export function setStorage(next) {
53
+ const ok = next && typeof next.get === "function" && typeof next.set === "function" && typeof next.remove === "function";
54
+ if (!ok) throw new Error("хранилище должно уметь get/set/remove");
55
+ adapter = next;
56
+ return adapter;
57
+ }
58
+
59
+ /** Что используется сейчас: это видно снаружи, и по этому признаку проверки отличают память от браузера. */
60
+ export const storage = () => adapter;
61
+
62
+ export function loadState() {
63
+ try {
64
+ const raw = adapter.get(APP.storageKey);
65
+ if (!raw) return structuredClone(EMPTY);
66
+ const parsed = JSON.parse(raw);
67
+ return { ...structuredClone(EMPTY), ...parsed, settings: { ...EMPTY.settings, ...(parsed.settings || {}) } };
68
+ } catch {
69
+ return structuredClone(EMPTY);
70
+ }
71
+ }
72
+
73
+ // BigInt В JSON НЕ СЕРИАЛИЗУЕТСЯ - а половины ключей это BigInt. JSON.stringify на них ПАДАЕТ
74
+ // ("Do not know how to serialize a BigInt", bigintSafe), и падение молчаливое на вид: запись сделки просто не сохраняется,
75
+ // файл восстановления не выпускается, а экран показывает адрес, которого на цепи нет.
76
+ // Приводим BigInt к шестнадцатеричной строке: это тот же смысл, но сериализуемый, и секрет не теряется.
77
+ const bigintSafe = (key, value) => (typeof value === "bigint" ? "0x" + value.toString(16) : value);
78
+ export function saveState(state) {
79
+ try {
80
+ adapter.set(APP.storageKey, JSON.stringify(state, bigintSafe));
81
+ } catch (e) {
82
+ console.warn("state save failed", e);
83
+ }
84
+ return state;
85
+ }
86
+
87
+ export function update(mutator) {
88
+ const s = loadState();
89
+ const out = mutator(s) || s;
90
+ saveState(out);
91
+ return out;
92
+ }
93
+
94
+ export function reset() {
95
+ adapter.remove(APP.storageKey);
96
+ }
97
+
98
+ export function exportState() {
99
+ return JSON.stringify(loadState(), bigintSafe, 2);
100
+ }