@ninsei-labs/sdk 0.39.3 → 0.40.0-develop.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.
- package/CHANGELOG.md +35 -0
- package/engine/atomic/dleq2.js +86 -111
- package/engine/atomic/halfenc.js +58 -72
- package/engine/atomic/halves.js +31 -53
- package/engine/atomic/order-client.js +15 -16
- package/engine/atomic/order.js +44 -62
- package/engine/atomic/orderContext.js +26 -50
- package/engine/atomic/orderHalfCommit.js +17 -18
- package/engine/core/chainSource.js +54 -69
- package/engine/core/clock.js +11 -12
- package/engine/core/config.js +554 -449
- package/engine/core/finality.js +34 -53
- package/engine/core/format.js +35 -36
- package/engine/core/preSignGuards.js +109 -117
- package/engine/core/rfqSource.js +57 -75
- package/engine/core/sellWindow.js +23 -24
- package/engine/core/store.js +20 -21
- package/engine/core/swap.js +242 -243
- package/engine/evm/amounts.js +50 -51
- package/engine/evm/asyncExec.js +577 -0
- package/engine/evm/asyncLegs.js +308 -0
- package/engine/evm/asyncRoute.js +179 -0
- package/engine/evm/auth.js +41 -48
- package/engine/evm/claimGas.js +28 -47
- package/engine/evm/depositor.js +39 -43
- package/engine/evm/dex.js +266 -267
- package/engine/evm/escrow.js +113 -129
- package/engine/evm/factory.js +71 -77
- package/engine/evm/fees.js +41 -42
- package/engine/evm/funding.js +160 -162
- package/engine/evm/gasReserve.js +67 -36
- package/engine/evm/index.js +46 -53
- package/engine/evm/injected.js +34 -42
- package/engine/evm/permit.js +129 -56
- package/engine/evm/prices.js +30 -45
- package/engine/evm/session.js +188 -189
- package/engine/mock/chain.js +17 -18
- package/engine/monero/address.js +33 -48
- package/engine/monero/halvesSweep.js +25 -43
- package/engine/monero/node.js +14 -15
- package/engine/monero/scan.js +82 -95
- package/engine/monero/swapKeys.js +30 -33
- package/engine/monero/unlock.js +17 -20
- package/engine/monero/wallet.js +32 -42
- package/engine/recovery/recoveryFile.js +154 -161
- package/engine/recovery/restore.js +50 -61
- package/engine/sdk/bridge.js +244 -234
- package/engine/ui/dom.js +4 -5
- package/engine/ui/walletModal.js +20 -31
- package/index.d.ts +8 -0
- package/package.json +1 -1
- package/src/adapters/evm-wallet.mjs +16 -0
- package/src/adapters/monero-wallet.mjs +1 -1
- package/src/adapters/monero.mjs +1 -1
- package/src/adapters/node-test-evm-wallet.mjs +5 -5
- package/src/config.mjs +1 -1
- package/src/engine.mjs +4 -0
- package/src/legs/cow-spec.mjs +254 -0
- package/src/legs/cow.mjs +315 -0
- package/src/legs/evm.mjs +86 -2
- package/src/legs/kyber-spec.mjs +265 -0
- package/src/legs/kyber.mjs +425 -0
- package/src/legs/shape.mjs +35 -0
- package/src/legs/uniswaprelay-spec.mjs +541 -0
- package/src/legs/uniswaprelay.mjs +422 -0
- package/src/legs/uniswapx-spec.mjs +329 -0
- package/src/legs/uniswapx.mjs +362 -0
- package/src/quotes.mjs +1 -1
- package/src/swaps.mjs +1 -1
package/engine/monero/scan.js
CHANGED
|
@@ -1,55 +1,50 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/monero/scan.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
5
|
+
// MONERO INDEXER: we find arrivals at the swap address ourselves by parsing blocks.
|
|
6
|
+
// WHY. Before, arrivals were learned from monero-wallet-rpc through a watch-only wallet, a path that hit a defect
|
|
7
|
+
// (the wallet+keys pair is created and never reopens) and where one instance held one wallet, so scanning 25
|
|
8
|
+
// swaps took 140 s. Our own block parsing depends on neither.
|
|
9
|
+
// WHAT IS NEEDED: the VIEW KEY and the PUBLIC SPEND KEY - both on the client and already sent to the backend by
|
|
10
|
+
// an allowlist. The spend key is NOT needed: seeing arrivals and spending them are different rights, the
|
|
11
|
+
// non-custodial boundary.
|
|
12
|
+
// FORMULAS FROM THE MONERO SOURCES, not from memory:
|
|
13
|
+
// derivation = 8 * (view_secret * tx_public_key)
|
|
14
|
+
// scalar_i = Hs(derivation || varint(i))
|
|
15
|
+
// derived_key_i = scalar_i * G + spend_public_key
|
|
16
|
+
// amount_i = ecdhInfo[i].amount XOR Hs("amount" || derivation)[0..8] (RingCT v2)
|
|
17
|
+
// amount_i = ecdhInfo[i].amount - Hs(derivation) (RingCT v1)
|
|
18
|
+
// Both versions are implemented: old transactions in the chain are still there.
|
|
19
|
+
// keccak-256 from @noble/hashes, the same package as the SDK core: the standalone keccak256 npm package needs Buffer.
|
|
20
|
+
// THE unlock_time RULE IS THE SAME ONE, lives next door (./unlock.js) and is re-exported from here.
|
|
7
21
|
//
|
|
8
|
-
// ЗАЧЕМ. До этого поступления узнавались у monero-wallet-rpc через watch-only кошелёк
|
|
9
|
-
// (generate_from_keys). Этот путь упёрся в дефект: пара «кошелёк + ключи» создаётся и больше не
|
|
10
|
-
// открывается (.hermes/docs/19), а кроме того один экземпляр держит один кошелёк и обход 25 свопов
|
|
11
|
-
// занимал 140 с. Свой разбор блоков не зависит ни от сборки кошелька, ни от числа свопов.
|
|
12
22
|
//
|
|
13
|
-
// ЧТО НУЖНО И ЧЕГО НЕ НУЖНО. Нужен КЛЮЧ ПРОСМОТРА и ПУБЛИЧНЫЙ КЛЮЧ ТРАТЫ - оба есть у клиента и оба уже
|
|
14
|
-
// уходят на бэкенд по белому списку полей. Ключ траты НЕ нужен: смотреть поступления и распоряжаться
|
|
15
|
-
// ими - разные права, и это ровно та граница, на которой стоит некастодиальность.
|
|
16
23
|
//
|
|
17
|
-
// ФОРМУЛЫ - ИЗ ИСХОДНИКОВ MONERO, не по памяти (src/crypto/crypto.cpp, src/ringct/rctOps.cpp):
|
|
18
|
-
// derivation = 8 * (view_secret * tx_public_key) - точка, 32 байта (generate_key_derivation)
|
|
19
|
-
// scalar_i = Hs(derivation || varint(i)) - хеш в скаляр (derivation_to_scalar)
|
|
20
|
-
// derived_key_i = scalar_i * G + spend_public_key - (derive_public_key); совпал с выводом - наш
|
|
21
|
-
// amount_i = ecdhInfo[i].amount XOR Hs("amount" || derivation)[0..8] - RingCT v2 (текущий)
|
|
22
|
-
// amount_i = ecdhInfo[i].amount - Hs(derivation) - RingCT v1 (старое)
|
|
23
|
-
// Здесь реализованы обе версии: старые транзакции в цепочке никуда не делись.
|
|
24
24
|
|
|
25
25
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
26
|
-
// keccak-256 - из @noble/hashes, того же пакета, что у ядра SDK: собственный npm-пакет keccak256 требует
|
|
27
|
-
// Buffer и в браузер не входит. keccak_256 даёт тот же результат (проверено на известном значении).
|
|
28
26
|
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
29
|
-
// ПРАВИЛО unlock_time - ОДНО И ТО ЖЕ, живёт рядом (./unlock.js) и реэкспортируется отсюда, чтобы все, кто
|
|
30
|
-
// ходил в app/indexer.mjs, получали те же имена.
|
|
31
27
|
import { UNLOCK_TIME_BLOCK_MAX, unlockStateOf, laterBoundary } from "./unlock.js";
|
|
32
28
|
export { UNLOCK_TIME_BLOCK_MAX, unlockStateOf, laterBoundary };
|
|
33
29
|
|
|
34
30
|
export function keccak(bytes) { return keccak_256(bytes); }
|
|
35
31
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
32
|
+
// Order of the edwards25519 group (l). Taken from the curve, not a remembered number: a wrong constant would
|
|
33
|
+
// corrupt the scalar, the derivation and the sum - an "invisible arrival".
|
|
38
34
|
export const ED_ORDER = ed25519.Point.CURVE().n;
|
|
39
35
|
|
|
40
36
|
export function hexToBytes(hex) {
|
|
41
37
|
const t = String(hex).trim().replace(/^0x/, "");
|
|
42
|
-
if (t.length % 2) throw new Error("
|
|
38
|
+
if (t.length % 2) throw new Error("odd hex length");
|
|
43
39
|
const out = new Uint8Array(t.length / 2);
|
|
44
40
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(t.substr(i * 2, 2), 16);
|
|
45
41
|
return out;
|
|
46
42
|
}
|
|
47
43
|
export function bytesToHex(b) { return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); }
|
|
48
44
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// уже один раз потеряли вечер.
|
|
45
|
+
// BYTE ORDER IS LITTLE-ENDIAN. Monero stores scalars least-significant first, and sc_reduce32 reads the 32 hash
|
|
46
|
+
// bytes that way. Reading them big-endian gives a DIFFERENT number, the derivation does not match, and the
|
|
47
|
+
// arrival is not found SILENTLY.
|
|
53
48
|
export function scalarFromBytesLE(bytes) {
|
|
54
49
|
const rev = Uint8Array.from(bytes).reverse();
|
|
55
50
|
return BigInt("0x" + bytesToHex(rev));
|
|
@@ -62,7 +57,7 @@ export function scalarToBytesLE(v, n = 32) {
|
|
|
62
57
|
return out;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
//
|
|
60
|
+
// Hash to scalar: Hs(...) in Monero = keccak256 read LEAST-SIGNIFICANT first, reduced mod l.
|
|
66
61
|
export function hashToScalar(...parts) {
|
|
67
62
|
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
68
63
|
const buf = new Uint8Array(total);
|
|
@@ -71,7 +66,7 @@ export function hashToScalar(...parts) {
|
|
|
71
66
|
return scalarFromBytesLE(keccak(buf)) % ED_ORDER;
|
|
72
67
|
}
|
|
73
68
|
|
|
74
|
-
//
|
|
69
|
+
// Monero varint (LEB128, 7 bits per byte, high bit = continuation).
|
|
75
70
|
export function varint(n) {
|
|
76
71
|
const out = [];
|
|
77
72
|
let v = BigInt(n);
|
|
@@ -80,29 +75,27 @@ export function varint(n) {
|
|
|
80
75
|
return new Uint8Array(out);
|
|
81
76
|
}
|
|
82
77
|
|
|
83
|
-
//
|
|
78
|
+
// The swap public key is checked on parse: the point must lie on the curve.
|
|
84
79
|
function pointFromHex(hex) { return ed25519.Point.fromHex(String(hex).replace(/^0x/, "")); }
|
|
85
80
|
|
|
86
|
-
// derivation = 8 * (view_secret * tx_public_key) -
|
|
81
|
+
// derivation = 8 * (view_secret * tx_public_key) - exactly what generate_key_derivation does.
|
|
87
82
|
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// честно докладывал ноль. Поэтому вид перебирается ЯВНО, а победивший называется в результате.
|
|
83
|
+
// BYTE ORDER: HERE WAS A SILENT LOSS OF ARRIVALS. The view key comes in two forms that look identical as a
|
|
84
|
+
// string: from monero-rpc as LITTLE-ENDIAN BYTES, from the swap record as a NUMBER in hex. Reading one as the
|
|
85
|
+
// other gives another scalar: the derivation does not match, the arrival is not found, and no error is written.
|
|
86
|
+
// So the form is tried EXPLICITLY and the winner is named in the result.
|
|
93
87
|
export function viewSecretCandidates(viewSecret) {
|
|
94
|
-
if (typeof viewSecret === "bigint") return [["
|
|
88
|
+
if (typeof viewSecret === "bigint") return [["number", viewSecret % ED_ORDER]];
|
|
95
89
|
const hex = String(viewSecret).replace(/^0x/, "");
|
|
96
|
-
if (!/^[0-9a-fA-F]{64}$/.test(hex)) return [["
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
return [["BE
|
|
90
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hex)) return [["unparsed", null]];
|
|
91
|
+
// Both forms are reduced mod the group order: without this, a value larger than the order (any garbage string)
|
|
92
|
+
// cannot produce a point and the key check would crash instead of saying "no match".
|
|
93
|
+
return [["BE-number", BigInt("0x" + hex) % ED_ORDER], ["LE-bytes", scalarFromHexLE(hex) % ED_ORDER]];
|
|
100
94
|
}
|
|
101
95
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// null - сверять не с чем.
|
|
96
|
+
// WHETHER THE VIEW KEY MATCHES THE ADDRESS. One comparison: the key point must equal the view point in the
|
|
97
|
+
// address. Return: the form name that matched; false - no form matched, so the key is from another address;
|
|
98
|
+
// null - nothing to compare.
|
|
106
99
|
export function viewKeyFormForAddress(viewSecret, expectedViewPubHex) {
|
|
107
100
|
const want = String(expectedViewPubHex || "").toLowerCase();
|
|
108
101
|
if (!want) return null;
|
|
@@ -115,12 +108,12 @@ export function viewKeyFormForAddress(viewSecret, expectedViewPubHex) {
|
|
|
115
108
|
|
|
116
109
|
export function keyDerivation(txPublicKeyHex, viewSecretHex) {
|
|
117
110
|
const txPub = pointFromHex(txPublicKeyHex);
|
|
118
|
-
//
|
|
111
|
+
// The view key is accepted both as LE bytes from monero-rpc and as a number if already parsed.
|
|
119
112
|
const a = (typeof viewSecretHex === "bigint" ? viewSecretHex : scalarFromHexLE(String(viewSecretHex).replace(/^0x/, ""))) % ED_ORDER;
|
|
120
113
|
const shared = txPub.multiply(a);
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
return hexToBytes(shared.multiply(8n).toHex()); // 32
|
|
114
|
+
// IMPORTANT: return BYTES, not a hex string. A string would later travel as the string's "bytes" (ASCII codes
|
|
115
|
+
// into the hash) and the arrival simply would not be found - silently.
|
|
116
|
+
return hexToBytes(shared.multiply(8n).toHex()); // 32-byte point (ge_tobytes) - this is the derivation
|
|
124
117
|
}
|
|
125
118
|
|
|
126
119
|
// derived_key_i = Hs(derivation || varint(i)) * G + B
|
|
@@ -130,9 +123,8 @@ export function derivedOutputKey(derivation, outputIndex, spendPubHex) {
|
|
|
130
123
|
return point.toHex();
|
|
131
124
|
}
|
|
132
125
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// extra приходит из ноды МАССИВОМ БАЙТ (не hex-строкой), поэтому принимаем оба вида.
|
|
126
|
+
// Helper: the tx public key sits in the transaction extra, tag 0x01, 32 bytes. We read extra as is - the same
|
|
127
|
+
// parse the wallet does. extra arrives from the node as a BYTE ARRAY (not hex), so both forms are accepted.
|
|
136
128
|
export function txPublicKeyFromExtra(extra) {
|
|
137
129
|
const b = Array.isArray(extra) ? Uint8Array.from(extra) : hexToBytes(extra);
|
|
138
130
|
let i = 0;
|
|
@@ -140,29 +132,28 @@ export function txPublicKeyFromExtra(extra) {
|
|
|
140
132
|
const tag = b[i];
|
|
141
133
|
if (tag === 0x01) return bytesToHex(b.subarray(i + 1, i + 33));
|
|
142
134
|
if (tag === 0x02) { i += 1 + b[i + 1]; continue; } // nonce
|
|
143
|
-
if (tag === 0x00) { i += 1 + b[i + 1]; continue; } //
|
|
144
|
-
return null; //
|
|
135
|
+
if (tag === 0x00) { i += 1 + b[i + 1]; continue; } // additional public key
|
|
136
|
+
return null; // unknown tag: we do not guess
|
|
145
137
|
}
|
|
146
138
|
return null;
|
|
147
139
|
}
|
|
148
140
|
|
|
149
|
-
//
|
|
141
|
+
// Amount decryption. version: 2 = current scheme (xor of the first 8 bytes), 1 = old (scalar subtraction).
|
|
150
142
|
//
|
|
151
|
-
//
|
|
152
|
-
// genAmountEncodingFactor
|
|
153
|
-
//
|
|
154
|
-
// числах: правильный ответ 1000000000 atomic, с derivation выходило 8830061611517333961.
|
|
143
|
+
// THE SECRET IS THE DERIVATION SCALAR, NOT THE DERIVATION. Easy to get wrong: both are 32 bytes, but what goes
|
|
144
|
+
// into genAmountEncodingFactor is the scalar (derivation_to_scalar), written LEAST-SIGNIFICANT first. With the
|
|
145
|
+
// derivation the result looks plausible and is WRONG (checked on live numbers).
|
|
155
146
|
export function decodeAmount(ecdhAmountHex, scalarLE, rctVersion) {
|
|
156
147
|
const enc = hexToBytes(ecdhAmountHex);
|
|
157
148
|
if (Number(rctVersion) >= 2) {
|
|
158
|
-
//
|
|
149
|
+
// Multiplier: keccak("amount" || scalar_LE), 38 bytes - order checked against the source.
|
|
159
150
|
const label = new TextEncoder().encode("amount");
|
|
160
151
|
const buf = new Uint8Array(label.length + scalarLE.length);
|
|
161
152
|
buf.set(label, 0); buf.set(scalarLE, label.length);
|
|
162
153
|
const factor = keccak(buf);
|
|
163
154
|
const out = enc.slice();
|
|
164
|
-
for (let i = 0; i < 8; i++) out[i] ^= factor[i]; // xor
|
|
165
|
-
//
|
|
155
|
+
for (let i = 0; i < 8; i++) out[i] ^= factor[i]; // xor of the FIRST 8 bytes only
|
|
156
|
+
// And again LEAST-SIGNIFICANT first: Monero amounts are little-endian too.
|
|
166
157
|
return scalarFromBytesLE(out);
|
|
167
158
|
}
|
|
168
159
|
const s2 = hashToScalar(hexToBytes(hashToScalar(hexToBytes(bytesToHex(rctVersion === 1 ? scalarLE : scalarLE))).toString(16).padStart(64, "0")));
|
|
@@ -170,15 +161,14 @@ export function decodeAmount(ecdhAmountHex, scalarLE, rctVersion) {
|
|
|
170
161
|
return (amt - s2 + ED_ORDER) % ED_ORDER;
|
|
171
162
|
}
|
|
172
163
|
|
|
173
|
-
//
|
|
164
|
+
// MAIN: parse a transaction and return OUR outputs (their indices, keys and amounts).
|
|
174
165
|
// txView - { txPublicKey, vout: [{key, index}], ecdhAmounts: [...], rctVersion }
|
|
175
166
|
export function findOurOutputs({ txPublicKey, viewSecretHex, spendPubHex, outputs, ecdhAmounts = [], rctVersion = 2 }) {
|
|
176
|
-
if (!txPublicKey) throw new Error("
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// осмысленный вид, а не пустоту: иначе в журнале будет «ничем», и причина снова спрячется.
|
|
167
|
+
if (!txPublicKey) throw new Error("the transaction has no public key (extra tag 0x01) - nothing to parse");
|
|
168
|
+
// The key form is tried EXPLICITLY; the winner's name goes up so the log shows what was used. If none matched,
|
|
169
|
+
// the first meaningful form is returned, not nothing.
|
|
180
170
|
const usable = viewSecretCandidates(viewSecretHex).filter(([, v]) => v !== null);
|
|
181
|
-
const first = usable[0] || ["
|
|
171
|
+
const first = usable[0] || ["unparsed", null];
|
|
182
172
|
for (const [form, secret] of usable) {
|
|
183
173
|
const derivation = keyDerivation(txPublicKey, secret);
|
|
184
174
|
const found = [];
|
|
@@ -186,7 +176,7 @@ export function findOurOutputs({ txPublicKey, viewSecretHex, spendPubHex, output
|
|
|
186
176
|
const scalar = hashToScalar(derivation, varint(o.index));
|
|
187
177
|
const derived = derivedOutputKey(derivation, o.index, spendPubHex);
|
|
188
178
|
if (derived.toLowerCase() === String(o.key).toLowerCase()) {
|
|
189
|
-
//
|
|
179
|
+
// The amount decryption uses THIS output's SCALAR, least-significant first.
|
|
190
180
|
const amount = ecdhAmounts[o.index] ? decodeAmount(ecdhAmounts[o.index], scalarToBytesLE(scalar), rctVersion) : null;
|
|
191
181
|
found.push({ index: o.index, key: o.key, amountAtomic: amount });
|
|
192
182
|
}
|
|
@@ -201,13 +191,12 @@ export function findOurOutputs({ txPublicKey, viewSecretHex, spendPubHex, output
|
|
|
201
191
|
}
|
|
202
192
|
|
|
203
193
|
// ============================================================================================
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// зависит от числа свопов: блок читается один раз для всех.
|
|
194
|
+
// SCANNING A HEIGHT RANGE. This is the indexing itself: read blocks in order, take their transaction hashes
|
|
195
|
+
// and parse them with ONE request per block, then check each transaction's outputs against ALL watched
|
|
196
|
+
// addresses at once. That is why the scan does not depend on the number of swaps.
|
|
208
197
|
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
198
|
+
// Deliberately NOT here: truncation, "height-based" confirmations, writing state to files. The height is a call
|
|
199
|
+
// parameter; the caller owns the scan state.
|
|
211
200
|
// ============================================================================================
|
|
212
201
|
|
|
213
202
|
|
|
@@ -217,12 +206,12 @@ export async function fetchBlock(daemonUrl, height) {
|
|
|
217
206
|
body: JSON.stringify({ jsonrpc: "2.0", id: "block", method: "get_block", params: { height } }),
|
|
218
207
|
});
|
|
219
208
|
const j = await res.json();
|
|
220
|
-
if (!j.result) throw new Error(
|
|
209
|
+
if (!j.result) throw new Error(`block ${height} not received: ${j.error ? j.error.message : "empty answer"}`);
|
|
221
210
|
return j.result;
|
|
222
211
|
}
|
|
223
212
|
|
|
224
|
-
//
|
|
225
|
-
//
|
|
213
|
+
// Transactions by hash list. We hit the PATH /get_transactions, not /json_rpc: over JSON-RPC this method
|
|
214
|
+
// silently returns empty, which looks like "no transactions".
|
|
226
215
|
export async function fetchTransactions(daemonUrl, hashes) {
|
|
227
216
|
if (!hashes.length) return [];
|
|
228
217
|
const res = await fetch(`${String(daemonUrl).replace(/\/+$/, "")}/get_transactions`, {
|
|
@@ -231,9 +220,8 @@ export async function fetchTransactions(daemonUrl, hashes) {
|
|
|
231
220
|
});
|
|
232
221
|
const j = await res.json();
|
|
233
222
|
const wrappers = j.txs || [];
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
// этом всё выглядит успешным.
|
|
223
|
+
// HASH AND HEIGHT LIVE IN THE WRAPPER, not in the parsed body: as_json omits them. Without this stitching a
|
|
224
|
+
// "found transfer" would have neither hash nor height - useless to the caller, yet everything would look successful.
|
|
237
225
|
return (j.txs_as_json || []).map((s, i) => {
|
|
238
226
|
const body = typeof s === "string" ? JSON.parse(s) : s;
|
|
239
227
|
const w = wrappers[i] || {};
|
|
@@ -245,15 +233,14 @@ export async function fetchTransactions(daemonUrl, hashes) {
|
|
|
245
233
|
});
|
|
246
234
|
}
|
|
247
235
|
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
// (см. viewSecretCandidates). В результате едет viewKeyForm: имя вида, которым вывод сошёлся.
|
|
236
|
+
// Parsing one transaction against the watched addresses. watched: [{ address, viewSecret, spendPub }] - viewSecret
|
|
237
|
+
// is taken as a number, LE-hex or BE-hex. The result carries viewKeyForm: the form name that matched.
|
|
251
238
|
export function matchTransaction(tx, watched, { height = null, nowSec = null } = {}) {
|
|
252
239
|
const out = [];
|
|
253
240
|
const txPublicKey = txPublicKeyFromExtra(tx.extra);
|
|
254
|
-
if (!txPublicKey) return out; //
|
|
255
|
-
//
|
|
256
|
-
//
|
|
241
|
+
if (!txPublicKey) return out; // no key in extra - nothing to look at
|
|
242
|
+
// WHETHER THE OUTPUT IS LOCKED IS DECIDED HERE, ONCE PER TRANSACTION: all its outputs carry one unlock_time.
|
|
243
|
+
// A normal transaction (unlock_time = 0) goes as before; the height comes from the block holding the transfer.
|
|
257
244
|
const blockHeight = height === null || height === undefined ? (tx.block_height ?? null) : height;
|
|
258
245
|
const unlock = unlockStateOf(tx.unlock_time, { height: blockHeight, nowSec });
|
|
259
246
|
const keyOf = (o) => ((o.target || {}).tagged_key || o.target || {}).key;
|
|
@@ -270,17 +257,17 @@ export function matchTransaction(tx, watched, { height = null, nowSec = null } =
|
|
|
270
257
|
return out;
|
|
271
258
|
}
|
|
272
259
|
|
|
273
|
-
//
|
|
274
|
-
//
|
|
260
|
+
// Range scan. Returns per address the sum (receivedAtomic - only the UNLOCKED), hashes, the locked sum
|
|
261
|
+
// (lockedAtomic) with its boundary (lockedUntil) and the best (highest) height of a found output.
|
|
275
262
|
export async function scanRange({ daemonUrl, fromHeight, toHeight, watched, log = () => {}, concurrency = 4, nowSec = null }) {
|
|
276
263
|
const from = Number(fromHeight), to = Number(toHeight);
|
|
277
|
-
if (!(from >= 0) || !(to >= from)) throw new Error("
|
|
264
|
+
if (!(from >= 0) || !(to >= from)) throw new Error("bad height range: " + fromHeight + ".." + toHeight);
|
|
278
265
|
const acc = new Map();
|
|
279
266
|
for (const w of watched) acc.set(w.address, { address: w.address, receivedAtomic: 0n, lockedAtomic: 0n, lockedUntil: null, lockedTxids: [], txids: [], bestHeight: null, outputs: 0, viewKeyForm: null });
|
|
280
267
|
const heights = [];
|
|
281
268
|
for (let h = from; h <= to; h++) heights.push(h);
|
|
282
269
|
|
|
283
|
-
//
|
|
270
|
+
// Bounded parallelism: the node is a shared resource and blocks are independent.
|
|
284
271
|
let cursor = 0;
|
|
285
272
|
const worker = async () => {
|
|
286
273
|
while (cursor < heights.length) {
|
|
@@ -294,13 +281,13 @@ export async function scanRange({ daemonUrl, fromHeight, toHeight, watched, log
|
|
|
294
281
|
for (const hit of matchTransaction(tx, watched, { height: h, nowSec })) {
|
|
295
282
|
const a = acc.get(hit.address);
|
|
296
283
|
if (!a) continue;
|
|
297
|
-
if (hit.viewKeyForm) a.viewKeyForm = hit.viewKeyForm; //
|
|
284
|
+
if (hit.viewKeyForm) a.viewKeyForm = hit.viewKeyForm; // what matched - this goes to the log
|
|
298
285
|
const lockedHere = !!(hit.unlock && hit.unlock.locked);
|
|
299
286
|
const hash = String(tx.tx_hash || "").replace(/^0x/, "");
|
|
300
287
|
for (const o of hit.outputs) {
|
|
301
288
|
const amount = o.amountAtomic || 0n;
|
|
302
|
-
//
|
|
303
|
-
//
|
|
289
|
+
// A LOCKED OUTPUT IS NOT AN ARRIVAL. Its transfer shows in txids (money is on the address) but the
|
|
290
|
+
// sum does NOT go into receivedAtomic: else 'ready' would allow claiming ETH against locked XMR.
|
|
304
291
|
if (lockedHere) {
|
|
305
292
|
a.lockedAtomic += amount;
|
|
306
293
|
if (hash && !a.lockedTxids.includes(hash)) a.lockedTxids.push(hash);
|
|
@@ -1,68 +1,65 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/monero/swapKeys.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
5
|
+
// JOINT ADDRESS KEYS FROM HALVES.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// ключа на его стороне. Это кастодиальная схема, и она не часть того, что здесь, а то, от чего мы ушли.
|
|
7
|
+
// THERE IS NO FULL SPEND KEY HERE. Neither side generates the swap wallet whole: each gives ITS half and the
|
|
8
|
+
// address is their SUM. So "take the XMR and hand the ETH back" does not work - the counterparty lacks the other
|
|
9
|
+
// half and cannot spend the output alone. The old combineAddress assembled the address from the maker's seed,
|
|
10
|
+
// i.e. a full key on one side - a custodial scheme we moved away from.
|
|
13
11
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// ARITHMETIC. Monero is edwards25519. The address spend key is the SUM of halves and the public point is the SUM
|
|
13
|
+
// of points, because (a+b)G == aG + bG. That equality is what the split rests on: both sides know the address,
|
|
14
|
+
// only the holder of both halves can spend.
|
|
17
15
|
//
|
|
18
|
-
//
|
|
19
|
-
// 1)
|
|
20
|
-
//
|
|
21
|
-
// 2)
|
|
22
|
-
// До этого его нет ни у кого - иначе схема не работала бы.
|
|
16
|
+
// TWO OPERATIONS, NOT TO BE MERGED:
|
|
17
|
+
// 1) THE ADDRESS is built from PUBLIC data: own half + the other side's POINT. No secret of the counterparty is
|
|
18
|
+
// needed, so it is built when the order is created.
|
|
19
|
+
// 2) THE SPEND KEY (s_a + s_b) appears only AFTER the second half is revealed by settlement.
|
|
23
20
|
|
|
24
|
-
//
|
|
21
|
+
// (1) Address from public data: nothing secret about the counterparty is needed.
|
|
25
22
|
export function swapAddressFromHalves({ halves, addressFromKeys, deps, ownSpendHalf, ownViewHalf, otherSpendPoint, otherViewPoint, network }) {
|
|
26
|
-
if (!halves || !addressFromKeys) throw new Error("
|
|
23
|
+
if (!halves || !addressFromKeys) throw new Error("halves and addressFromKeys are required");
|
|
27
24
|
for (const [name, v] of Object.entries({ ownSpendHalf, ownViewHalf, otherSpendPoint, otherViewPoint })) {
|
|
28
|
-
if (v === undefined || v === null || v === "") throw new Error("
|
|
25
|
+
if (v === undefined || v === null || v === "") throw new Error("not set: " + name);
|
|
29
26
|
}
|
|
30
27
|
const ownSpendPub = halves.publicHalves(ownSpendHalf).ed;
|
|
31
28
|
const ownViewPub = halves.publicHalves(ownViewHalf).ed;
|
|
32
29
|
const spendPub = halves.combinePublic(ownSpendPub, otherSpendPoint);
|
|
33
30
|
const viewPub = halves.combinePublic(ownViewPub, otherViewPoint);
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// deps = { keccak256, Buffer }:
|
|
31
|
+
// The address string uses the same encoder as before: the network comes from the config, so mainnet and
|
|
32
|
+
// stagenet both work. Only the key source changed - halves, not a seed.
|
|
33
|
+
// deps = { keccak256, Buffer }: the encoder computes the checksum itself and must not rely on globals.
|
|
37
34
|
const address = addressFromKeys({ network, kind: "primary", spendPub, viewPub }, deps);
|
|
38
35
|
return { address, spendPub, viewPub, ownSpendPub, ownViewPub };
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
//
|
|
38
|
+
// (2) Spend key after the second half is revealed, WITH A MANDATORY CHECK.
|
|
42
39
|
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
40
|
+
// THE CHECK IS THE POINT. The revealed half may be the wrong one: the contract accepts any half whose hash
|
|
41
|
+
// matches the commitment, so this comparison is the only place the link "revealed half <-> address point" is
|
|
42
|
+
// confirmed. We compare (s_a + s_b)*G with the spend public key from the order: no match - we do NOT sweep and
|
|
43
|
+
// say so aloud.
|
|
47
44
|
export function sweepSpendSecret({ halves, ownSpendHalf, revealedOtherHalf, expectedSpendPub }) {
|
|
48
|
-
if (!halves) throw new Error("
|
|
45
|
+
if (!halves) throw new Error("halves are required");
|
|
49
46
|
for (const [name, v] of Object.entries({ ownSpendHalf, revealedOtherHalf, expectedSpendPub })) {
|
|
50
|
-
if (v === undefined || v === null || v === "") throw new Error("
|
|
47
|
+
if (v === undefined || v === null || v === "") throw new Error("not set: " + name);
|
|
51
48
|
}
|
|
52
49
|
const sum = halves.combineHalves(ownSpendHalf, BigInt(revealedOtherHalf));
|
|
53
50
|
const sumPub = halves.publicHalves(sum).ed;
|
|
54
51
|
const norm = (h) => "0x" + String(h).replace(/^0x/i, "").toLowerCase();
|
|
55
52
|
if (norm(sumPub) !== norm(expectedSpendPub)) {
|
|
56
53
|
throw new Error(
|
|
57
|
-
"
|
|
58
|
-
"
|
|
54
|
+
"the revealed half does not match the address: (s_a + s_b)*G != the order's spend public key. " +
|
|
55
|
+
"Do not sweep - the half is foreign or from another order"
|
|
59
56
|
);
|
|
60
57
|
}
|
|
61
58
|
return { spendSecret: sum, spendPub: sumPub };
|
|
62
59
|
}
|
|
63
60
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
61
|
+
// The view key for watching the arrival: also a sum, but from its own pair of halves. A separate function
|
|
62
|
+
// because watching and spending are different secrets and must not be confused.
|
|
66
63
|
export function watchViewSeedHex({ halves, ownViewHalf, otherViewHalf }) {
|
|
67
64
|
const sum = halves.combineHalves(ownViewHalf, BigInt(otherViewHalf));
|
|
68
65
|
return "0x" + sum.toString(16).padStart(64, "0");
|
package/engine/monero/unlock.js
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/monero/unlock.js for the SDK package.
|
|
2
|
-
// Edit the source under www/js
|
|
3
|
-
// tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
|
|
2
|
+
// Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
|
|
4
3
|
// Everything below this header is identical to the source.
|
|
5
4
|
|
|
6
|
-
//
|
|
5
|
+
// OUTPUT UNLOCK TIME (unlock_time). ONE RULE FOR EVERYONE WHO DECIDES "ARRIVAL IS THERE".
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// *
|
|
11
|
-
// * 500 000 000
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
7
|
+
// A Monero transaction has an unlock_time field. Zero - "the output is open in the normal order". A non-zero
|
|
8
|
+
// value FORBIDS spending the output until the given boundary, and the boundary is of two kinds:
|
|
9
|
+
// * less than 500 000 000 - this is a BLOCK HEIGHT (CRYPTONOTE_MAX_BLOCK_NUMBER in the Monero sources);
|
|
10
|
+
// * 500 000 000 and more - this is a TIME (unix seconds).
|
|
11
|
+
// Until the boundary has passed, the money LIES at the address but cannot be spent: this is NOT an arrival
|
|
12
|
+
// that can close the swap. A maker who sent the correct amount with unlock_time five years ahead received ETH
|
|
13
|
+
// on the "done" mark, while the XMR stayed locked.
|
|
15
14
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// tools/check-sdk-engine.mjs. Вторая копия означала бы вторую правду о деньгах, поэтому копии нет:
|
|
20
|
-
// зеркало обязано совпасть с этим файлом до байта, и расхождение ловит проверка, а не человек.
|
|
15
|
+
// WHY THE ONE SOURCE LIVES HERE. The rule is read by the backend indexer through the scan module and by the
|
|
16
|
+
// SDK core through the engine mirror. A second copy would mean a second truth about money, so there is no copy:
|
|
17
|
+
// the mirror must match this file to the byte, and a divergence between them is caught mechanically, not by a human.
|
|
21
18
|
export const UNLOCK_TIME_BLOCK_MAX = 500_000_000;
|
|
22
19
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
20
|
+
// Output state by its unlock_time. The return is a NAMED result, not a boolean: the interface must
|
|
21
|
+
// explain to the person UP TO WHAT exactly the money is locked (heights or dates), and in what language to
|
|
22
|
+
// say it is the business of the interface dictionary, not of the core.
|
|
26
23
|
export function unlockStateOf(unlockTime, { height = null, nowSec = null } = {}) {
|
|
27
24
|
const u = Number(unlockTime) || 0;
|
|
28
25
|
if (u <= 0) return { code: "unlocked", locked: false, untilHeight: null, untilTime: null };
|
|
@@ -36,8 +33,8 @@ export function unlockStateOf(unlockTime, { height = null, nowSec = null } = {})
|
|
|
36
33
|
return { code: open ? "unlocked" : "locked_height", locked: !open, untilHeight: u, untilTime: null };
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
36
|
+
// How "late" the boundary is: of two locked transfers in the record state we keep the one that opens
|
|
37
|
+
// later - otherwise one of them would show "opening soon" while the second holds the money longer.
|
|
41
38
|
export function laterBoundary(state) {
|
|
42
39
|
if (!state) return -1;
|
|
43
40
|
if (state.untilTime !== null && state.untilTime !== undefined) return Number(state.untilTime);
|