@ninsei-labs/sdk 0.39.0 → 0.39.1

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,88 +1,88 @@
1
- // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/gasReserve.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
- // ЗАПАС ГАЗА ПОКУПАТЕЛЯ НА ДЕЙСТВИЯ ОРДЕРА (issue #97, часть B): ОТМЕТКА ГОТОВНОСТИ И ВОЗМОЖНЫЙ ВОЗВРАТ.
7
- //
8
- // СУТЬ. В прямом обмене человек вносит средства в эскроу, а после прихода XMR сам зовёт markReady (когда XMR
9
- // подтверждены) либо refund (когда сделка не состоялась). Оба вызова стоят газа в НАТИВНОЙ монете сети, и у
10
- // человека, пришедшего из USDC, своего натива нет. Поэтому ДО котировки решается, СКОЛЬКО НАТИВА ОСТАВИТЬ под
11
- // эти два вызова; решение показывается и в ETH, и в долларах, а строкой входит в разбивку цены.
12
- //
13
- // ПОЧЕМУ ЧИСТЫЕ ФУНКЦИИ. Решение стоит денег и обязано проверяться без сети (tools/check-buy-gas-reserve.mjs)
14
- // и считаться ОДНИМ И ТЕМ ЖЕ кодом на экране и в прогоне. Сеть касается только чтения цены газа - его
15
- // подставляет вызывающий (www/js/evm/prices.js кладёт gasPriceWei в живые цены, экран передаёт его сюда).
16
- //
17
- // ЧИСЛА НЕ ВЫДУМАНЫ. Пределы газа - те же, с которыми НАШ ЖЕ код отправляет обе транзакции
18
- // (движок сделки теперь в пакете: sdk/src/swap-flow.mjs - markReadyOrder - gas 120000, refundOrder - gas 250000). Проверка
19
- // tools/check-buy-gas-reserve.mjs сверяет обе константы со строками того файла, поэтому правка одного места
20
- // без другого краснит прогон - ровно так же, как это сделано для газа забора в rfq/reverseGas.mjs.
21
- //
22
- // ПОЧЕМУ ЗАПАС ФИКСИРУЕТСЯ ДО КОТИРОВКИ. `amount` ордера входит в ПОДПИСАННУЮ котировку, и решение о запасе
23
- // меняет итоговую сумму взноса; значит оно принимается ДО запроса котировки и дальше не пересчитывается.
24
- // Экран кладёт готовый план в состояние формы (www/js/ui/views/swapForm.js), а экран подписи пользуется тем
25
- // же зафиксированным планом - второй пересчёт разошёлся бы с первым.
26
-
27
- // ПРЕДЕЛЫ ГАЗА - ИЗ НАШЕГО ЖЕ КОДА. Строки-источники: sdk/src/swap-flow.mjs (gas: 120_000n у отметки и
28
- // gas: 250_000n у возврата). Заниженный предел - это отказ уже после раскрытия половины; неиспользованный
29
- // газ в лимите не списывается, поэтому запас берётся с запасом.
30
- export const ORDER_READY_GAS_LIMIT = 120_000n; // markReady - sdk/src/swap-flow.mjs markReadyOrder
31
- export const ORDER_REFUND_GAS_LIMIT = 250_000n; // refund - sdk/src/swap-flow.mjs refundOrder
32
- export const ORDER_GAS_LIMIT = ORDER_READY_GAS_LIMIT + ORDER_REFUND_GAS_LIMIT;
33
-
34
- const toBig = (v) => {
35
- if (v === null || v === undefined || v === "") return null;
36
- try { return BigInt(typeof v === "bigint" ? v : String(v)); } catch { return null; }
37
- };
38
-
39
- // СКОЛЬКО ВЕЙ НУЖНО НА ОБА ВЫЗОВА. Цену газа даёт цепь, пределы - наши. Ноль цены и нечитаемое число
40
- // означают «не измерено», и это НЕ ноль газа: неизвестное требование обязано называться, а не выдаваться за
41
- // ноль (то же правило, что у газа забора: rfq/reverseGas.mjs).
42
- export function orderGasReserveWei({ gasPriceWei, readyGasLimit = ORDER_READY_GAS_LIMIT, refundGasLimit = ORDER_REFUND_GAS_LIMIT } = {}) {
43
- const gp = toBig(gasPriceWei);
44
- if (gp === null || gp <= 0n) return null;
45
- const ready = toBig(readyGasLimit);
46
- const refund = toBig(refundGasLimit);
47
- if (ready === null || refund === null || ready <= 0n || refund <= 0n) return null;
48
- return gp * (ready + refund);
49
- }
50
-
51
- // ПОКАЗ В ОБЕИХ ВЕЛИЧИНАХ. У человека, пришедшего из USDC, нет чувства масштаба ETH, поэтому запас показан и
52
- // в ETH, и в долларах. Доллар - оценка по живому курсу натива; нет курса - нет и оценки (null), а не выдумка.
53
- export function gasReserveDisplay({ reserveWei, nativeUsd, decimals = 18 } = {}) {
54
- const w = toBig(reserveWei);
55
- if (w === null || w <= 0n) return null;
56
- const native = Number(w) / 10 ** decimals;
57
- const usd = Number.isFinite(Number(nativeUsd)) && Number(nativeUsd) > 0 ? native * Number(nativeUsd) : null;
58
- return { native, usd };
59
- }
60
-
61
- // РЕШЕНИЕ ПО ГАЛОЧКЕ. Галочка НУЖНА, только когда своего натива НЕ ХВАТАЕТ на оба вызова; при достатке ETH
62
- // она не показывается вовсе - человек платит своим газом, и просить у него нечего. Нечитаемый баланс натива -
63
- // отдельное состояние: запас предлагаем (лучше предложить, чем промолчать о нехватке), и это названо полем.
64
- export function gasReserveChoice({ nativeBalanceWei, reserveWei } = {}) {
65
- const need = toBig(reserveWei);
66
- if (need === null || need <= 0n) return { measured: false, enough: null, show: false, balanceUnread: false };
67
- const have = toBig(nativeBalanceWei);
68
- if (have === null) return { measured: true, enough: false, show: true, balanceUnread: true };
69
- return { measured: true, enough: have >= need, show: have < need, balanceUnread: false };
70
- }
71
-
72
- // ПОЛНОЕ РЕШЕНИЕ ОДНИМ ОБЪЕКТОМ - именно оно фиксируется в состоянии формы до котировки. Все числовые поля -
73
- // строки либо null: так план переживает запись и чтение, и BigInt не превращается в «[object BigInt]».
74
- export function orderGasReservePlan({ gasPriceWei = null, nativeBalanceWei = null, nativeUsd = null, decimals = 18 } = {}) {
75
- const reserveWei = orderGasReserveWei({ gasPriceWei });
76
- const display = reserveWei === null ? null : gasReserveDisplay({ reserveWei, nativeUsd, decimals });
77
- const choice = gasReserveChoice({ nativeBalanceWei, reserveWei });
78
- return {
79
- measured: reserveWei !== null,
80
- gasPriceWei: gasPriceWei === null || gasPriceWei === undefined ? null : String(gasPriceWei),
81
- reserveWei: reserveWei === null ? null : reserveWei.toString(),
82
- native: display ? display.native : null,
83
- usd: display ? display.usd : null,
84
- enough: choice.enough,
85
- show: choice.show,
86
- balanceUnread: Boolean(choice.balanceUnread),
87
- };
88
- }
1
+ // GENERATED FILE - a byte-for-byte copy of the engine module www/js/evm/gasReserve.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
+ // ЗАПАС ГАЗА ПОКУПАТЕЛЯ НА ДЕЙСТВИЯ ОРДЕРА (issue #97, часть B): ОТМЕТКА ГОТОВНОСТИ И ВОЗМОЖНЫЙ ВОЗВРАТ.
7
+ //
8
+ // СУТЬ. В прямом обмене человек вносит средства в эскроу, а после прихода XMR сам зовёт markReady (когда XMR
9
+ // подтверждены) либо refund (когда сделка не состоялась). Оба вызова стоят газа в НАТИВНОЙ монете сети, и у
10
+ // человека, пришедшего из USDC, своего натива нет. Поэтому ДО котировки решается, СКОЛЬКО НАТИВА ОСТАВИТЬ под
11
+ // эти два вызова; решение показывается и в ETH, и в долларах, а строкой входит в разбивку цены.
12
+ //
13
+ // ПОЧЕМУ ЧИСТЫЕ ФУНКЦИИ. Решение стоит денег и обязано проверяться без сети (tools/check-buy-gas-reserve.mjs)
14
+ // и считаться ОДНИМ И ТЕМ ЖЕ кодом на экране и в прогоне. Сеть касается только чтения цены газа - его
15
+ // подставляет вызывающий (www/js/evm/prices.js кладёт gasPriceWei в живые цены, экран передаёт его сюда).
16
+ //
17
+ // ЧИСЛА НЕ ВЫДУМАНЫ. Пределы газа - те же, с которыми НАШ ЖЕ код отправляет обе транзакции
18
+ // (движок сделки теперь в пакете: sdk/src/swap-flow.mjs - markReadyOrder - gas 120000, refundOrder - gas 250000). Проверка
19
+ // tools/check-buy-gas-reserve.mjs сверяет обе константы со строками того файла, поэтому правка одного места
20
+ // без другого краснит прогон - ровно так же, как это сделано для газа забора в rfq/reverseGas.mjs.
21
+ //
22
+ // ПОЧЕМУ ЗАПАС ФИКСИРУЕТСЯ ДО КОТИРОВКИ. `amount` ордера входит в ПОДПИСАННУЮ котировку, и решение о запасе
23
+ // меняет итоговую сумму взноса; значит оно принимается ДО запроса котировки и дальше не пересчитывается.
24
+ // Экран кладёт готовый план в состояние формы (www/js/ui/views/swapForm.js), а экран подписи пользуется тем
25
+ // же зафиксированным планом - второй пересчёт разошёлся бы с первым.
26
+
27
+ // ПРЕДЕЛЫ ГАЗА - ИЗ НАШЕГО ЖЕ КОДА. Строки-источники: sdk/src/swap-flow.mjs (gas: 120_000n у отметки и
28
+ // gas: 250_000n у возврата). Заниженный предел - это отказ уже после раскрытия половины; неиспользованный
29
+ // газ в лимите не списывается, поэтому запас берётся с запасом.
30
+ export const ORDER_READY_GAS_LIMIT = 120_000n; // markReady - sdk/src/swap-flow.mjs markReadyOrder
31
+ export const ORDER_REFUND_GAS_LIMIT = 250_000n; // refund - sdk/src/swap-flow.mjs refundOrder
32
+ export const ORDER_GAS_LIMIT = ORDER_READY_GAS_LIMIT + ORDER_REFUND_GAS_LIMIT;
33
+
34
+ const toBig = (v) => {
35
+ if (v === null || v === undefined || v === "") return null;
36
+ try { return BigInt(typeof v === "bigint" ? v : String(v)); } catch { return null; }
37
+ };
38
+
39
+ // СКОЛЬКО ВЕЙ НУЖНО НА ОБА ВЫЗОВА. Цену газа даёт цепь, пределы - наши. Ноль цены и нечитаемое число
40
+ // означают «не измерено», и это НЕ ноль газа: неизвестное требование обязано называться, а не выдаваться за
41
+ // ноль (то же правило, что у газа забора: rfq/reverseGas.mjs).
42
+ export function orderGasReserveWei({ gasPriceWei, readyGasLimit = ORDER_READY_GAS_LIMIT, refundGasLimit = ORDER_REFUND_GAS_LIMIT } = {}) {
43
+ const gp = toBig(gasPriceWei);
44
+ if (gp === null || gp <= 0n) return null;
45
+ const ready = toBig(readyGasLimit);
46
+ const refund = toBig(refundGasLimit);
47
+ if (ready === null || refund === null || ready <= 0n || refund <= 0n) return null;
48
+ return gp * (ready + refund);
49
+ }
50
+
51
+ // ПОКАЗ В ОБЕИХ ВЕЛИЧИНАХ. У человека, пришедшего из USDC, нет чувства масштаба ETH, поэтому запас показан и
52
+ // в ETH, и в долларах. Доллар - оценка по живому курсу натива; нет курса - нет и оценки (null), а не выдумка.
53
+ export function gasReserveDisplay({ reserveWei, nativeUsd, decimals = 18 } = {}) {
54
+ const w = toBig(reserveWei);
55
+ if (w === null || w <= 0n) return null;
56
+ const native = Number(w) / 10 ** decimals;
57
+ const usd = Number.isFinite(Number(nativeUsd)) && Number(nativeUsd) > 0 ? native * Number(nativeUsd) : null;
58
+ return { native, usd };
59
+ }
60
+
61
+ // РЕШЕНИЕ ПО ГАЛОЧКЕ. Галочка НУЖНА, только когда своего натива НЕ ХВАТАЕТ на оба вызова; при достатке ETH
62
+ // она не показывается вовсе - человек платит своим газом, и просить у него нечего. Нечитаемый баланс натива -
63
+ // отдельное состояние: запас предлагаем (лучше предложить, чем промолчать о нехватке), и это названо полем.
64
+ export function gasReserveChoice({ nativeBalanceWei, reserveWei } = {}) {
65
+ const need = toBig(reserveWei);
66
+ if (need === null || need <= 0n) return { measured: false, enough: null, show: false, balanceUnread: false };
67
+ const have = toBig(nativeBalanceWei);
68
+ if (have === null) return { measured: true, enough: false, show: true, balanceUnread: true };
69
+ return { measured: true, enough: have >= need, show: have < need, balanceUnread: false };
70
+ }
71
+
72
+ // ПОЛНОЕ РЕШЕНИЕ ОДНИМ ОБЪЕКТОМ - именно оно фиксируется в состоянии формы до котировки. Все числовые поля -
73
+ // строки либо null: так план переживает запись и чтение, и BigInt не превращается в «[object BigInt]».
74
+ export function orderGasReservePlan({ gasPriceWei = null, nativeBalanceWei = null, nativeUsd = null, decimals = 18 } = {}) {
75
+ const reserveWei = orderGasReserveWei({ gasPriceWei });
76
+ const display = reserveWei === null ? null : gasReserveDisplay({ reserveWei, nativeUsd, decimals });
77
+ const choice = gasReserveChoice({ nativeBalanceWei, reserveWei });
78
+ return {
79
+ measured: reserveWei !== null,
80
+ gasPriceWei: gasPriceWei === null || gasPriceWei === undefined ? null : String(gasPriceWei),
81
+ reserveWei: reserveWei === null ? null : reserveWei.toString(),
82
+ native: display ? display.native : null,
83
+ usd: display ? display.usd : null,
84
+ enough: choice.enough,
85
+ show: choice.show,
86
+ balanceUnread: Boolean(choice.balanceUnread),
87
+ };
88
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninsei-labs/sdk",
3
- "version": "0.39.0",
3
+ "version": "0.39.1",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"