@ninsei-labs/sdk 0.39.0 → 0.39.2

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.
@@ -1,179 +1,179 @@
1
- // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/prices.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
- // Живые рыночные числа для EVM-ноги: цена нейтива (Chainlink on-chain) и стоимость газа (RPC).
7
- //
8
- // ПОЧЕМУ ON-CHAIN, А НЕ API. Цена берётся вызовом контракта через тот же RPC, что уже разрешён
9
- // в CSP (`connect-src`), поэтому:
10
- // - не появляется ни одного нового внешнего домена (и ни одного нового доверенного сервиса);
11
- // - нет ключей, лимитов и «мы упали, потому что у CoinGecko 429»;
12
- // - значение нельзя перепутать с ценой другой сети: адрес фида свой у каждой цепи.
13
- // Адреса фидов НЕ выдуманы: каждый подтверждён вызовом в сети - описание `description()` должно
14
- // совпасть с ожидаемой парой, а ответ - попасть в разумный коридор и быть свежим. Проверка живёт
15
- // в tools/check-prices.mjs (работает на заглушке RPC, без сети).
16
- //
17
- // ЧЕГО ЗДЕСЬ НЕТ: если фида для сети нет или он молчит - возвращаем null, и вызывающий код обязан
18
- // показать, что число демонстрационное, а не подставить вчерашнюю константу молча.
19
-
20
- const SELECTOR = {
21
- description: "0x7284e416", // description()
22
- latestRoundData: "0xfeaf968c", // latestRoundData() -> (roundId, answer, startedAt, updatedAt, answeredInRound)
23
- };
24
-
25
- const CACHE_MS = 60_000; // цена: минута - компромисс между свежестью и числом запросов
26
- const GAS_CACHE_MS = 30_000; // газ меняется чаще, но и он не обязан бегать на каждый ввод символа
27
- const MAX_AGE_MS = 24 * 60 * 60 * 1000; // старше суток фид считаем несвежим и не показываем как живой
28
-
29
- const cache = new Map(); // ключ: chainId + вид запроса
30
-
31
- function cached(key, ttlMs) {
32
- const hit = cache.get(key);
33
- if (hit && Date.now() - hit.at < ttlMs) return hit.value;
34
- return undefined;
35
- }
36
-
37
- function put(key, value) {
38
- cache.set(key, { at: Date.now(), value });
39
- return value;
40
- }
41
-
42
- async function rpc(chain, method, params = []) {
43
- const res = await fetch(chain.rpcUrl, {
44
- method: "POST",
45
- headers: { "content-type": "application/json" },
46
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
47
- });
48
- if (!res.ok) throw new Error(method + ": HTTP " + res.status);
49
- const j = await res.json();
50
- if (j.error) throw new Error(method + ": " + (j.error.message || "ошибка RPC"));
51
- return j.result;
52
- }
53
-
54
- const word = (data, i) => BigInt("0x" + String(data).slice(2).slice(i * 64, (i + 1) * 64));
55
-
56
- // Декодирование строки ABI: [offset][length][bytes]. Нужно, чтобы отличить фид ETH/USD от любого
57
- // другого, который кто-то подставил в конфиг. TextDecoder, а не Buffer: этот код живёт в браузере.
58
- function decodeString(data) {
59
- const s = String(data || "");
60
- if (s.length < 2 + 128) return "";
61
- const len = Number(BigInt("0x" + s.slice(2 + 64, 2 + 128)));
62
- const body = s.slice(2 + 128, 2 + 128 + len * 2);
63
- const bytes = new Uint8Array(body.length / 2);
64
- for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(body.slice(i * 2, i * 2 + 2), 16);
65
- return new TextDecoder().decode(bytes);
66
- }
67
-
68
- // Цена нейтива в USD. null - если фида нет, он молчит или значения не проходят проверку.
69
- export async function nativeUsd(chain, { force = false } = {}) {
70
- const feed = chain && chain.priceFeed;
71
- if (!feed || !feed.address) return null;
72
- const key = chain.id + ":usd";
73
- if (!force) {
74
- const hit = cached(key, CACHE_MS);
75
- if (hit !== undefined) return hit;
76
- }
77
- try {
78
- const [descHex, roundHex] = await Promise.all([
79
- rpc(chain, "eth_call", [{ to: feed.address, data: SELECTOR.description }, "latest"]),
80
- rpc(chain, "eth_call", [{ to: feed.address, data: SELECTOR.latestRoundData }, "latest"]),
81
- ]);
82
- const desc = decodeString(descHex);
83
- const answer = word(roundHex, 1);
84
- const updatedAt = Number(word(roundHex, 3)) * 1000;
85
- const decimals = Number(feed.decimals || 8);
86
- const value = Number(answer) / 10 ** decimals;
87
- const fresh = updatedAt > 0 && Date.now() - updatedAt < MAX_AGE_MS;
88
- const sane = value >= (feed.min || 0.01) && value <= (feed.max || 1e7);
89
- if (desc !== feed.pair || !fresh || !sane) {
90
- return put(key, null); // null, а не «примерно столько»: молчание честнее выдумки
91
- }
92
- return put(key, { usd: value, updatedAt, source: "chainlink", pair: desc });
93
- } catch {
94
- return put(key, null);
95
- }
96
- }
97
-
98
- // Стоимость газа в USD: оценка лимита × цена газа × цена нейтива.
99
- // На Arbitrum в цену газа уже входит L1-составляющая (так устроен их gas price), поэтому
100
- // отдельный вызов NodeInterface не нужен - обычная оценка даёт сопоставимую с кошельками цифру.
101
- export async function gasUsd(chain, { from = null, payToken = null, amountWei = null } = {}) {
102
- const key = chain.id + ":gas";
103
- const hit = cached(key, GAS_CACHE_MS);
104
- if (hit !== undefined) return hit;
105
- const price = await nativeUsd(chain);
106
- if (!price) return put(key, null);
107
- try {
108
- const gasPrice = BigInt(await rpc(chain, "eth_gasPrice"));
109
- let gasLimit = null;
110
- let measured = false;
111
- // Оцениваем ту транзакцию, которую человек и подпишет: перевод оплачиваемого токена.
112
- // Эскроу ещё не задеплоен, поэтому это честная нижняя оценка, а не «газ свопа».
113
- try {
114
- // Без аккаунта eth_estimateGas подставляет НУЛЕВОЙ адрес, и перевод токена справедливо
115
- // падает с "ERC20: transfer from the zero address". Поэтому без from не оцениваем вовсе:
116
- // берём типовой лимит и честно помечаем, что он не измерен (в интерфейсе это видно).
117
- // ВАЖНО: признак "перевод токена" и признак "есть аккаунт" - РАЗНЫЕ условия. Раньше они
118
- // были слиты в одну ветку, и без аккаунта перевод токена получал лимит нативного перевода
119
- // (21000 вместо 65000) - поймал tools/check-prices.mjs.
120
- const tokenTransfer = Boolean(payToken && !payToken.native);
121
- if (tokenTransfer && from) {
122
- // transfer(address,uint256) = 0xa9059cbb
123
- const to = String(from || "").replace(/^0x/, "").padStart(64, "0");
124
- const value = BigInt(amountWei || 1).toString(16).padStart(64, "0");
125
- gasLimit = BigInt(await rpc(chain, "eth_estimateGas", [{ from, to: payToken.address, data: "0xa9059cbb" + to + value }]));
126
- measured = true;
127
- } else if (from) {
128
- gasLimit = BigInt(await rpc(chain, "eth_estimateGas", [{ from, to: from, value: "0x1" }]));
129
- measured = true;
130
- } else {
131
- // Ни аккаунта, ни измерения: типовой лимит и честный признак «не измерено».
132
- gasLimit = BigInt(chain.gasLimitFallback || (tokenTransfer ? 65000 : 21000));
133
- }
134
- } catch {
135
- gasLimit = BigInt(chain.gasLimitFallback || (tokenTransfer ? 65000 : 21000));
136
- }
137
- const wei = gasPrice * gasLimit;
138
- const usd = (Number(wei) / 1e18) * price.usd;
139
- return put(key, { usd, gasLimit: gasLimit.toString(), gasPrice: gasPrice.toString(), native: price, source: "rpc", measured });
140
- } catch {
141
- return put(key, null);
142
- }
143
- }
144
-
145
- // Одна точка обновления: подтянуть цену и газ для сети и вернуть то, чем можно подписать интерфейс.
146
- export async function refreshChainPrices(chain, opts = {}) {
147
- if (!chain || !chain.rpcUrl) return { nativeUsd: null, gasUsd: null, source: "none" };
148
- const [price, gas] = await Promise.all([nativeUsd(chain), gasUsd(chain, opts)]);
149
- // Цена нейтива - общая для всех расчётов сети (балансы, «≈ $», оценка газа), поэтому пишем её
150
- // в конфиг сети: иначе одна и та же цифра жила бы в двух местах и разъезжалась.
151
- if (price && Number.isFinite(price.usd)) chain.native.usd = price.usd;
152
- return {
153
- nativeUsd: price ? price.usd : null,
154
- nativeUpdatedAt: price ? price.updatedAt : null,
155
- gasUsd: gas ? gas.usd : null,
156
- // ЦЕНА ГАЗА В ВЕЙ - НАРУЖУ, А НЕ ТОЛЬКО СТОИМОСТЬ ОЦЕНКИ. Из неё считается запас газа на действия ордера
157
- // (www/js/evm/gasReserve.js): отметка готовности и возврат - другие транзакции, у них свои пределы, и
158
- // «стоимость одной оценки» их не заменяет. Нет газа у цепи - null, и вызывающий обязан сказать, что запас
159
- // не измерен, а не подставить ноль.
160
- gasPriceWei: gas ? gas.gasPrice : null,
161
- gasSource: gas ? gas.source : null,
162
- gasMeasured: gas ? Boolean(gas.measured) : false,
163
- source: price ? "chainlink" : "demo",
164
- };
165
- }
166
-
167
- // Коротко и в одну строку, но про ОБЕ половины: строка называется «Gas & price», значит и газ,
168
- // и цена должны быть в значении. Живая цена видна по самому тексту (Chainlink), демо - по «demo values».
169
- export function priceLabel(info) {
170
- if (!info || !info.source || info.source === "none") return "demo values";
171
- if (info.source !== "chainlink") return "demo values";
172
- const when = info.nativeUpdatedAt ? new Date(info.nativeUpdatedAt).toISOString().slice(11, 16) : "?";
173
- const gas = info.gasMeasured ? "gas by RPC" : "gas estimate";
174
- return gas + ", price by Chainlink at " + when + " UTC";
175
- }
176
-
177
- export function resetPriceCache() {
178
- cache.clear();
179
- }
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/prices.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
+ // Живые рыночные числа для EVM-ноги: цена нейтива (Chainlink on-chain) и стоимость газа (RPC).
7
+ //
8
+ // ПОЧЕМУ ON-CHAIN, А НЕ API. Цена берётся вызовом контракта через тот же RPC, что уже разрешён
9
+ // в CSP (`connect-src`), поэтому:
10
+ // - не появляется ни одного нового внешнего домена (и ни одного нового доверенного сервиса);
11
+ // - нет ключей, лимитов и «мы упали, потому что у CoinGecko 429»;
12
+ // - значение нельзя перепутать с ценой другой сети: адрес фида свой у каждой цепи.
13
+ // Адреса фидов НЕ выдуманы: каждый подтверждён вызовом в сети - описание `description()` должно
14
+ // совпасть с ожидаемой парой, а ответ - попасть в разумный коридор и быть свежим. Проверка живёт
15
+ // в tools/check-prices.mjs (работает на заглушке RPC, без сети).
16
+ //
17
+ // ЧЕГО ЗДЕСЬ НЕТ: если фида для сети нет или он молчит - возвращаем null, и вызывающий код обязан
18
+ // показать, что число демонстрационное, а не подставить вчерашнюю константу молча.
19
+
20
+ const SELECTOR = {
21
+ description: "0x7284e416", // description()
22
+ latestRoundData: "0xfeaf968c", // latestRoundData() -> (roundId, answer, startedAt, updatedAt, answeredInRound)
23
+ };
24
+
25
+ const CACHE_MS = 60_000; // цена: минута - компромисс между свежестью и числом запросов
26
+ const GAS_CACHE_MS = 30_000; // газ меняется чаще, но и он не обязан бегать на каждый ввод символа
27
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000; // старше суток фид считаем несвежим и не показываем как живой
28
+
29
+ const cache = new Map(); // ключ: chainId + вид запроса
30
+
31
+ function cached(key, ttlMs) {
32
+ const hit = cache.get(key);
33
+ if (hit && Date.now() - hit.at < ttlMs) return hit.value;
34
+ return undefined;
35
+ }
36
+
37
+ function put(key, value) {
38
+ cache.set(key, { at: Date.now(), value });
39
+ return value;
40
+ }
41
+
42
+ async function rpc(chain, method, params = []) {
43
+ const res = await fetch(chain.rpcUrl, {
44
+ method: "POST",
45
+ headers: { "content-type": "application/json" },
46
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
47
+ });
48
+ if (!res.ok) throw new Error(method + ": HTTP " + res.status);
49
+ const j = await res.json();
50
+ if (j.error) throw new Error(method + ": " + (j.error.message || "ошибка RPC"));
51
+ return j.result;
52
+ }
53
+
54
+ const word = (data, i) => BigInt("0x" + String(data).slice(2).slice(i * 64, (i + 1) * 64));
55
+
56
+ // Декодирование строки ABI: [offset][length][bytes]. Нужно, чтобы отличить фид ETH/USD от любого
57
+ // другого, который кто-то подставил в конфиг. TextDecoder, а не Buffer: этот код живёт в браузере.
58
+ function decodeString(data) {
59
+ const s = String(data || "");
60
+ if (s.length < 2 + 128) return "";
61
+ const len = Number(BigInt("0x" + s.slice(2 + 64, 2 + 128)));
62
+ const body = s.slice(2 + 128, 2 + 128 + len * 2);
63
+ const bytes = new Uint8Array(body.length / 2);
64
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(body.slice(i * 2, i * 2 + 2), 16);
65
+ return new TextDecoder().decode(bytes);
66
+ }
67
+
68
+ // Цена нейтива в USD. null - если фида нет, он молчит или значения не проходят проверку.
69
+ export async function nativeUsd(chain, { force = false } = {}) {
70
+ const feed = chain && chain.priceFeed;
71
+ if (!feed || !feed.address) return null;
72
+ const key = chain.id + ":usd";
73
+ if (!force) {
74
+ const hit = cached(key, CACHE_MS);
75
+ if (hit !== undefined) return hit;
76
+ }
77
+ try {
78
+ const [descHex, roundHex] = await Promise.all([
79
+ rpc(chain, "eth_call", [{ to: feed.address, data: SELECTOR.description }, "latest"]),
80
+ rpc(chain, "eth_call", [{ to: feed.address, data: SELECTOR.latestRoundData }, "latest"]),
81
+ ]);
82
+ const desc = decodeString(descHex);
83
+ const answer = word(roundHex, 1);
84
+ const updatedAt = Number(word(roundHex, 3)) * 1000;
85
+ const decimals = Number(feed.decimals || 8);
86
+ const value = Number(answer) / 10 ** decimals;
87
+ const fresh = updatedAt > 0 && Date.now() - updatedAt < MAX_AGE_MS;
88
+ const sane = value >= (feed.min || 0.01) && value <= (feed.max || 1e7);
89
+ if (desc !== feed.pair || !fresh || !sane) {
90
+ return put(key, null); // null, а не «примерно столько»: молчание честнее выдумки
91
+ }
92
+ return put(key, { usd: value, updatedAt, source: "chainlink", pair: desc });
93
+ } catch {
94
+ return put(key, null);
95
+ }
96
+ }
97
+
98
+ // Стоимость газа в USD: оценка лимита × цена газа × цена нейтива.
99
+ // На Arbitrum в цену газа уже входит L1-составляющая (так устроен их gas price), поэтому
100
+ // отдельный вызов NodeInterface не нужен - обычная оценка даёт сопоставимую с кошельками цифру.
101
+ export async function gasUsd(chain, { from = null, payToken = null, amountWei = null } = {}) {
102
+ const key = chain.id + ":gas";
103
+ const hit = cached(key, GAS_CACHE_MS);
104
+ if (hit !== undefined) return hit;
105
+ const price = await nativeUsd(chain);
106
+ if (!price) return put(key, null);
107
+ try {
108
+ const gasPrice = BigInt(await rpc(chain, "eth_gasPrice"));
109
+ let gasLimit = null;
110
+ let measured = false;
111
+ // Оцениваем ту транзакцию, которую человек и подпишет: перевод оплачиваемого токена.
112
+ // Эскроу ещё не задеплоен, поэтому это честная нижняя оценка, а не «газ свопа».
113
+ try {
114
+ // Без аккаунта eth_estimateGas подставляет НУЛЕВОЙ адрес, и перевод токена справедливо
115
+ // падает с "ERC20: transfer from the zero address". Поэтому без from не оцениваем вовсе:
116
+ // берём типовой лимит и честно помечаем, что он не измерен (в интерфейсе это видно).
117
+ // ВАЖНО: признак "перевод токена" и признак "есть аккаунт" - РАЗНЫЕ условия. Раньше они
118
+ // были слиты в одну ветку, и без аккаунта перевод токена получал лимит нативного перевода
119
+ // (21000 вместо 65000) - поймал tools/check-prices.mjs.
120
+ const tokenTransfer = Boolean(payToken && !payToken.native);
121
+ if (tokenTransfer && from) {
122
+ // transfer(address,uint256) = 0xa9059cbb
123
+ const to = String(from || "").replace(/^0x/, "").padStart(64, "0");
124
+ const value = BigInt(amountWei || 1).toString(16).padStart(64, "0");
125
+ gasLimit = BigInt(await rpc(chain, "eth_estimateGas", [{ from, to: payToken.address, data: "0xa9059cbb" + to + value }]));
126
+ measured = true;
127
+ } else if (from) {
128
+ gasLimit = BigInt(await rpc(chain, "eth_estimateGas", [{ from, to: from, value: "0x1" }]));
129
+ measured = true;
130
+ } else {
131
+ // Ни аккаунта, ни измерения: типовой лимит и честный признак «не измерено».
132
+ gasLimit = BigInt(chain.gasLimitFallback || (tokenTransfer ? 65000 : 21000));
133
+ }
134
+ } catch {
135
+ gasLimit = BigInt(chain.gasLimitFallback || (tokenTransfer ? 65000 : 21000));
136
+ }
137
+ const wei = gasPrice * gasLimit;
138
+ const usd = (Number(wei) / 1e18) * price.usd;
139
+ return put(key, { usd, gasLimit: gasLimit.toString(), gasPrice: gasPrice.toString(), native: price, source: "rpc", measured });
140
+ } catch {
141
+ return put(key, null);
142
+ }
143
+ }
144
+
145
+ // Одна точка обновления: подтянуть цену и газ для сети и вернуть то, чем можно подписать интерфейс.
146
+ export async function refreshChainPrices(chain, opts = {}) {
147
+ if (!chain || !chain.rpcUrl) return { nativeUsd: null, gasUsd: null, source: "none" };
148
+ const [price, gas] = await Promise.all([nativeUsd(chain), gasUsd(chain, opts)]);
149
+ // Цена нейтива - общая для всех расчётов сети (балансы, «≈ $», оценка газа), поэтому пишем её
150
+ // в конфиг сети: иначе одна и та же цифра жила бы в двух местах и разъезжалась.
151
+ if (price && Number.isFinite(price.usd)) chain.native.usd = price.usd;
152
+ return {
153
+ nativeUsd: price ? price.usd : null,
154
+ nativeUpdatedAt: price ? price.updatedAt : null,
155
+ gasUsd: gas ? gas.usd : null,
156
+ // ЦЕНА ГАЗА В ВЕЙ - НАРУЖУ, А НЕ ТОЛЬКО СТОИМОСТЬ ОЦЕНКИ. Из неё считается запас газа на действия ордера
157
+ // (www/js/evm/gasReserve.js): отметка готовности и возврат - другие транзакции, у них свои пределы, и
158
+ // «стоимость одной оценки» их не заменяет. Нет газа у цепи - null, и вызывающий обязан сказать, что запас
159
+ // не измерен, а не подставить ноль.
160
+ gasPriceWei: gas ? gas.gasPrice : null,
161
+ gasSource: gas ? gas.source : null,
162
+ gasMeasured: gas ? Boolean(gas.measured) : false,
163
+ source: price ? "chainlink" : "demo",
164
+ };
165
+ }
166
+
167
+ // Коротко и в одну строку, но про ОБЕ половины: строка называется «Gas & price», значит и газ,
168
+ // и цена должны быть в значении. Живая цена видна по самому тексту (Chainlink), демо - по «demo values».
169
+ export function priceLabel(info) {
170
+ if (!info || !info.source || info.source === "none") return "demo values";
171
+ if (info.source !== "chainlink") return "demo values";
172
+ const when = info.nativeUpdatedAt ? new Date(info.nativeUpdatedAt).toISOString().slice(11, 16) : "?";
173
+ const gas = info.gasMeasured ? "gas by RPC" : "gas estimate";
174
+ return gas + ", price by Chainlink at " + when + " UTC";
175
+ }
176
+
177
+ export function resetPriceCache() {
178
+ cache.clear();
179
+ }
@@ -88,7 +88,10 @@ export const PAYLOAD_VERSION = 3;
88
88
  // источник, v3 - ПОЛОВИНЫ. Читать обязаны все три: у людей на руках есть файлы всех версий, и файл
