@ninsei-labs/sdk 0.52.0 → 0.53.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 +239 -0
- package/README.md +76 -3
- package/engine/atomic/order-worker.js +29 -2
- package/engine/atomic/order.js +24 -0
- package/engine/core/store.js +60 -12
- package/engine/evm/funding.js +65 -18
- package/engine/monero/arrivalRead.js +3 -22
- package/engine/monero/halvesSweep.js +5 -1
- package/engine/monero/scan.js +144 -33
- package/index.d.ts +35 -21
- package/package.json +23 -3
- package/src/adapters/monero-wallet.mjs +13 -6
- package/src/adapters/monero.mjs +5 -7
- package/src/codes.mjs +4 -0
- package/src/config.mjs +6 -3
- package/src/decline.mjs +6 -1
- package/src/feeEstimate.mjs +20 -13
- package/src/index.mjs +5 -4
- package/src/legs/index.mjs +8 -0
- package/src/legs/profiles.mjs +51 -0
- package/src/lock.mjs +4 -6
- package/src/order.mjs +53 -7
- package/src/public/networks.d.ts +14 -1
- package/src/public/recovery.d.ts +42 -5
- package/src/public/recovery.mjs +5 -0
- package/src/public/xmr.d.ts +26 -8
- package/src/quotes.mjs +165 -2
- package/src/recovery-key.mjs +177 -39
- package/src/recovery.mjs +9 -1
- package/src/swap-flow.mjs +3 -2
- package/src/swaps.mjs +204 -79
- package/src/sweep-fees.mjs +32 -0
- package/src/sweep-restore.mjs +30 -9
- package/src/sweep.mjs +28 -5
- package/src/version.mjs +1 -1
- package/src/xmr-arrival.mjs +76 -32
package/src/version.mjs
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
// tools/sync-version.mjs. Do not edit it by hand - set the version in package.json and run `npm run sync:version`.
|
|
7
7
|
// The `version` check in tools/check.mjs refuses a mismatch, and refuses a runtime read of package.json here.
|
|
8
8
|
|
|
9
|
-
export const SDK_VERSION = "0.
|
|
9
|
+
export const SDK_VERSION = "0.53.0";
|
package/src/xmr-arrival.mjs
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
// decision moves into the client, which reads the chain itself with its own view key.
|
|
8
8
|
//
|
|
9
9
|
// TWO NODES OF DIFFERENT OPERATORS. A single node is a single truth that can lie. So `ready` opens only when at
|
|
10
|
-
// least two nodes belonging to DIFFERENT operators see
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// the
|
|
14
|
-
//
|
|
10
|
+
// least two nodes belonging to DIFFERENT operators see THE WHOLE OBSERVATION THE SAME WAY: the same transaction in
|
|
11
|
+
// the same block, with the same output keys, the same decoded amounts (each backed by its Pedersen commitment) and
|
|
12
|
+
// the same unlock state. A failed or lagging node does not count and blocks nothing; a synced node that does not
|
|
13
|
+
// have the transaction, reports another block, or reports another amount is a DISAGREEMENT - the button stays closed
|
|
14
|
+
// and the screen says the nodes do not agree. When only one operator has confirmed, the core names that state
|
|
15
|
+
// (`one-node`) instead of waiting silently.
|
|
15
16
|
//
|
|
16
17
|
// WHERE THE RULE LIVES. "Locked by unlock_time is not an arrival" is ONE rule (issue #84). It is not copied here:
|
|
17
18
|
// it lives under www/js/monero/unlock.js, is mirrored byte-for-byte into the engine (sdk/engine/monero/unlock.js,
|
|
@@ -108,8 +109,9 @@ async function getBlockTxs(node, hashes, fetchImpl, timeoutMs) {
|
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
// The amount of our own outputs in a matched transaction (a swap can be paid in several outputs).
|
|
112
|
-
|
|
112
|
+
// The amount of our own outputs in a matched transaction (a swap can be paid in several outputs). An output whose
|
|
113
|
+
// amount did not match its Pedersen commitment is NOT money: it contributes zero, and the caller names it.
|
|
114
|
+
const sumOutputs = (hit) => hit.outputs.reduce((n, o) => n + (o.amountVerified ? (o.amountAtomic || 0n) : 0n), 0n);
|
|
113
115
|
|
|
114
116
|
/**
|
|
115
117
|
* FIND THE TRANSACTION, IF IT IS THERE. Blocks are walked from the swap's birth height upwards and the whole block
|
|
@@ -128,7 +130,10 @@ async function locate(node, { txid, address, viewSecret, spendPub, fromHeight, t
|
|
|
128
130
|
for (const tx of txs) {
|
|
129
131
|
if (txid && tx.tx_hash !== txid) continue;
|
|
130
132
|
const hit = engine.xmrScan.matchTransaction(tx, [{ address, viewSecret, spendPub }], { height: h, nowSec });
|
|
131
|
-
|
|
133
|
+
// LOCATE FINDS AN OUTPUT, NOT AN AMOUNT. The amount can be forged or unbacked; the verification step below
|
|
134
|
+
// refuses it. Requiring a positive verified sum here made two nodes that agreed on a forged amount look like
|
|
135
|
+
// "no transaction at all".
|
|
136
|
+
if (hit.length) return { txid: tx.tx_hash, height: h };
|
|
132
137
|
}
|
|
133
138
|
}
|
|
134
139
|
return null;
|
|
@@ -207,22 +212,51 @@ export async function verifyArrival(request) {
|
|
|
207
212
|
try { body = (await getBlockTxs(node, block.txHashes, fetchImpl, timeoutMs)).find((t) => t.tx_hash === located.txid) || null; }
|
|
208
213
|
catch { record.state = "down"; seen.push(record); continue; }
|
|
209
214
|
if (!body) { record.state = "no-tx"; seen.push(record); continue; }
|
|
210
|
-
|
|
215
|
+
// THE LOCK IS JUDGED AT THE WITNESS'S TIP, NOT AT THE BLOCK THAT MINED THE TRANSFER. `located.height` stays for
|
|
216
|
+
// confirmations; a height lock that has matured is no longer `xmr-locked`.
|
|
217
|
+
const hit = engine.xmrScan.matchTransaction(body, [watched], {
|
|
218
|
+
height: located.height,
|
|
219
|
+
tipHeight: Math.max(0, info.height - 1),
|
|
220
|
+
nowSec,
|
|
221
|
+
});
|
|
211
222
|
if (!hit.length) { record.state = "no-output"; seen.push(record); continue; }
|
|
212
223
|
record.state = "ok";
|
|
213
224
|
record.amountAtomic = sumOutputs(hit[0]);
|
|
225
|
+
record.amountVerified = hit[0].outputs.every((o) => o.amountVerified === true);
|
|
226
|
+
record.outputs = hit[0].outputs.map((o) => ({
|
|
227
|
+
index: o.index,
|
|
228
|
+
key: String(o.key || "").replace(/^0x/, "").toLowerCase(),
|
|
229
|
+
amountAtomic: o.amountVerified ? String(o.amountAtomic ?? null) : null,
|
|
230
|
+
amountVerified: o.amountVerified === true,
|
|
231
|
+
}));
|
|
214
232
|
record.unlock = hit[0].unlock;
|
|
215
233
|
// Confirmations of a block: top - height + 1, and top = info.height - 1 (see above), so this is height - located.
|
|
216
234
|
record.confirmations = Number.isFinite(Number(body.confirmations)) ? Number(body.confirmations) : (info.height - located.height);
|
|
235
|
+
// A NODE THAT CHANGED THE AMOUNT IS A DISAGREEMENT, NOT A WITNESS. The commitment check above is what
|
|
236
|
+
// makes this observable: without it an altered ecdhInfo.amount would still look like a valid output.
|
|
237
|
+
if (!record.amountVerified) { record.state = "amount-unverified"; seen.push(record); continue; }
|
|
238
|
+
// THE WHOLE OBSERVATION, NOT A BLOCK HASH. Two nodes must agree on the transaction id, the block, every
|
|
239
|
+
// output key and amount, and the unlock state. A block hash alone let one node change the amount and still
|
|
240
|
+
// form its own group; that is exactly the finding this closes.
|
|
241
|
+
record.observation = {
|
|
242
|
+
txid: located.txid,
|
|
243
|
+
blockHash: String(block.hash || "").replace(/^0x/, "").toLowerCase(),
|
|
244
|
+
outputs: record.outputs,
|
|
245
|
+
unlock: record.unlock ? {
|
|
246
|
+
locked: record.unlock.locked === true,
|
|
247
|
+
untilHeight: record.unlock.untilHeight ?? null,
|
|
248
|
+
untilTime: record.unlock.untilTime ?? null,
|
|
249
|
+
} : null,
|
|
250
|
+
};
|
|
217
251
|
seen.push(record);
|
|
218
252
|
}
|
|
219
253
|
|
|
220
|
-
// A DISAGREEMENT CLOSES THE BUTTON - it is named, and the nodes and
|
|
221
|
-
const disagreeing = seen.filter((r) => r.state === "no-tx" || r.state === "no-output");
|
|
254
|
+
// A DISAGREEMENT CLOSES THE BUTTON - it is named, and the nodes and observations travel in the params.
|
|
255
|
+
const disagreeing = seen.filter((r) => r.state === "no-tx" || r.state === "no-output" || r.state === "amount-unverified");
|
|
222
256
|
if (disagreeing.length) {
|
|
223
257
|
throw new SdkError("xmr-nodes-disagree", {
|
|
224
258
|
txid: located.txid, height: located.height,
|
|
225
|
-
nodes: disagreeing.map((r) => ({ url: r.url, operator: r.operator, blockHash: r.blockHash || null, why: r.state })),
|
|
259
|
+
nodes: disagreeing.map((r) => ({ url: r.url, operator: r.operator, blockHash: r.blockHash || null, why: r.state, outputs: r.outputs || null })),
|
|
226
260
|
agreed: seen.filter((r) => r.state === "ok").map((r) => ({ url: r.url, operator: r.operator, blockHash: r.blockHash })),
|
|
227
261
|
});
|
|
228
262
|
}
|
|
@@ -230,43 +264,53 @@ export async function verifyArrival(request) {
|
|
|
230
264
|
const okay = seen.filter((r) => r.state === "ok");
|
|
231
265
|
if (!okay.length) return { seen: false, state: "pending", txid: located.txid, height: located.height, checked: seen };
|
|
232
266
|
|
|
267
|
+
// THE OBSERVATIONS MUST BE IDENTICAL. Grouping by the whole observation (not just the block hash) means a
|
|
268
|
+
// node that reports a different amount, output key or unlock state does not count as agreeing with anyone.
|
|
269
|
+
const byObservation = new Map();
|
|
270
|
+
for (const r of okay) {
|
|
271
|
+
const key = JSON.stringify(r.observation);
|
|
272
|
+
if (!byObservation.has(key)) byObservation.set(key, { observation: r.observation, operators: new Set(), nodes: [] });
|
|
273
|
+
const g = byObservation.get(key);
|
|
274
|
+
g.operators.add(r.operator);
|
|
275
|
+
g.nodes.push(r);
|
|
276
|
+
}
|
|
277
|
+
if (byObservation.size > 1) {
|
|
278
|
+
throw new SdkError("xmr-nodes-disagree", {
|
|
279
|
+
txid: located.txid, height: located.height,
|
|
280
|
+
nodes: okay.map((r) => ({ url: r.url, operator: r.operator, blockHash: r.blockHash || null, outputs: r.outputs, why: "observation" })),
|
|
281
|
+
agreed: [],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const best = [...byObservation.values()][0];
|
|
285
|
+
// A node that is further along is not a liar: the minimum is the figure every witness can support. What
|
|
286
|
+
// matters is that the transaction itself is identical; confirmations only decide when the button opens.
|
|
287
|
+
const confirmations = Math.min(...best.nodes.map((r) => r.confirmations));
|
|
288
|
+
const amountAtomic = best.nodes[0].amountAtomic;
|
|
289
|
+
const agreedBy = best.nodes.map((r) => ({ url: r.url, operator: r.operator, blockHash: r.blockHash, confirmations: r.confirmations }));
|
|
290
|
+
|
|
233
291
|
// LOCKED IS NOT AN ARRIVAL, and the rule for that is the shared one (issue #84), mirrored from www/js.
|
|
234
|
-
const locked =
|
|
292
|
+
const locked = best.nodes.find((r) => r.unlock && r.unlock.locked);
|
|
235
293
|
if (locked) {
|
|
236
294
|
const until = locked.unlock;
|
|
237
295
|
throw new SdkError("xmr-locked", { txid: located.txid, height: located.height, address: request.address,
|
|
238
296
|
untilHeight: until.untilHeight ?? null, untilTime: until.untilTime ?? null });
|
|
239
297
|
}
|
|
240
298
|
|
|
241
|
-
// THE AMOUNT IS NOT LESS THAN EXPECTED. A shortfall is named rather than shown as a completed swap.
|
|
242
|
-
|
|
299
|
+
// THE AMOUNT IS NOT LESS THAN EXPECTED. A shortfall is named rather than shown as a completed swap. The figure
|
|
300
|
+
// comes from the agreed observation, never from the largest answer of one node: that maximum was the defect.
|
|
243
301
|
if (amountAtomic < expectedAtomic) {
|
|
244
302
|
throw new SdkError("xmr-underpaid", { txid: located.txid, height: located.height, got: amountAtomic.toString(), want: expectedAtomic.toString() });
|
|
245
303
|
}
|
|
246
304
|
|
|
247
|
-
// AGREEMENT: the same block hash, held by at least two DIFFERENT operators, with the required confirmations.
|
|
248
|
-
const byHash = new Map();
|
|
249
|
-
for (const r of okay) {
|
|
250
|
-
const key = r.blockHash;
|
|
251
|
-
if (!byHash.has(key)) byHash.set(key, { blockHash: key, operators: new Set(), nodes: [] });
|
|
252
|
-
const g = byHash.get(key);
|
|
253
|
-
g.operators.add(r.operator);
|
|
254
|
-
g.nodes.push(r);
|
|
255
|
-
}
|
|
256
|
-
let best = null;
|
|
257
|
-
for (const g of byHash.values()) if (!best || g.operators.size > best.operators.size) best = g;
|
|
258
|
-
const confirmations = Math.min(...best.nodes.map((r) => r.confirmations));
|
|
259
|
-
const agreedBy = best.nodes.map((r) => ({ url: r.url, operator: r.operator, blockHash: r.blockHash, confirmations: r.confirmations }));
|
|
260
|
-
|
|
261
305
|
if (best.operators.size < 2) {
|
|
262
|
-
return { seen: false, state: "one-node", txid: located.txid, height: located.height, blockHash: best.blockHash,
|
|
306
|
+
return { seen: false, state: "one-node", txid: located.txid, height: located.height, blockHash: best.observation.blockHash,
|
|
263
307
|
amountAtomic: amountAtomic.toString(), confirmations, operators: [...best.operators], agreedBy, checked: seen };
|
|
264
308
|
}
|
|
265
309
|
if (confirmations < minConfirmations) {
|
|
266
|
-
return { seen: false, state: "pending", why: "confirmations", txid: located.txid, height: located.height, blockHash: best.blockHash,
|
|
310
|
+
return { seen: false, state: "pending", why: "confirmations", txid: located.txid, height: located.height, blockHash: best.observation.blockHash,
|
|
267
311
|
amountAtomic: amountAtomic.toString(), confirmations, needConfirmations: minConfirmations, agreedBy, checked: seen };
|
|
268
312
|
}
|
|
269
|
-
return { seen: true, state: "arrived", txid: located.txid, height: located.height, blockHash: best.blockHash,
|
|
313
|
+
return { seen: true, state: "arrived", txid: located.txid, height: located.height, blockHash: best.observation.blockHash,
|
|
270
314
|
amountAtomic: amountAtomic.toString(), confirmations, needConfirmations: minConfirmations,
|
|
271
315
|
operators: [...best.operators], agreedBy, checked: seen };
|
|
272
316
|
}
|