@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.
@@ -147,3 +147,109 @@ export function decodeAddress(returnedHex) {
147
147
  if (hex.length < 64) throw new Error("короткий ответ: " + returnedHex);
148
148
  return "0x" + hex.slice(24, 64);
149
149
  }
150
+
151
+
152
+ // ============================================================================================
153
+ // РЕАЛИЗАЦИЯ ЭСКРОУ: ЕЁ АДРЕС И ХЕШ ЕЁ КОДА (issue #114, поправка 2).
154
+ // С #114 эскроу каждого ордера - КЛОН EIP-1167 одной общей реализации, а адрес реализации и хеш её кода
155
+ // лежат у фабрики (implementation() / implementationCodeHash()). ФАБРИКА при создании ордера проверяет эту
156
+ // пару; КЛИЕНТ обязан проверить ТУ ЖЕ пару, иначе клон исполнит чужой код, а денежные пути "пройдут" впустую
157
+ // (delegatecall на адрес без кода УСПЕШЕН). Здесь - только чтение полей и вердикт; вызовы к цепи делает
158
+ // носитель (session.js: readContract/readCode), поэтому функция чистая и проверяется без сети.
159
+ // ============================================================================================
160
+
161
+ // Селекторы посчитаны инструментом, не вручную:
162
+ // cast sig "implementation()" -> 0x5c60da1b
163
+ // cast sig "implementationCodeHash()" -> 0xbc0a3981
164
+ export const FACTORY_READS = {
165
+ implementation: "0x5c60da1b",
166
+ implementationCodeHash: "0xbc0a3981",
167
+ };
168
+
169
+ // Чтение поля фабрики без аргументов: calldata равен селектору.
170
+ export function encodeFactoryRead(name) {
171
+ const sel = FACTORY_READS[name];
172
+ if (typeof sel !== "string" || !/^0x[0-9a-f]{8}$/.test(sel)) throw new Error("нет такого чтения фабрики: " + name);
173
+ return sel;
174
+ }
175
+
176
+ // implementation() - один адрес в 32-байтовом слове.
177
+ export function decodeImplementation(returnedHex) {
178
+ return decodeAddress(returnedHex);
179
+ }
180
+
181
+ // implementationCodeHash() - одно слово bytes32.
182
+ export function decodeImplementationCodeHash(returnedHex) {
183
+ const hex = String(returnedHex || "").replace(/^0x/, "");
184
+ if (hex.length < 64) throw new Error("короткий ответ implementationCodeHash(): " + returnedHex);
185
+ return "0x" + hex.slice(0, 64).toLowerCase();
186
+ }
187
+
188
+ // ВЕРДИКТ ПО РЕАЛИЗАЦИИ - те же три условия, что проверяет фабрика (и что закрывает поправки #114):
189
+ // 1) адрес реализации назван и её КОД на цепи непуст: delegatecall на адрес без кода успешен, поэтому
190
+ // клон без реализации принял бы ETH, а markReady/claim/refund "прошли" бы, ничего не сделав;
191
+ // 2) хеш кода реализации равен recorded-хешу фабрики (сравнение делает сама фабрика через codehash);
192
+ // 3) recorded-хеш входит в политику интерфейса (список известных сборок) - если она задана.
193
+ // Если передан hashCode (keccak256 байтов кода), сверяется и живой код: тогда вердикт не доверяет одному
194
+ // лишь ответу фабрики, а пересчитывает хеш из eth_getCode.
195
+ export function verifyFactoryImplementation({ implementation, implementationCodeHash, code, hashCode, policy } = {}) {
196
+ if (!implementation || !/^0x[0-9a-fA-F]{40}$/.test(String(implementation))) {
197
+ return { ok: false, why: "implementation-missing" };
198
+ }
199
+ if (!implementationCodeHash || !/^0x[0-9a-fA-F]{64}$/.test(String(implementationCodeHash))) {
200
+ return { ok: false, why: "code-hash-missing" };
201
+ }
202
+ if (code === undefined || code === null || code === "0x" || code === "0x0") {
203
+ return { ok: false, why: "implementation-has-no-code" };
204
+ }
205
+ const pinned = String(implementationCodeHash).toLowerCase();
206
+ if (Array.isArray(policy) && policy.length) {
207
+ if (!policy.map((h) => String(h).toLowerCase()).includes(pinned)) {
208
+ return { ok: false, why: "implementation-code-unknown", codeHash: pinned };
209
+ }
210
+ }
211
+ if (typeof hashCode === "function") {
212
+ const live = String(hashCode(code)).toLowerCase();
213
+ if (live !== pinned) return { ok: false, why: "implementation-code-mismatch", got: live, want: pinned };
214
+ }
215
+ return { ok: true, codeHash: pinned };
216
+ }
217
+
218
+ // ЧТЕНИЕ ПАРЫ У ФАБРИКИ + ВЕРДИКТ - ОДНО МЕСТО НА ВСЕ НОСИТЕЛИ (SDK, узел, сторож). Читает пару полей
219
+ // фабрики (implementation() / implementationCodeHash()) и КОД по адресу реализации, затем применяет тот же
220
+ // вердикт, что и фабрика при создании ордера (verifyFactoryImplementation выше). Цепь инжектится: read -
221
+ // это eth_call ({to,data}) -> hex, code - это eth_getCode (address) -> hex. Модуль остаётся чистым: ни
222
+ // одного импорта, ни одного сетевого вызова, поэтому проверяется без сети. Носители подставляют СВОИ
223
+ // чтения (SDK - evmCall/evmCode кошелька страницы, узел и сторож - свой RPC). hashCode, если передан,
224
+ // пересчитывает живой код: тогда вердикт не доверяет одному лишь ответу фабрики.
225
+ export async function verifyFactoryImplementationOnChain({ factory, read, code, policy = null, hashCode = null } = {}) {
226
+ if (!factory || !/^0x[0-9a-fA-F]{40}$/.test(String(factory))) {
227
+ return { ok: false, why: "factory-missing" };
228
+ }
229
+ if (typeof read !== "function") return { ok: false, why: "no-chain-read" };
230
+ if (typeof code !== "function") return { ok: false, why: "no-code-read" };
231
+ let implRaw, hashRaw;
232
+ try {
233
+ implRaw = await read({ to: factory, data: FACTORY_READS.implementation });
234
+ hashRaw = await read({ to: factory, data: FACTORY_READS.implementationCodeHash });
235
+ } catch {
236
+ // ЧТЕНИЕ ПАРЫ НЕ УДАЛОСЬ - ЭТО НАЗВАННЫЙ ОТКАЗ, А НЕ МОЛЧАНИЕ. Старая фабрика (без этих геттеров)
237
+ // отвечает revert'ом: пара не прочитана, вердикта нет, и носитель обязан отказать.
238
+ return { ok: false, why: "implementation-unreadable", factory: String(factory).toLowerCase() };
239
+ }
240
+ let implementation, implementationCodeHash;
241
+ try {
242
+ implementation = decodeImplementation(implRaw);
243
+ implementationCodeHash = decodeImplementationCodeHash(hashRaw);
244
+ } catch {
245
+ return { ok: false, why: "implementation-unreadable", factory: String(factory).toLowerCase() };
246
+ }
247
+ let liveCode;
248
+ try {
249
+ liveCode = await code(implementation);
250
+ } catch {
251
+ return { ok: false, why: "implementation-code-unreadable", implementation };
252
+ }
253
+ const verdict = verifyFactoryImplementation({ implementation, implementationCodeHash, code: liveCode, hashCode, policy });
254
+ return { ...verdict, implementation, implementationCodeHash };
255
+ }
@@ -104,17 +104,17 @@ export function toWei(human, decimals = 18) {
104
104
  return BigInt(intPart || "0") * 10n ** BigInt(decimals) + BigInt(frac || "0");
105
105
  }