89
89
  // восстановления - единственный способ забрать свои XMR. Выбрасывать старые версии нельзя.
90
90
  const SUPPORTED_PAYLOAD_VERSIONS = [1, 2, 3];
91
- const NETWORKS = ["mainnet", "stagenet", "testnet"];
91
+ // СЕТИ, КОТОРЫЕ ЧИТАТЕЛЬ ФАЙЛА ЗНАЕТ. Три стандартные плюс fakechain - официальное имя regtest-режима
92
+ // monerod: нода контура CI называет себя именно так, и файл, выпущенный на такой сети, обязан читаться.
93
+ // Список поимённый: сеть вне него отвергается как неизвестная, и это остаётся в силе.
94
+ const NETWORKS = ["mainnet", "stagenet", "testnet", "fakechain"];
92
95
 
93
96
  function b64(bytes) {
94
97
  return btoa(String.fromCharCode(...new Uint8Array(bytes)));
@@ -37,7 +37,7 @@
37
37
  // загрузился (нет файла, старый кеш, сеть) - это НАЗВАННЫЙ отказ с причиной в консоли (sdkBundle ниже), а не
38
38
  // молчание и не подмена.
39
39
 
40
- import { chainById, DEFAULT_CHAIN, MONERO, allowedProvidersFor, knownFactoryCodesFor } from "../core/config.js";
40
+ import { chainById, DEFAULT_CHAIN, MONERO, allowedProvidersFor, knownFactoryCodesFor, knownImplementationCodesFor } from "../core/config.js";
41
41
  import { storage as pageStorage } from "../core/store.js";
42
42
  import { token as authToken, signIn } from "../evm/auth.js";
43
43
  import * as evm from "../evm/index.js";
@@ -69,7 +69,7 @@ import { height as nodeHeightValue } from "../monero/node.js";
69
69
 
70
70
  // АДРЕС ЯДРА СТРОКОЙ - СОБРАННЫЙ БАНДЛ ПОД www/ (tools/build-sdk.mjs): страница получает ядро ОДНИМ
71
71
  // модулем, а не пачкой исходников sdk/. ?v=<хеш> проставляет tools/stamp-assets.mjs.
72
- const SDK_BUNDLE = "/assets/vendors/sdk/sdk-browser.js?v=bdb79a39";
72
+ const SDK_BUNDLE = "/assets/vendors/sdk/sdk-browser.js?v=31e62de5";
73
73
 
74
74
  // ОДНА ЗАГРУЗКА НА СТРАНИЦУ. Отказ - это состояние, а не поломка: он назван в консоли, и вызывающий решает.
75
75
  let loading = null;
@@ -140,6 +140,11 @@ function optionsFor(chain) {
140
140
  // берёт из записи реестра провайдера, а её КОД сверяет с этим списком (extcodehash). Пусто = политики
141
141
  // нет; тогда фабрика всё равно обязана совпасть с подписанной котировкой.
142
142
  knownFactoryCodes: knownFactoryCodesFor(chain),
143
+ // ХЕШ ИЗВЕСТНОЙ СБОРКИ РЕАЛИЗАЦИИ ЭСКРОУ (#114) - РЯДОМ С knownFactoryCodes. Фабрика сверяет пару
144
+ // (implementation, implementationCodeHash) при создании ордера; ядро сверяет ТУ ЖЕ пару на цепи, и этот
145
+ // список - политика интерфейса: какие сборки реализации он принимает. Пусто = политики нет (сеть со
146
+ // старой фабрикой), и тогда пара всё равно читается, а код по адресу реализации обязан быть непустым.
147
+ knownImplementationCodes: knownImplementationCodesFor(chain),
143
148
  };
144
149
  }
