@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.
- package/CHANGELOG.md +866 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/engine/atomic/dleq2.js +330 -0
- package/engine/atomic/halfenc.js +205 -0
- package/engine/atomic/halves.js +119 -0
- package/engine/atomic/order-client.js +57 -0
- package/engine/atomic/order.js +132 -0
- package/engine/atomic/orderContext.js +70 -0
- package/engine/atomic/orderHalfCommit.js +29 -0
- package/engine/core/chainSource.js +306 -0
- package/engine/core/clock.js +40 -0
- package/engine/core/config.js +885 -0
- package/engine/core/finality.js +93 -0
- package/engine/core/format.js +168 -0
- package/engine/core/preSignGuards.js +495 -0
- package/engine/core/rfqSource.js +193 -0
- package/engine/core/sellWindow.js +43 -0
- package/engine/core/store.js +100 -0
- package/engine/core/swap.js +1153 -0
- package/engine/evm/amounts.js +131 -0
- package/engine/evm/auth.js +156 -0
- package/engine/evm/claimGas.js +87 -0
- package/engine/evm/depositor.js +127 -0
- package/engine/evm/dex.js +836 -0
- package/engine/evm/escrow.js +208 -0
- package/engine/evm/factory.js +149 -0
- package/engine/evm/fees.js +90 -0
- package/engine/evm/funding.js +309 -0
- package/engine/evm/gasReserve.js +88 -0
- package/engine/evm/index.js +123 -0
- package/engine/evm/injected.js +175 -0
- package/engine/evm/permit.js +125 -0
- package/engine/evm/prices.js +179 -0
- package/engine/evm/session.js +777 -0
- package/engine/mock/chain.js +98 -0
- package/engine/monero/address.js +265 -0
- package/engine/monero/halvesSweep.js +68 -0
- package/engine/monero/node.js +110 -0
- package/engine/monero/scan.js +325 -0
- package/engine/monero/swapKeys.js +69 -0
- package/engine/monero/unlock.js +46 -0
- package/engine/monero/wallet.js +168 -0
- package/engine/recovery/recoveryFile.js +363 -0
- package/engine/recovery/restore.js +176 -0
- package/engine/sdk/bridge.js +609 -0
- package/engine/ui/dom.js +90 -0
- package/engine/ui/walletModal.js +132 -0
- package/index.d.ts +1417 -0
- package/package.json +74 -0
- package/src/actions.mjs +72 -0
- package/src/adapters/evm-wallet.mjs +126 -0
- package/src/adapters/index.mjs +21 -0
- package/src/adapters/local-storage.mjs +32 -0
- package/src/adapters/monero-wallet.mjs +185 -0
- package/src/adapters/monero.mjs +51 -0
- package/src/adapters/node-test-evm-wallet.mjs +345 -0
- package/src/codes.mjs +188 -0
- package/src/config.mjs +277 -0
- package/src/engine.mjs +53 -0
- package/src/errors.mjs +24 -0
- package/src/http.mjs +72 -0
- package/src/index.mjs +256 -0
- package/src/legs/evm.mjs +118 -0
- package/src/legs/index.mjs +50 -0
- package/src/levelsWalkSpec.mjs +183 -0
- package/src/lock.mjs +140 -0
- package/src/order.mjs +212 -0
- package/src/preflight.mjs +137 -0
- package/src/primitives.mjs +47 -0
- package/src/quoteEip712.mjs +35 -0
- package/src/quoteEip712Spec.mjs +214 -0
- package/src/quotes.mjs +465 -0
- package/src/record.mjs +65 -0
- package/src/recovery.mjs +40 -0
- package/src/sim.mjs +37 -0
- package/src/swap-flow.mjs +492 -0
- package/src/swaps.mjs +935 -0
- package/src/sweep.mjs +222 -0
- package/src/wallet.mjs +164 -0
- package/src/xmr-arrival.mjs +289 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/core/preSignGuards.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
|
+
// ЗАЩИТЫ ПЕРЕД ПОДПИСЬЮ: НЕДОПУСТИМАЯ СДЕЛКА НЕ НАЧИНАЕТСЯ (док 45).
|
|
7
|
+
//
|
|
8
|
+
// ЗАЧЕМ ОТДЕЛЬНЫЙ МОДУЛЬ. До сих пор защиты жили врозь: одна - в реестре маршрутов (evm/dex.js:
|
|
9
|
+
// покрытие суммы и цена DEX-ноги), прочие - в виде проверок, разбросанных по форме и по потоку ордера. От
|
|
10
|
+
// этого заводится ровно тот дефект, из-за которого файл и написан: правило есть, а подпись не
|
|
11
|
+
// останавливается, потому что экран его не спросил. Здесь правила сведены в ОДНО место, у них ОДНА
|
|
12
|
+
// форма ответа, и сводка (signingGateVerdict) отвечает на единственный вопрос, который решает дело:
|
|
13
|
+
// начинать подпись или нет.
|
|
14
|
+
//
|
|
15
|
+
// ЧТО ЗНАЧИТ ОТВЕТ. Каждый приговор несёт три поля, и они разные по смыслу:
|
|
16
|
+
// blocks - подписывать НЕЛЬЗЯ (это и есть защита; вызывающий ОБЯЗАН считаться с этим полем);
|
|
17
|
+
// checked - удалось ли вообще проверить (true/false);
|
|
18
|
+
// reason - слова, и в них ЧИСЛА: человеку нужно не "нельзя", а "нельзя, потому что вот столько".
|
|
19
|
+
// Отличать checked от blocks обязательно, и вот почему на двух примерах из этого файла:
|
|
20
|
+
//
|
|
21
|
+
// * НЕПРОВЕРЕННЫЙ БАЛАНС БЛОКИРУЕТ. Прочитать баланс мы умеем всегда, и "не прочитали" здесь значит
|
|
22
|
+
// "не знаем, хватает ли денег на то, что человек собирается подписать". Пропустить это значило бы
|
|
23
|
+
// отправить человека подписывать отказ кошелька.
|
|
24
|
+
// * НЕИЗМЕРЕННЫЙ ЗАПАС НА ГАЗ НЕ БЛОКИРУЕТ, И ЭТО РЕШЕНИЕ, А НЕ ЗАБЫВЧИВОСТЬ. Отличие от проверки
|
|
25
|
+
// цены DEX-ноги принципиальное: негодная цена означает, что сделка НЕВЕРНАЯ, а неизмеренный газ -
|
|
26
|
+
// всего лишь "запас неизвестен". Сама сделка от этого не становится недопустимой, а нехватку
|
|
27
|
+
// топлива на транзакцию назовёт кошелёк. Поэтому такой случай помечается словами (checked: false,
|
|
28
|
+
// kind reserve-unstated) и НЕ запирает подпись.
|
|
29
|
+
// * НЕ НАЗВАННЫЙ ПРОВАЙДЕРОМ ДИАПАЗОН ОБЪЁМА - ТАК ЖЕ: помечается, не запирает (решение уже принято
|
|
30
|
+
// рынком: "объём не назван - ограничивать нечем", www/js/mock/market.js: coversSize).
|
|
31
|
+
//
|
|
32
|
+
// И ЕЩЁ ОДНО, ЧТО ЗДЕСЬ ПРИНЦИПИАЛЬНО. Все приговоры - ЧИСТЫЕ функции: ни сети, ни кошелька, ни DOM.
|
|
33
|
+
// Поэтому каждую защиту можно прогнать на числах, которые её ЛОМАЮТ, и увидеть, что она краснеет:
|
|
34
|
+
// "код есть" не равно "отказ работает" (tools/check-signing-guards.mjs).
|
|
35
|
+
//
|
|
36
|
+
// ПОРОГИ ПРИХОДЯТ ДАННЫМИ (реестр: SIGNING_GUARDS в core/config.js), а не лежат здесь числами: менять
|
|
37
|
+
// их - правка данных, а не логики.
|
|
38
|
+
|
|
39
|
+
// wei из того, что дал вызывающий (BigInt, строка, число). Не разобралось - null, и это НЕ ноль:
|
|
40
|
+
// ноль баланса и "баланс неизвестен" - два разных состояния, и путать их нельзя.
|
|
41
|
+
const wei = (v) => {
|
|
42
|
+
if (v === null || v === undefined || v === "") return null;
|
|
43
|
+
try { const n = BigInt(v); return n >= 0n ? n : null; } catch { return null; }
|
|
44
|
+
};
|
|
45
|
+
// КОЛИЧЕСТВО В ЧЕЛОВЕЧЕСКОМ ВИДЕ - ОТДЕЛЬНО ОТ human. Размер объёма и границы диапазона приходят УЖЕ
|
|
46
|
+
// человеческими числами (единицы ASSET пары, док 41 §5), а не wei, и делить их на 10^decimals нечего:
|
|
47
|
+
// общая функция дала бы в отказе числа, отличающиеся от настоящих в 10^14 раз - то есть уверенный, но
|
|
48
|
+
// неверный ответ ("минимум 0.00001 XMR" вместо "0.1 XMR"). Поэтому формат свой.
|
|
49
|
+
const qty = (v) => String(Number(Number(v).toFixed(4)));
|
|
50
|
+
const finite = (v) => (v === null || v === undefined || v === "" ? null : (Number.isFinite(Number(v)) ? Number(v) : null));
|
|
51
|
+
// Число в человеческом виде для слов: 6 знаков после запятой хватает и стейблу, и нативу, а полный
|
|
52
|
+
// wei в тексте человек не читает.
|
|
53
|
+
const human = (v, decimals = 18) => {
|
|
54
|
+
const n = Number(v) / 10 ** Number(decimals);
|
|
55
|
+
if (!Number.isFinite(n)) return String(v);
|
|
56
|
+
return String(Number(n.toFixed(6)));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// 1) СУММА ПРОТИВ ДИАПАЗОНА КОТИРОВКИ. Диапазон называет провайдер (min/max/step), и за его границы
|
|
61
|
+
// он не берётся: размер вне диапазона - это НЕ "попробуем", а отказ, и он должен случиться ДО подписи,
|
|
62
|
+
// а не у провайдера после неё.
|
|
63
|
+
// ЕДИНИЦА - ЧАСТЬ ПРОВЕРКИ, А НЕ УКРАШЕНИЕ (док 41, §5): объём и границы обязаны быть в ОДНОЙ единице
|
|
64
|
+
// (ASSET пары), иначе сравниваются разные величины, и "в диапазоне" значит ничего.
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
export function rangeVerdict({ size = null, unit = null, min = null, max = null, step = null, quoteUnit = null } = {}) {
|
|
67
|
+
const u = String(unit || quoteUnit || "").toUpperCase();
|
|
68
|
+
const statedUnit = String(quoteUnit || "").toUpperCase();
|
|
69
|
+
if (statedUnit && u && statedUnit !== u) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false, checked: true, blocks: true, kind: "unit-mismatch",
|
|
72
|
+
reason: `the size is measured in ${u} while the quote states its range in ${statedUnit} - the two cannot be compared`,
|
|
73
|
+
size, min, max, step, unit: u, quoteUnit: statedUnit,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const s = finite(size);
|
|
77
|
+
if (s === null || s <= 0) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false, checked: false, blocks: false, kind: "size-unstated",
|
|
80
|
+
reason: "the size is not entered, so the provider's range cannot be checked",
|
|
81
|
+
size: null, min: finite(min), max: finite(max), step: finite(step), unit: u,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const lo = finite(min), hi = finite(max), st = finite(step);
|
|
85
|
+
if (lo === null && hi === null) {
|
|
86
|
+
// Провайдер не назвал границ - это его слово, а не наша ошибка: помечаем и не запираем.
|
|
87
|
+
return {
|
|
88
|
+
ok: true, checked: false, blocks: false, kind: "range-unstated",
|
|
89
|
+
reason: "the provider did not state a volume range (min/max), so the size could not be checked against it",
|
|
90
|
+
size: s, min: null, max: null, step: st, unit: u,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const base = { size: s, min: lo, max: hi, step: st, unit: u, quoteUnit: statedUnit || u };
|
|
94
|
+
if (lo !== null && s < lo) {
|
|
95
|
+
return { ok: false, checked: true, blocks: true, kind: "below-min", ...base,
|
|
96
|
+
reason: `the order size ${qty(s)} ${u} is below the provider minimum ${qty(lo)} ${u} - short by ${qty(lo - s)} ${u}: the provider does not take this size` };
|
|
97
|
+
}
|
|
98
|
+
if (hi !== null && s > hi) {
|
|
99
|
+
return { ok: false, checked: true, blocks: true, kind: "above-max", ...base,
|
|
100
|
+
reason: `the order size ${qty(s)} ${u} is above the provider maximum ${qty(hi)} ${u} - over by ${qty(s - hi)} ${u}: the provider does not take this size` };
|
|
101
|
+
}
|
|
102
|
+
if (st !== null && st > 0) {
|
|
103
|
+
// Шаг считается ОТ min, а не от нуля: min=0.05 при шаге 0.1 не даёт больше ни одного размера,
|
|
104
|
+
// и это правило рынка (www/js/mock/market.js: coversSize), а не выдумка этой функции.
|
|
105
|
+
const from = lo !== null ? lo : 0;
|
|
106
|
+
const grid = (s - from) / st;
|
|
107
|
+
if (Math.abs(grid - Math.round(grid)) > 1e-6) {
|
|
108
|
+
return { ok: false, checked: true, blocks: true, kind: "off-step", ...base,
|
|
109
|
+
reason: `the order size ${qty(s)} ${u} does not sit on the ${qty(st)} ${u} step from ${qty(from)} ${u}` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { ok: true, checked: true, blocks: false, kind: "in-range", ...base,
|
|
113
|
+
reason: `the order size ${qty(s)} ${u} is inside the provider range ${lo === null ? "?" : qty(lo)}..${hi === null ? "?" : qty(hi)} ${u}` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// 2) БАЛАНС ОПЛАТЫ. Не хватает того, чем платят, - подписывать нечего: транзакция откажет в кошельке,
|
|
118
|
+
// и человек узнает о нехватке ПОСЛЕ подписи. Неизвестный баланс (не прочитан) тоже запирает: см.
|
|
119
|
+
// рассуждение в шапке файла.
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
export function payBalanceVerdict({ payBalanceWei = null, payAmountWei = null, symbol = null, decimals = 18 } = {}) {
|
|
122
|
+
const have = wei(payBalanceWei);
|
|
123
|
+
const need = wei(payAmountWei);
|
|
124
|
+
if (need === null || need <= 0n) {
|
|
125
|
+
return { ok: false, checked: false, blocks: false, kind: "amount-unstated",
|
|
126
|
+
reason: "the amount to fund is not known, so the wallet balance cannot be checked against it" };
|
|
127
|
+
}
|
|
128
|
+
if (have === null) {
|
|
129
|
+
return { ok: false, checked: false, blocks: true, kind: "balance-unchecked",
|
|
130
|
+
reason: "the wallet balance was not read, so it is unknown whether the wallet covers the order - signing is blocked" };
|
|
131
|
+
}
|
|
132
|
+
if (have < need) {
|
|
133
|
+
return { ok: false, checked: true, blocks: true, kind: "balance-short",
|
|
134
|
+
haveWei: have.toString(), needWei: need.toString(), shortfallWei: (need - have).toString(),
|
|
135
|
+
reason: `the wallet holds ${human(have, decimals)} ${symbol} but the order locks ${human(need, decimals)} ${symbol} - short by ${human(need - have, decimals)} ${symbol}` };
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, checked: true, blocks: false, kind: "balance-enough",
|
|
138
|
+
haveWei: have.toString(), needWei: need.toString(),
|
|
139
|
+
reason: `the wallet holds ${human(have, decimals)} ${symbol} for the order's ${human(need, decimals)} ${symbol}` };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// 3) ЗАПАС НА ГАЗ. Фондирование - транзакция, и она стоит газа в НАТИВНОЙ монете. Поэтому проверяется
|
|
144
|
+
// не только сумма ордера, но и сумма ордера ПЛЮС запас: у аккаунта, где лежит ровно amount, транзакция
|
|
145
|
+
// не пройдёт. Требуемое здесь считает вызывающий (amount + reserve), а не эта функция: одна величина
|
|
146
|
+
// вместо двух веток - меньше места для расхождения.
|
|
147
|
+
// Неизмеренный запас НЕ запирает: см. шапку файла (это отличие от проверки цены осознанное).
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
export function gasReserveVerdict({ nativeBalanceWei = null, requiredNativeWei = null, gasReserveWei = null, symbol = null, decimals = 18 } = {}) {
|
|
150
|
+
const need = wei(requiredNativeWei);
|
|
151
|
+
const reserve = wei(gasReserveWei);
|
|
152
|
+
const have = wei(nativeBalanceWei);
|
|
153
|
+
if (reserve === null || reserve <= 0n) {
|
|
154
|
+
return { ok: true, checked: false, blocks: false, kind: "reserve-unstated",
|
|
155
|
+
reason: "the gas reserve was not measured, so the gas part of the funding could not be checked (the wallet will name any shortfall)" };
|
|
156
|
+
}
|
|
157
|
+
if (need === null) {
|
|
158
|
+
return { ok: false, checked: false, blocks: false, kind: "amount-unstated", reserveWei: reserve.toString(),
|
|
159
|
+
reason: "the amount to fund is not known, so the native balance cannot be checked against amount + gas" };
|
|
160
|
+
}
|
|
161
|
+
if (have === null) {
|
|
162
|
+
return { ok: false, checked: false, blocks: true, kind: "native-unchecked", needWei: need.toString(), reserveWei: reserve.toString(),
|
|
163
|
+
reason: "the native balance was not read, so it is unknown whether the wallet covers amount + gas - signing is blocked" };
|
|
164
|
+
}
|
|
165
|
+
const total = need + reserve;
|
|
166
|
+
if (have < total) {
|
|
167
|
+
return { ok: false, checked: true, blocks: true, kind: "gas-short",
|
|
168
|
+
haveWei: have.toString(), needWei: need.toString(), reserveWei: reserve.toString(), requiredTotalWei: total.toString(),
|
|
169
|
+
shortfallWei: (total - have).toString(),
|
|
170
|
+
reason: `funding needs ${human(need, decimals)} ${symbol} plus ${human(reserve, decimals)} ${symbol} of gas, but the wallet holds ${human(have, decimals)} ${symbol} - short by ${human(total - have, decimals)} ${symbol}` };
|
|
171
|
+
}
|
|
172
|
+
return { ok: true, checked: true, blocks: false, kind: "gas-enough",
|
|
173
|
+
haveWei: have.toString(), needWei: need.toString(), reserveWei: reserve.toString(), requiredTotalWei: total.toString(),
|
|
174
|
+
reason: `the wallet holds ${human(have, decimals)} ${symbol}: ${human(need, decimals)} for the order and ${human(reserve, decimals)} for gas` };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// 4) СВЕЖЕСТЬ КОТИРОВКИ. Правило продукта: просроченную (старше её собственного ttlMs) котировку не
|
|
179
|
+
// показывает никто, и подписать по ней нельзя - цена в ней уже не та, что человек видит. Метка
|
|
180
|
+
// считается по СВОЕЙ метке котировки (at), а не по времени ответа: иначе возраст "омолаживался" бы
|
|
181
|
+
// каждым опросом (док 41, §7).
|
|
182
|
+
// Нет метки или нет срока - проверить нечего; это помечается и не запирает (у рынка то же решение:
|
|
183
|
+
// "срок котировки провайдер не назвал" на экране, а не закрытие рынка).
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
export function quoteFreshnessVerdict({ at = null, ttlMs = null, expiresAt = null, nowMs = null } = {}) {
|
|
186
|
+
const now = finite(nowMs) === null ? Date.now() : Number(nowMs);
|
|
187
|
+
const exp = finite(expiresAt);
|
|
188
|
+
if (exp !== null && exp <= now) {
|
|
189
|
+
return { ok: false, checked: true, blocks: true, kind: "expired", expiresAt: exp, nowMs: now,
|
|
190
|
+
reason: `the order quote expired ${Math.round((now - exp) / 1000)}s ago` };
|
|
191
|
+
}
|
|
192
|
+
const stamp = finite(at);
|
|
193
|
+
if (stamp === null) {
|
|
194
|
+
return { ok: false, checked: false, blocks: false, kind: "untimed",
|
|
195
|
+
reason: "the quote carries no timestamp, so its age could not be checked" };
|
|
196
|
+
}
|
|
197
|
+
const ttl = finite(ttlMs);
|
|
198
|
+
const ageMs = Math.max(0, now - stamp);
|
|
199
|
+
if (ttl === null || ttl <= 0) {
|
|
200
|
+
return { ok: true, checked: false, blocks: false, kind: "no-ttl", at: stamp, ageMs,
|
|
201
|
+
reason: `the provider did not state how long the quote lives, so only its age is known: said ${Math.round(ageMs / 1000)}s ago` };
|
|
202
|
+
}
|
|
203
|
+
if (ageMs > ttl) {
|
|
204
|
+
return { ok: false, checked: true, blocks: true, kind: "stale", at: stamp, ttlMs: ttl, ageMs,
|
|
205
|
+
overMs: ageMs - ttl,
|
|
206
|
+
reason: `the quote is ${Math.round(ageMs / 1000)}s old while it was valid for ${Math.round(ttl / 1000)}s - it expired ${Math.round((ageMs - ttl) / 1000)}s ago` };
|
|
207
|
+
}
|
|
208
|
+
return { ok: true, checked: true, blocks: false, kind: "fresh", at: stamp, ttlMs: ttl, ageMs,
|
|
209
|
+
reason: `the quote is ${Math.round(ageMs / 1000)}s old and valid for ${Math.round(ttl / 1000)}s` };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// 5) СРОКИ ОРДЕРА (readyBy / t1). Срок в прошлом означает ордер, созданный уже закрытым: забрать по
|
|
214
|
+
// нему нельзя, деньги повиснут до возврата. Слишком тесные сроки означают то же самое, но незаметно:
|
|
215
|
+
// окно отметки готовности короче, чем занимает само подтверждение XMR, а окно расчёта короче, чем нужно
|
|
216
|
+
// мейкеру на забор. Слишком далёкий срок - третья крайность (issue #88): вернуть ETH можно только до
|
|
217
|
+
// `readyBy` и после `t1`, значит ордер с `t1` в году запирает деньги на год. Отсюда потолок.
|
|
218
|
+
// Пороги - данные (SIGNING_GUARDS).
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
export function deadlineVerdict({ nowSec = null, readyBy = null, t1 = null, minReadyLeadSec = null, minClaimWindowSec = null, maxDeadlineSec = null } = {}) {
|
|
221
|
+
const now = finite(nowSec) === null ? Math.floor(Date.now() / 1000) : Number(nowSec);
|
|
222
|
+
const r = finite(readyBy), c = finite(t1);
|
|
223
|
+
if (r === null || c === null) {
|
|
224
|
+
return { ok: false, checked: false, blocks: true, kind: "deadlines-unstated",
|
|
225
|
+
reason: "the order's deadlines are not stated, so neither the ready window nor the claim window can be checked - signing is blocked" };
|
|
226
|
+
}
|
|
227
|
+
if (r <= now) {
|
|
228
|
+
return { ok: false, checked: true, blocks: true, kind: "ready-by-in-past", readyBy: r, nowSec: now,
|
|
229
|
+
reason: `the order would be created already past its ready-by deadline (${r} is ${Math.round(now - r)}s ago): nothing could be claimed in it` };
|
|
230
|
+
}
|
|
231
|
+
if (c <= r) {
|
|
232
|
+
return { ok: false, checked: true, blocks: true, kind: "t1-not-after-ready-by", readyBy: r, t1: c,
|
|
233
|
+
reason: `t1 (${c}) must be later than readyBy (${r}): an overlapping window lets claim and refund both be valid at once` };
|
|
234
|
+
}
|
|
235
|
+
const lead = finite(minReadyLeadSec), claim = finite(minClaimWindowSec);
|
|
236
|
+
if (lead !== null && r - now < lead) {
|
|
237
|
+
return { ok: false, checked: true, blocks: true, kind: "ready-window-too-short", readyBy: r, nowSec: now, minReadyLeadSec: lead,
|
|
238
|
+
reason: `the ready-by deadline is only ${Math.round(r - now)}s away while the mark-ready window needs at least ${lead}s - XMR confirmations alone take minutes` };
|
|
239
|
+
}
|
|
240
|
+
if (claim !== null && c - r < claim) {
|
|
241
|
+
return { ok: false, checked: true, blocks: true, kind: "claim-window-too-short", readyBy: r, t1: c, minClaimWindowSec: claim,
|
|
242
|
+
reason: `the claim window is ${Math.round(c - r)}s while at least ${claim}s is needed to take the funds before the refund opens` };
|
|
243
|
+
}
|
|
244
|
+
// ПОТОЛОК СРОКА (issue #88). Без него срок можно поставить на год вперёд: обе стороны подпишут ордер,
|
|
245
|
+
// ETH уйдут в эскроу - и вернуть их будет нельзя ни до `readyBy`, ни до `t1`, то есть целый год. Потолок
|
|
246
|
+
// считается от `t1` (самого дальнего срока): `readyBy` всегда ближе, значит эта проверка покрывает оба.
|
|
247
|
+
const cap = finite(maxDeadlineSec);
|
|
248
|
+
if (cap !== null && c - now > cap) {
|
|
249
|
+
return { ok: false, checked: true, blocks: true, kind: "deadline-too-far", readyBy: r, t1: c, nowSec: now, maxDeadlineSec: cap,
|
|
250
|
+
reason: `the order's deadline is ${Math.round(c - now)}s (${Math.round((c - now) / 3600)}h) away while at most ${cap}s (${Math.round(cap / 3600)}h) is allowed: a deposit locked that long could not be refunded until t1` };
|
|
251
|
+
}
|
|
252
|
+
return { ok: true, checked: true, blocks: false, kind: "deadlines-ok", readyBy: r, t1: c, nowSec: now,
|
|
253
|
+
reason: `ready in ${Math.round(r - now)}s, claim window ${Math.round(c - r)}s` };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// 6) СОСТОЯНИЕ ЭСКРОУ. Контракт запрещает второе внесение (AlreadyFunded) и не даёт забрать дважды,
|
|
258
|
+
// поэтому "подписаться ещё раз" в уже зафондированный, забранный или возвращённый эскроу - это не
|
|
259
|
+
// "на всякий случай проверим": вторая подпись либо откатится, либо создаст ВТОРОЙ ордер вместо
|
|
260
|
+
// прежнего. Читается вызовом status() по предсказанному адресу - до подписи.
|
|
261
|
+
// Отдельно про пустой ответ: по адресу, которого ещё нет, eth_call возвращает пусто - это "ордер
|
|
262
|
+
// новый", а не "проверить не удалось". Ошибка вызова - другое дело, и она запирает.
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
export function escrowStateVerdict({ status = null, error = null, address = null } = {}) {
|
|
265
|
+
const where = address ? " the escrow at " + address : " the escrow";
|
|
266
|
+
if (error) {
|
|
267
|
+
return { ok: false, checked: false, blocks: true, kind: "state-unchecked", error: String(error),
|
|
268
|
+
reason: `the state of${where} could not be read (${String(error)}), so it is unknown whether this order already exists - signing is blocked` };
|
|
269
|
+
}
|
|
270
|
+
if (!status) {
|
|
271
|
+
return { ok: true, checked: true, blocks: false, kind: "new", address: address || null,
|
|
272
|
+
reason: address ? `no escrow exists at ${address} yet: this is a new order` : "no escrow exists yet: this is a new order" };
|
|
273
|
+
}
|
|
274
|
+
if (status.isClaimed) {
|
|
275
|
+
return { ok: false, checked: true, blocks: true, kind: "already-claimed", status, address: address || null,
|
|
276
|
+
reason: `${where} is already claimed: the order is settled, a new signature would create a second order` };
|
|
277
|
+
}
|
|
278
|
+
if (status.isRefunded) {
|
|
279
|
+
return { ok: false, checked: true, blocks: true, kind: "already-refunded", status, address: address || null,
|
|
280
|
+
reason: `${where} was already refunded: the order is closed, a new signature would create a second order` };
|
|
281
|
+
}
|
|
282
|
+
if (status.isFunded) {
|
|
283
|
+
return { ok: false, checked: true, blocks: true, kind: "already-funded", status, address: address || null,
|
|
284
|
+
reason: `${where} is already funded: the contract refuses a second funding (AlreadyFunded) - top up that order instead of signing a new one` };
|
|
285
|
+
}
|
|
286
|
+
return { ok: true, checked: true, blocks: false, kind: "new", status, address: address || null,
|
|
287
|
+
reason: `${where} exists but holds nothing: this is a new order` };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// 7) СОГЛАСИЕ НА ERC-20. Появится вместе с исполнением DEX-ноги: своп токена требует approve, и
|
|
292
|
+
// разрешение на списание - это ОТДЕЛЬНОЕ действие человека, а не следствие подписи свопа. Поэтому
|
|
293
|
+
// здесь две проверки, и обе обязательны: разрешение выдано (allowance хватает) И человек на него
|
|
294
|
+
// согласился явно. Достаток без согласия и согласие без достатка - разные отказы, и называются они
|
|
295
|
+
// по-разному.
|
|
296
|
+
// ПОТРЕБИТЕЛЬ У ЭТОЙ ЗАЩИТЫ УЖЕ ЕСТЬ (24 сентября 2026): сборщик исполнения DEX-ноги
|
|
297
|
+
// (www/js/evm/dexExec.js, swapAllowanceVerdict) зовёт её, чтобы отличить "разрешения не хватает" от
|
|
298
|
+
// "человек на него не согласился" - это РАЗНЫЕ отказы и называются они по-разному. В ПОДПИСЬ формы
|
|
299
|
+
// она по-прежнему не включена: форма ещё не свопает (своп - платный этап, док 48), и включать проверку
|
|
300
|
+
// в подпись раньше появления самого действия значило бы запирать человека на том, чего он ещё не делает.
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
export function allowanceVerdict({ tokenIsNative = false, allowanceWei = null, amountWei = null, consent = false, symbol = null, decimals = 18 } = {}) {
|
|
303
|
+
if (tokenIsNative) {
|
|
304
|
+
return { ok: true, checked: true, blocks: false, kind: "not-needed",
|
|
305
|
+
reason: "the payment is the network's own coin: no ERC-20 approval and no pool are involved" };
|
|
306
|
+
}
|
|
307
|
+
const need = wei(amountWei), have = wei(allowanceWei);
|
|
308
|
+
if (consent !== true) {
|
|
309
|
+
return { ok: false, checked: false, blocks: true, kind: "consent-missing", needWei: need === null ? null : need.toString(),
|
|
310
|
+
reason: `swapping ${symbol || "the token"} needs an ERC-20 approval for you to sign, and no consent was given for it` };
|
|
311
|
+
}
|
|
312
|
+
if (have === null) {
|
|
313
|
+
return { ok: false, checked: false, blocks: true, kind: "allowance-unchecked",
|
|
314
|
+
reason: `the existing allowance for ${symbol || "the token"} was not read, so it is unknown whether the approval is needed - signing is blocked` };
|
|
315
|
+
}
|
|
316
|
+
if (need === null || need <= 0n) {
|
|
317
|
+
return { ok: false, checked: false, blocks: false, kind: "amount-unstated",
|
|
318
|
+
reason: "the amount to swap is not known, so the allowance cannot be checked against it" };
|
|
319
|
+
}
|
|
320
|
+
if (have < need) {
|
|
321
|
+
return { ok: false, checked: true, blocks: true, kind: "allowance-short",
|
|
322
|
+
allowanceWei: have.toString(), needWei: need.toString(),
|
|
323
|
+
reason: `the approved amount is ${human(have, decimals)} ${symbol || ""} but the swap spends ${human(need, decimals)} ${symbol || ""}: the approval must be raised first` };
|
|
324
|
+
}
|
|
325
|
+
return { ok: true, checked: true, blocks: false, kind: "allowed",
|
|
326
|
+
allowanceWei: have.toString(), needWei: need.toString(),
|
|
327
|
+
reason: `the approved amount ${human(have, decimals)} ${symbol || ""} covers the swap of ${human(need, decimals)} ${symbol || ""}` };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// 8) СЕТЬ КОШЕЛЬКА. Балансы читаются через кошелёк, то есть ИЗ ЕГО СЕТИ, и транзакция уйдёт туда же.
|
|
332
|
+
// Поэтому несовпадение сети - не предупреждение, а отказ: "страница на одной сети, кошелёк на другой"
|
|
333
|
+
// означает, что мы не знаем ни балансов, ни того, куда уйдут деньги. Признак сети кошелька берётся из
|
|
334
|
+
// session.js (chainIdOf), признак сети расчёта - из реестра (chainId выбранной сети).
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
export function networkGateVerdict({ walletChainId = null, selectedChainId = null, settlementChainId = null, selectedChainName = null } = {}) {
|
|
337
|
+
const w = finite(walletChainId), s = finite(selectedChainId), set = finite(settlementChainId);
|
|
338
|
+
if (w === null) {
|
|
339
|
+
return { ok: false, checked: false, blocks: true, kind: "wallet-unknown",
|
|
340
|
+
reason: "the wallet's network is unknown, so balances and the funding transaction have no network - signing is blocked" };
|
|
341
|
+
}
|
|
342
|
+
if (s === null) {
|
|
343
|
+
return { ok: false, checked: false, blocks: true, kind: "network-unknown",
|
|
344
|
+
reason: "the page's settlement network is unknown, so it cannot be compared with the wallet's - signing is blocked" };
|
|
345
|
+
}
|
|
346
|
+
if (w !== s) {
|
|
347
|
+
return { ok: false, checked: true, blocks: true, kind: "wallet-network", walletChainId: w, selectedChainId: s,
|
|
348
|
+
reason: `the wallet is on chain ${w} while this page settles on chain ${s}${selectedChainName ? " (" + selectedChainName + ")" : ""}: balances and the funding transaction would come from another network` };
|
|
349
|
+
}
|
|
350
|
+
if (set !== null && set !== s) {
|
|
351
|
+
return { ok: false, checked: true, blocks: true, kind: "settlement-network", walletChainId: w, selectedChainId: s, settlementChainId: set,
|
|
352
|
+
reason: `the escrow contract sits on chain ${set} while the page is set to chain ${s}: the money would go to a network where this order's escrow does not exist` };
|
|
353
|
+
}
|
|
354
|
+
return { ok: true, checked: true, blocks: false, kind: "network-match", chainId: s,
|
|
355
|
+
reason: `the wallet and the page agree on chain ${s}${selectedChainName ? " (" + selectedChainName + ")" : ""}` };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// СВОДКА. Вызывающий собирает то, что у него есть, и получает ОДИН ответ: можно ли начинать подпись.
|
|
360
|
+
// Порядок проверок задаётся порядком массива: причина называется первая по списку, потому что при
|
|
361
|
+
// неверной сети вопрос о покрытии суммы уже не имеет смысла.
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// ЖИВАЯ СЕТЬ НЕ ПОДПИСЫВАЕТ СДЕЛКУ С ЗАГЛУШКОЙ. В демке сторона контрагента собирается локально и помечена
|
|
364
|
+
// standIn: тогда DLEQ проверяется сам с собой и не доказывает ничего - это режим витрины, а не сделки.
|
|
365
|
+
// На живой сети такой подписи быть не должно: деньги ушли бы против несуществующей стороны.
|
|
366
|
+
export function counterpartyVerdict({ standIn = false, liveChain = false, network = null, chainId = null } = {}) {
|
|
367
|
+
const v = (ok, kind, words) => ({ ok, checked: true, blocks: !ok, kind, words, reason: words });
|
|
368
|
+
// СЕТЬ НАЗЫВАЕТСЯ ЧИСЛОМ И КОДОМ, а не словом «боевая»: по отказу должно быть видно, о какой сети речь
|
|
369
|
+
// (у одной и той же ноды их может быть несколько), и это тот же стандарт, что у прочих отказов - причина
|
|
370
|
+
// без числа считается дефектом, потому что по ней нельзя понять, что именно случилось.
|
|
371
|
+
const where = (network || "this network") + (chainId === null || chainId === undefined ? "" : " (chainId " + chainId + ")");
|
|
372
|
+
if (!standIn) return v(true, "real-provider", "counterparty is a real provider: the proof is checked against it");
|
|
373
|
+
if (liveChain) {
|
|
374
|
+
return v(false, "standin-on-production",
|
|
375
|
+
`${where} is a live network and the counterparty is a local stand-in: the proof would be checked against itself, so the signature stays locked until a real provider answers`);
|
|
376
|
+
}
|
|
377
|
+
return v(true, "standin-on-testnet", `demo mode: the counterparty is a local stand-in and ${where} is a testnet (not a live network), the proof proves nothing by itself`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// 9) ТОЧКИ ОРДЕРА В ЦЕПИ ПРОТИВ ТОЧЕК, НА КОТОРЫХ СОШЁЛСЯ DLEQ (находка P0-3 аудита).
|
|
383
|
+
// DLEQ доказывает связь точки с половиной, но сам по себе не мешает положить в ордер ДРУГИЕ точки:
|
|
384
|
+
// доказательство сойдётся на одних, а в createOrderAndFund уйдут другие - и после claim забор XMR
|
|
385
|
+
// не соберётся, потому что ключ траты выведен из других половин. Цепь этого не проверит (в EVM нет
|
|
386
|
+
// ed25519), поэтому проверяет та сторона, которая рискует, и ДО своего действия. Здесь только решается
|
|
387
|
+
// вопрос «заперта ли подпись»; сама сверка берёт точки ИЗ СЛОТОВ ЖИВОГО ЭСКРОУ, а не из памяти формы.
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
const normHex = (v) => (v === null || v === undefined ? "" : String(v).replace(/^0x/i, "").toLowerCase());
|
|
390
|
+
export function escrowPointsVerdict({ role = null, what = null, point = null, onchainPoint = null, commit = null, onchainCommit = null, readError = null } = {}) {
|
|
391
|
+
const who = what || (role ? "the " + role + " point" : "the point");
|
|
392
|
+
if (readError) {
|
|
393
|
+
return { ok: false, checked: false, blocks: true, kind: "points-unchecked",
|
|
394
|
+
reason: `${who} of the live escrow could not be read (${String(readError)}): whether the order holds the points that DLEQ proved is unknown - signing is blocked` };
|
|
395
|
+
}
|
|
396
|
+
// ВЕТКА ВЫБИРАЕТСЯ ТЕМ, ЧТО ПРОСЯТ СВЕРИТЬ. Порядок проверок здесь - не косметика: если спрашивают про
|
|
397
|
+
// обязательство, а проверять сначала точку, то на отсутствии ТОЧКИ вердикт скажет "точки не прочитаны",
|
|
398
|
+
// и настоящая причина (обязательство не то) останется неназванной - это уже было поймано зубом.
|
|
399
|
+
let compared = false;
|
|
400
|
+
if (normHex(point)) {
|
|
401
|
+
compared = true;
|
|
402
|
+
if (!normHex(onchainPoint)) {
|
|
403
|
+
return { ok: false, checked: false, blocks: true, kind: "points-unchecked",
|
|
404
|
+
reason: `the live escrow did not return ${who}: the order may hold different points than the proof proved - signing is blocked` };
|
|
405
|
+
}
|
|
406
|
+
if (normHex(point) !== normHex(onchainPoint)) {
|
|
407
|
+
return { ok: false, checked: true, blocks: true, kind: "point-mismatch",
|
|
408
|
+
reason: `${who} in the escrow (0x${normHex(onchainPoint)}) is NOT the point DLEQ proved (0x${normHex(point)}): the Monero address would be assembled from other halves and the XMR could not be swept after claim` };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// ОБА ПОЛЯ СВЕРЯЮТСЯ НЕЗАВИСИМО. Ветка "иначе" здесь была ошибкой: при совпавшей точке расхождение
|
|
412
|
+
// обязательства просто не проверялось, и подмена половины проходила мимо вердикта. Поймано зубом.
|
|
413
|
+
if (normHex(commit)) {
|
|
414
|
+
compared = true;
|
|
415
|
+
if (!normHex(onchainCommit)) {
|
|
416
|
+
return { ok: false, checked: false, blocks: true, kind: "points-unchecked",
|
|
417
|
+
reason: `the live escrow did not return ${who}: the order may hold a different half than we do - signing is blocked` };
|
|
418
|
+
}
|
|
419
|
+
if (normHex(commit) !== normHex(onchainCommit)) {
|
|
420
|
+
return { ok: false, checked: true, blocks: true, kind: "commit-mismatch",
|
|
421
|
+
reason: `${who} in the escrow (0x${normHex(onchainCommit)}) is NOT the one we hold (0x${normHex(commit)}): the reveal would not match the order` };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!compared) {
|
|
425
|
+
return { ok: false, checked: false, blocks: true, kind: "points-unchecked",
|
|
426
|
+
reason: `nothing to compare for ${who}: neither a point nor a commitment was recorded - signing is blocked` };
|
|
427
|
+
}
|
|
428
|
+
return { ok: true, checked: true, blocks: false, kind: "points-match",
|
|
429
|
+
reason: `${who} in the escrow matches what we hold${normHex(point) && normHex(commit) ? " (point and commitment)" : ""}` };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// СЛОТЫ ЖИВОГО ОРДЕРА ПРОТИВ СОБРАННОГО СТРАНИЦЕЙ (блок C аудита). Один вопрос, четыре ответа: точки обеих
|
|
433
|
+
// сторон и обязательства на обе половины. Сверяется КАЖДОЕ поле, которое у нас есть, и отсутствие поля у
|
|
434
|
+
// цепи запирает так же, как несовпадение. Собрано из того же примитива, что и одиночная сверка точки
|
|
435
|
+
// (escrowPointsVerdict), чтобы формулировки отказов и правила сравнения были одни на всех.
|
|
436
|
+
export function liveOrderSlotsVerdict({ slots = null, expect = null, readError = null } = {}) {
|
|
437
|
+
if (readError || !slots) {
|
|
438
|
+
return { ok: false, checked: false, blocks: true, kind: "slots-unchecked",
|
|
439
|
+
reason: `the live escrow slots could not be read (${String(readError || "no answer")}): whether the order holds the points and halves we assembled is unknown - signing is blocked` };
|
|
440
|
+
}
|
|
441
|
+
const fields = [
|
|
442
|
+
["edPointLocker", "the locker spend point", "point"],
|
|
443
|
+
["edPointClaimer", "the claimer spend point", "point"],
|
|
444
|
+
["edViewPointLocker", "the locker view point", "point"],
|
|
445
|
+
["commitHalfLocker", "the commitment of the locker half", "commit"],
|
|
446
|
+
["commitHalfClaimer", "the commitment of the claimer half", "commit"],
|
|
447
|
+
];
|
|
448
|
+
const checked = [];
|
|
449
|
+
const missing = [];
|
|
450
|
+
for (const [key, what, kind] of fields) {
|
|
451
|
+
if (!normHex(expect && expect[key])) continue; // чего у нас нет, того и не сверяем - и это названо ниже
|
|
452
|
+
if (!normHex(slots[key])) { missing.push(what); continue; }
|
|
453
|
+
const v = kind === "point"
|
|
454
|
+
? escrowPointsVerdict({ what, point: expect[key], onchainPoint: slots[key] })
|
|
455
|
+
: escrowPointsVerdict({ what, commit: expect[key], onchainCommit: slots[key] });
|
|
456
|
+
if (v.blocks) return { ...v, fields: checked.concat([key]) };
|
|
457
|
+
checked.push(key);
|
|
458
|
+
}
|
|
459
|
+
if (!checked.length) {
|
|
460
|
+
return { ok: false, checked: false, blocks: true, kind: "slots-unchecked",
|
|
461
|
+
reason: "nothing to compare: the app recorded neither a point nor a commitment for this order - signing is blocked" };
|
|
462
|
+
}
|
|
463
|
+
if (missing.length) {
|
|
464
|
+
return { ok: false, checked: true, blocks: true, kind: "slots-unchecked",
|
|
465
|
+
reason: `the live escrow did not return ${missing.join(", ")}: the order may differ from what was proved - signing is blocked` };
|
|
466
|
+
}
|
|
467
|
+
return { ok: true, checked: true, blocks: false, kind: "slots-match", fields: checked,
|
|
468
|
+
reason: `the live escrow holds exactly what we assembled: ${checked.join(", ")}` };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function signingGateVerdict(checks = []) {
|
|
472
|
+
const list = (Array.isArray(checks) ? checks : []).filter((c) => c && typeof c === "object");
|
|
473
|
+
const blocking = list.filter((c) => c.blocks === true);
|
|
474
|
+
const unchecked = list.filter((c) => c.blocks !== true && c.checked === false);
|
|
475
|
+
return {
|
|
476
|
+
ok: blocking.length === 0,
|
|
477
|
+
blocked: blocking.length > 0,
|
|
478
|
+
kind: blocking.length ? blocking[0].kind : (list.length ? list[list.length - 1].kind : "nothing-to-check"),
|
|
479
|
+
reason: blocking.length ? blocking.map((c) => c.reason).filter(Boolean).join("; ") : null,
|
|
480
|
+
blockedBy: blocking.map((c) => c.kind),
|
|
481
|
+
unchecked: unchecked.map((c) => c.kind),
|
|
482
|
+
// САМО ПРОВЕРЕННОЕ ВОЗВРАЩАЕТСЯ ЦЕЛИКОМ: экран и отчёт обязаны называть не только вердикт, но и
|
|
483
|
+
// из чего он взят, а числа там уже посчитаны.
|
|
484
|
+
checks: list,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Слова для кнопки и подписи под ней. Одна фраза на вердикт - чтобы экран и отчёт говорили одно и то
|
|
489
|
+
// же: разойдясь, они начнут объяснять человеку одно, а оператору другое.
|
|
490
|
+
export function signingGateWords(gate) {
|
|
491
|
+
if (!gate || !gate.blocked) return null;
|
|
492
|
+
const first = (gate.checks || []).find((c) => c.blocks === true) || null;
|
|
493
|
+
const why = first && first.reason ? first.reason : "the order cannot be checked";
|
|
494
|
+
return "Signing is blocked: " + why + " (" + gate.kind + ")";
|
|
495
|
+
}
|