106
106
 
107
- // ГАЗ ВЫСТАВЛЯЕМ САМИ И С ЗАПАСОМ - ЭТО НЕ ПЕРЕСТРАХОВКА, А ЛЕЧЕНИЕ КОНКРЕТНОЙ ОШИБКИ.
108
- // createOrder и lock уходят двумя транзакциями подряд. Кошелёк оценивает вторую в момент подписи, когда
109
- // эскроу ещё НЕ создан, поэтому считает по пустому адресу обычный перевод: 21000 + стоимость данных. На
110
- // живом контракте это дало лимит 24167 при реальной надобности 50294 - и lock упал по газу, хотя вызов
111
- // был полностью верный (та же сумма, тот же отправитель, тот же адрес). Оценку кошелька здесь доверять
112
- // нельзя, поэтому лимиты задаём явно, с запасом от измеренных значений.
113
- const GAS_CREATE_ORDER = 1_200_000n; // измерено 612567 - запас ~2x: создаётся целый контракт
114
- // Создание И взнос в одной транзакции: создание контракта плюс запись взноса, запас намеренно большой -
115
- // недобор лимита стоит человеку подписи, а лишний газ в лимите на Arbitrum почти ничего не стоит.
116
- const GAS_CREATE_ORDER_AND_FUND = 1_500_000n;
117
- // GAS_LOCK УБРАН вместе с вызовом lock(bytes): взнос идёт в той же транзакции, что и создание ордера.
107
+ // ГАЗ ВЫСТАВЛЯЕМ САМИ И С ЗАПАСОМ - ЭТО ЛЕЧЕНИЕ КОНКРЕТНОЙ ОШИБКИ, А НЕ ПЕРЕСТРАХОВКА.
108
+ // createOrderAndFund уходит ОДНОЙ транзакцией, но кошелёк оценивает её по адресу, где эскроу ещё НЕ создан:
109
+ // вместо создания клона и записи взноса он считает обычный перевод (21000 плюс данные). Оценку кошелька
110
+ // здесь доверять нельзя, поэтому лимит задаём явно, с запасом от ИЗМЕРЕННОГО значения.
111
+ // ИЗМЕРЕНО: полный createOrderAndFund после перехода на клоны EIP-1167 - 315 530 газа (у прежнего целого
112
+ // контракта было 1 190 683); замер и методика - в описании PR в репозитории контрактов. Лимит примерно
113
+ // вдвое выше измеренного: недобор стоит человеку подписи, а лишний газ в лимите почти ничего не стоит.
114
+ const GAS_CREATE_ORDER_AND_FUND = 700_000n;
115
+ // ОТДЕЛЬНОЙ КОНСТАНТЫ «СОЗДАНИЕ БЕЗ ВЗНОСА» НЕТ: целого контракта больше не создаётся - эскроу каждого
116
+ // ордера есть клон EIP-1167 одной общей реализации, условия лежат в коде клона, а фабрика умеет только
117
+ // createOrderAndFund. GAS_LOCK УБРАН вместе с вызовом lock(bytes): взнос идёт в той же транзакции.
118
118
 
119
119
  // СОБЫТИЕ OrderCreated: topic0 посчитан по фактической подписи (и сверяется проверкой watchtower),
120
120
  // topics = [topic0, escrow, locker, claimer]; в data - оба обязательства, обе точки, сумма, readyBy, t1, соль.
@@ -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
+ }