145
150
 
package/index.d.ts CHANGED
@@ -24,8 +24,12 @@ export interface NinseiOptions {
24
24
  * without a driver answers with the code `not-implemented`, rather than being assumed EVM by default.
25
25
  */
26
26
  settlement: { chain: string; vm?: string };
27
- /** The Monero network. A value from `sdk.config.xmrNetworks`. */
28
- xmrNetwork: "mainnet" | "stagenet" | "testnet";
27
+ /**
28
+ * The Monero network. A value from `sdk.config.xmrNetworks`. Besides mainnet/stagenet/testnet it names
29
+ * `fakechain` - the official network name monerod reports in regtest mode, which the withdrawal page picks up
30
+ * from the node on the CI contour.
31
+ */
32
+ xmrNetwork: "mainnet" | "stagenet" | "testnet" | "fakechain";
29
33
  /** Where to load workers and WASM from (order-worker, the Monero bundles). Default "/". */
30
34
  assetsBase?: string;
31
35
  /** Storage for unfinished swaps and sessions. Default - a localStorage adapter, if present. */
@@ -89,6 +93,18 @@ export interface NinseiOptions {
89
93
  * policy": the record's factory must still be named and agree with the quote.
90
94
  */
91
95
  knownFactoryCodes?: readonly string[];
96
+ /**
97
+ * THE INTERFACE'S ESCROW-IMPLEMENTATION POLICY (issue #114) - policy, not proof, next to knownFactoryCodes and
98
+ * allowedProviders. Since #114 every order's escrow is an EIP-1167 clone of ONE shared implementation, and the
99
+ * factory carries the pair (`implementation()`, `implementationCodeHash()`). The factory checks that pair at
100
+ * creation; the core checks the SAME pair on chain - it reads the pair off the factory, reads the code at the
101
+ * implementation's address, re-hashes it and matches it against this list of known build hashes (`keccak256` of
102
+ * the implementation's runtime code, a protocol constant: the implementation has no immutables). An implementation
103
+ * whose code is not a known build (or has no code at all) is refused with `quote-factory-code-unknown`; a pair
104
+ * that cannot be read at all (an older factory without those getters) is refused with `quote-factory-unchecked`.
105
+ * An empty/absent list means "no policy": the pair is still read and the implementation's code must be non-empty.
106
+ */
107
+ knownImplementationCodes?: readonly string[];
92
108
  /**
93
109
  * A PASS TO OUR SERVICE for the "server" step (recording the swap with us). Only a wallet can sign the sign-in
94
110
  * message, and the wallet's role in the SDK is an adapter: windows and message signatures remain the interface's,
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@ninsei-labs/sdk",
3
- "version": "0.39.0",
3
+ "version": "0.39.2",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
7
7
  },
8
- "packageManager": "pnpm@9.15.9",
8
+ "packageManager": "pnpm@12.10.1",
9
9
  "description": "XMR <-> EVM swap engine: quotes, guards, order steps, recovery (no DOM, no user-facing strings)",
10
10
  "license": "MIT",
11
11
  "type": "module",
@@ -16,9 +16,9 @@
16
16
  "check:import": "node tools/check.mjs --only=import",
17
17
  "check:surface": "node tools/check.mjs --only=surface",
18
18
  "check:mirror": "node tools/check.mjs --only=mirror",
19
- "check:language": "node tools/check.mjs --only=language",
20
19
  "check:pack": "node tools/check.mjs --only=pack",
21
- "check:secrets": "node tools/check.mjs --only=secrets"
20
+ "check:secrets": "node tools/check.mjs --only=secrets",
21
+ "check:sweep": "node tools/sweep-refusal.test.mjs"
22
22
  },
23
23
  "types": "./index.d.ts",
24
24
  "exports": {
package/src/actions.mjs CHANGED
@@ -1,72 +1,72 @@
1
- // ORDER ACTIONS: MARKING READY, CLAIMING, REFUNDING.
2
- //
3
- // All three go through ONE wallet seam (the `send`/`receipt` role) and through the slot check that in the engine
4
- // stands BEFORE signing. What is added here is exactly what the engine cannot do: `expect` is MANDATORY.
5
- //
6
- // Why it is mandatory. The slot check compares the order we assembled with the one lying on the chain. Without
7
- // expectations there is nothing to compare against, and the signature goes out under an order whose commitments
8
- // nobody checked: for marking ready that is granting a foreign address the right to take the settlement, for
9
- // claiming it is revealing your own half (irreversible), for refunding the same reveal. The transaction still
10
- // looks successful.
11
- import { SdkError } from "./errors.mjs";
12
- import * as engine from "./engine.mjs";
13
- // THE DEAL FLOW IS NOW A PACKAGE MODULE, NOT AN ENGINE FILE (#32, wave 3): www/js/core/swap-flow.js moved to
14
- // sdk/src/swap-flow.mjs. The actions therefore call the module directly instead of reaching it through the
15
- // mirror - there is exactly one implementation, and it lives here.
16
- import * as flow from "./swap-flow.mjs";
17
-
18
- const missingFields = (object, fields) =>
19
- fields.filter((k) => object[k] === undefined || object[k] === null || object[k] === "");
20
-
21
- const walletOrFail = (wallet) => {
22
- const role = ["send", "receipt"];
23
- const missing = wallet ? role.filter((k) => typeof wallet[k] !== "function") : role;
24
- if (missing.length) throw new SdkError("bad-input", { field: "wallet", missing });
25
- return wallet;
26
- };
27
-
28
- const requestOrFail = (request, extra) => {
29
- if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
30
- const missing = missingFields(request, ["escrow", "expect", ...extra]);
31
- if (missing.length) throw new SdkError("bad-input", { field: "request", missing });
32
- return request;
33
- };
34
-
35
- // The engine's refusal arrives as text; a CODE goes out. Texts are the interface's business, the core speaks in codes.
36
- const withCodes = async (step, work) => {
37
- try { return await work(); }
38
- catch (error) {
39
- if (error && typeof error.code === "string") throw error;
40
- throw new SdkError("contract-reverted", { step });
41
- }
42
- };
43
-
44
- export function createActions({ call = null } = {}) {
45
- // WHAT READS THE CHAIN IS ALSO A SEAM, and not only for the checks: the slot check and the signature must go
46
- // ONE way, otherwise part of the check goes past the wallet and part does not. By default - the engine's read.
47
- const reader = call || engine.evm.readContract;
48
- const deps = (wallet) => ({ call: reader, send: (tx) => wallet.send(tx), receipt: (h) => wallet.receipt(h) });
49
- return {
50
- /** Marking "I saw the XMR on the shared address". Without it, claiming is forbidden by the contract. */
51
- async markReady(request, wallet) {
52
- requestOrFail(request, []);
53
- walletOrFail(wallet);
54
- const { escrow, expect, onStep } = request;
55
- return await withCodes("mark-ready", () => flow.markReadyOrder({ escrow, expect, onStep, deps: deps(wallet) }));
56
- },
57
- /** Claiming: it reveals your own half, so expectations are mandatory. */
58
- async claim(request, wallet) {
59
- requestOrFail(request, ["halfClaimer"]);
60
- walletOrFail(wallet);
61
- const { escrow, halfClaimer, expect, onStep } = request;
62
- return await withCodes("claim", () => flow.claimOrder({ escrow, halfClaimer, expect, onStep, deps: deps(wallet) }));
63
- },
64
- /** Refunding: it also reveals a half. The core passes the expectations ALWAYS, though the engine does not require them. */
65
- async refund(request, wallet) {
66
- requestOrFail(request, ["halfLocker"]);
67
- walletOrFail(wallet);
68
- const { escrow, halfLocker, expect, onStep } = request;
69
- return await withCodes("refund", () => flow.refundOrder({ escrow, halfLocker, expect, onStep, deps: deps(wallet) }));
70
- },
71
- };
72
- }
1
+ // ORDER ACTIONS: MARKING READY, CLAIMING, REFUNDING.
2
+ //
3
+ // All three go through ONE wallet seam (the `send`/`receipt` role) and through the slot check that in the engine
4
+ // stands BEFORE signing. What is added here is exactly what the engine cannot do: `expect` is MANDATORY.
5
+ //
6
+ // Why it is mandatory. The slot check compares the order we assembled with the one lying on the chain. Without
7
+ // expectations there is nothing to compare against, and the signature goes out under an order whose commitments
8
+ // nobody checked: for marking ready that is granting a foreign address the right to take the settlement, for
9
+ // claiming it is revealing your own half (irreversible), for refunding the same reveal. The transaction still
10
+ // looks successful.
11
+ import { SdkError } from "./errors.mjs";
12
+ import * as engine from "./engine.mjs";
13
+ // THE DEAL FLOW IS NOW A PACKAGE MODULE, NOT AN ENGINE FILE (#32, wave 3): www/js/core/swap-flow.js moved to
14
+ // sdk/src/swap-flow.mjs. The actions therefore call the module directly instead of reaching it through the
15
+ // mirror - there is exactly one implementation, and it lives here.
16
+ import * as flow from "./swap-flow.mjs";
17
+
18
+ const missingFields = (object, fields) =>
19
+ fields.filter((k) => object[k] === undefined || object[k] === null || object[k] === "");
20
+
21
+ const walletOrFail = (wallet) => {
22
+ const role = ["send", "receipt"];
23
+ const missing = wallet ? role.filter((k) => typeof wallet[k] !== "function") : role;
24
+ if (missing.length) throw new SdkError("bad-input", { field: "wallet", missing });
25
+ return wallet;
26
+ };
27
+
28
+ const requestOrFail = (request, extra) => {
29
+ if (!request || typeof request !== "object") throw new SdkError("bad-input", { field: "request" });
30
+ const missing = missingFields(request, ["escrow", "expect", ...extra]);
31
+ if (missing.length) throw new SdkError("bad-input", { field: "request", missing });
32
+ return request;
33
+ };
34
+
35
+ // The engine's refusal arrives as text; a CODE goes out. Texts are the interface's business, the core speaks in codes.
36
+ const withCodes = async (step, work) => {
37
+ try { return await work(); }
38
+ catch (error) {
39
+ if (error && typeof error.code === "string") throw error;
40
+ throw new SdkError("contract-reverted", { step });
41
+ }
42
+ };
43
+
44
+ export function createActions({ call = null } = {}) {
45
+ // WHAT READS THE CHAIN IS ALSO A SEAM, and not only for the checks: the slot check and the signature must go
46
+ // ONE way, otherwise part of the check goes past the wallet and part does not. By default - the engine's read.
47
+ const reader = call || engine.evm.readContract;
48
+ const deps = (wallet) => ({ call: reader, send: (tx) => wallet.send(tx), receipt: (h) => wallet.receipt(h) });
49
+ return {
50
+ /** Marking "I saw the XMR on the shared address". Without it, claiming is forbidden by the contract. */
51
+ async markReady(request, wallet) {
52
+ requestOrFail(request, []);
53
+ walletOrFail(wallet);
54
+ const { escrow, expect, onStep } = request;
55
+ return await withCodes("mark-ready", () => flow.markReadyOrder({ escrow, expect, onStep, deps: deps(wallet) }));
56
+ },
57
+ /** Claiming: it reveals your own half, so expectations are mandatory. */
58
+ async claim(request, wallet) {
59
+ requestOrFail(request, ["halfClaimer"]);
60
+ walletOrFail(wallet);
61
+ const { escrow, halfClaimer, expect, onStep } = request;
62
+ return await withCodes("claim", () => flow.claimOrder({ escrow, halfClaimer, expect, onStep, deps: deps(wallet) }));
63
+ },
64
+ /** Refunding: it also reveals a half. The core passes the expectations ALWAYS, though the engine does not require them. */
65
+ async refund(request, wallet) {
66
+ requestOrFail(request, ["halfLocker"]);
67
+ walletOrFail(wallet);
68
+ const { escrow, halfLocker, expect, onStep } = request;
69
+ return await withCodes("refund", () => flow.refundOrder({ escrow, halfLocker, expect, onStep, deps: deps(wallet) }));
70
+ },
71
+ };
72
+ }