@ninsei-labs/sdk 0.49.0 → 0.51.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 +30 -0
- package/README.md +18 -0
- package/assets/vendors/atomic/atomic-browser.js +24 -0
- package/assets/vendors/monero/monero-js-browser.js +58 -0
- package/assets/vendors/monero-js/LICENSE +9 -0
- package/assets/vendors/sdk/sdk-browser.js +14 -0
- package/assets/vendors/walletconnect/wc-browser.js +6202 -0
- package/engine/atomic/order-worker.js +129 -0
- package/engine/core/config.js +3 -1
- package/engine/sdk/bridge.js +3 -3
- package/index.d.ts +74 -2
- package/package.json +4 -2
- package/src/actions.mjs +15 -0
- package/src/adapters/monero-wallet.mjs +26 -0
- package/src/config.mjs +9 -0
- package/src/index.mjs +2 -0
- package/src/public/networks.d.ts +2 -0
- package/src/public/recovery.d.ts +16 -0
- package/src/quotes.mjs +100 -3
- package/src/recovery-key.mjs +101 -13
- package/src/swaps.mjs +17 -4
- package/src/sweep-half.mjs +14 -0
- package/src/sweep-restore.mjs +4 -1
- package/src/sweep.mjs +85 -5
- package/src/version.mjs +1 -1
package/src/recovery-key.mjs
CHANGED
|
@@ -385,6 +385,27 @@ function decodeOrderLog(log) {
|
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
// THE CLONE CARRIES THE ORDER, INCLUDING THE XMR AMOUNT. OrderCreated does not repeat `xmrAmount`, and the clone
|
|
389
|
+
// has no getter for it, but word 18 of the condition blob is in its code. Read it only from a clone of the exact
|
|
390
|
+
// shape: a contract of another size is not this order and must not yield a guessed number.
|
|
391
|
+
const CLONE_CODE_BYTES = 877;
|
|
392
|
+
const XMR_AMOUNT_WORD = 18;
|
|
393
|
+
const CLONE_PROXY_BYTES = 45;
|
|
394
|
+
const XMR_AMOUNT_OFFSET = CLONE_PROXY_BYTES + XMR_AMOUNT_WORD * 32;
|
|
395
|
+
|
|
396
|
+
function xmrAmountFromClone(code) {
|
|
397
|
+
if (typeof code !== "string" || !/^0x[0-9a-fA-F]+$/.test(code)) return { value: null, reason: "code" };
|
|
398
|
+
const bytes = (code.length - 2) / 2;
|
|
399
|
+
if (!Number.isInteger(bytes) || bytes !== CLONE_CODE_BYTES) return { value: null, reason: "not-clone" };
|
|
400
|
+
const word = code.slice(2 + XMR_AMOUNT_OFFSET * 2, 2 + (XMR_AMOUNT_OFFSET + 32) * 2);
|
|
401
|
+
if (word.length !== 64) return { value: null, reason: "not-clone" };
|
|
402
|
+
try {
|
|
403
|
+
return { value: BigInt("0x" + word).toString(), reason: null };
|
|
404
|
+
} catch {
|
|
405
|
+
return { value: null, reason: "code" };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
388
409
|
async function readLogs(evmLogs, request) {
|
|
389
410
|
if (typeof evmLogs !== "function") throw bad("evmLogs");
|
|
390
411
|
let logs;
|
|
@@ -397,6 +418,25 @@ async function readLogs(evmLogs, request) {
|
|
|
397
418
|
return logs;
|
|
398
419
|
}
|
|
399
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Is this number already held by an order on chain?
|
|
423
|
+
*
|
|
424
|
+
* A browser store is per device. The same twelve words opened on a second device, or restored before any search, start
|
|
425
|
+
* at number 0 again; two swaps would then share the taker's halves, and a refund of one publishes a half the other's
|
|
426
|
+
* maker can use. The check is the factory's own `OrderCreated` log, matched by the spend commitment the number derives.
|
|
427
|
+
*
|
|
428
|
+
* The caller owns the log transport, as everywhere else in this module. An unreadable log request is a refusal, not
|
|
429
|
+
* "free": the number is not reserved on a guess.
|
|
430
|
+
*/
|
|
431
|
+
export async function recoveryIndexTaken({ mnemonic, chainId, index, factory, evmLogs, fromBlock = 0, toBlock = "latest" } = {}) {
|
|
432
|
+
if (typeof factory !== "string" || !ADDRESS_RE.test(factory)) throw bad("factory");
|
|
433
|
+
const own = takerHalves({ mnemonic, chainId, index });
|
|
434
|
+
const logs = await readLogs(evmLogs, { address: factory, topics: [ORDER_CREATED_TOPIC], fromBlock, toBlock });
|
|
435
|
+
return logs.map(decodeOrderLog).filter(Boolean).some((order) =>
|
|
436
|
+
order.commitHalfLocker.toLowerCase() === own.spendCommitment.toLowerCase() ||
|
|
437
|
+
order.commitHalfClaimer.toLowerCase() === own.spendCommitment.toLowerCase());
|
|
438
|
+
}
|
|
439
|
+
|
|
400
440
|
// THE MONERO HEIGHT A RECOVERED ORDER STARTED AT. With one key there is no per-swap file to carry it, so it is
|
|
401
441
|
// derived from the settlement chain: the timestamp of the block that holds OrderCreated, then the last Monero
|
|
402
442
|
// block at or before it. A Monero block's timestamp may run ahead of wall time, hence the safety margin.
|
|
@@ -407,7 +447,7 @@ const MONERO_HEIGHT_SAFETY_BLOCKS = 60;
|
|
|
407
447
|
|
|
408
448
|
function secondsOf(value) {
|
|
409
449
|
const n = Number(value);
|
|
410
|
-
if (!Number.isFinite(n) || n
|
|
450
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
411
451
|
return n > 1e12 ? Math.floor(n / 1000) : Math.floor(n);
|
|
412
452
|
}
|
|
413
453
|
|
|
@@ -460,11 +500,15 @@ async function moneroReadersFromNodes({ nodes = [], network = null, fetchImpl =
|
|
|
460
500
|
return body.result;
|
|
461
501
|
};
|
|
462
502
|
return {
|
|
463
|
-
|
|
503
|
+
// `get_info.height` is the block COUNT, one past the top block. Asking a real node for a header at that height
|
|
504
|
+
// answers "greater than current top block"; the tip reader must name the top block itself.
|
|
505
|
+
tip: async () => Math.max(0, Number(node.height) - 1),
|
|
464
506
|
headerTimestamp: async (height) => {
|
|
465
507
|
const result = await call("get_block_header_by_height", { height });
|
|
466
508
|
const ts = Number(result.block_header && result.block_header.timestamp);
|
|
467
|
-
|
|
509
|
+
// GENESIS HAS TIMESTAMP 0. The binary search reads height 0 to prove the requested moment is before it; a
|
|
510
|
+
// `<= 0` refusal turned a legitimate genesis answer into "monero-header" and every old order lost its height.
|
|
511
|
+
if (!Number.isFinite(ts) || ts < 0) throw new Error("monero-header");
|
|
468
512
|
return ts;
|
|
469
513
|
},
|
|
470
514
|
};
|
|
@@ -472,22 +516,28 @@ async function moneroReadersFromNodes({ nodes = [], network = null, fetchImpl =
|
|
|
472
516
|
|
|
473
517
|
async function recoveredStartHeight(order, { evmBlockTime, moneroTip, moneroBlockTimestamp, cache }) {
|
|
474
518
|
if (order.blockNumber === null || order.blockNumber === undefined) {
|
|
475
|
-
return { startHeight: null, startHeightReason: "no-block-number" };
|
|
519
|
+
return { startHeight: null, startHeightReason: "no-block-number", blockTime: null };
|
|
476
520
|
}
|
|
477
|
-
|
|
478
|
-
|
|
521
|
+
// NOTHING TO READ WITH: no settlement block time and no Monero readers. That is a missing seam, not a missing date.
|
|
522
|
+
if (typeof evmBlockTime !== "function" && typeof moneroTip !== "function" && typeof moneroBlockTimestamp !== "function") {
|
|
523
|
+
return { startHeight: null, startHeightReason: "no-height-readers", blockTime: null };
|
|
479
524
|
}
|
|
525
|
+
// THE BLOCK TIME IS PART OF THE FOUND ORDER. The page used to read the block again just to show a date; the
|
|
526
|
+
// timestamp is already needed here for the Monero height, so it is returned with it.
|
|
480
527
|
let timestamp = cache.get(order.blockNumber);
|
|
481
528
|
if (timestamp === undefined) {
|
|
482
|
-
timestamp = secondsOf(await evmBlockTime(order.blockNumber));
|
|
483
|
-
if (timestamp === null) return { startHeight: null, startHeightReason: "no-block-time" };
|
|
529
|
+
timestamp = typeof evmBlockTime === "function" ? secondsOf(await evmBlockTime(order.blockNumber)) : null;
|
|
530
|
+
if (timestamp === null) return { startHeight: null, startHeightReason: "no-block-time", blockTime: null };
|
|
484
531
|
cache.set(order.blockNumber, timestamp);
|
|
485
532
|
}
|
|
533
|
+
if (typeof moneroTip !== "function" || typeof moneroBlockTimestamp !== "function") {
|
|
534
|
+
return { startHeight: null, startHeightReason: "no-height-readers", blockTime: timestamp };
|
|
535
|
+
}
|
|
486
536
|
try {
|
|
487
537
|
const height = await heightAtOrBefore(timestamp, { tip: moneroTip, headerTimestamp: moneroBlockTimestamp });
|
|
488
|
-
return { startHeight: Math.max(0, height - MONERO_HEIGHT_SAFETY_BLOCKS), startHeightReason: null };
|
|
538
|
+
return { startHeight: Math.max(0, height - MONERO_HEIGHT_SAFETY_BLOCKS), startHeightReason: null, blockTime: timestamp };
|
|
489
539
|
} catch {
|
|
490
|
-
return { startHeight: null, startHeightReason: "monero-header" };
|
|
540
|
+
return { startHeight: null, startHeightReason: "monero-header", blockTime: timestamp };
|
|
491
541
|
}
|
|
492
542
|
}
|
|
493
543
|
|
|
@@ -499,14 +549,23 @@ async function recoveredStartHeight(order, { evmBlockTime, moneroTip, moneroBloc
|
|
|
499
549
|
* positions) and merges the answers. Without one, every OrderCreated of the factory is read and matched locally.
|
|
500
550
|
*/
|
|
501
551
|
export async function findSwaps({ mnemonic, chainId, factory, address = null, evmLogs,
|
|
502
|
-
fromBlock =
|
|
503
|
-
evmBlockTime = null, moneroTip = null, moneroBlockTimestamp = null,
|
|
552
|
+
fromBlock = null, deploymentBlock = null, toBlock = "latest", gap = 5, maxIndex = 1000,
|
|
553
|
+
evmBlockTime = null, evmCode = null, moneroTip = null, moneroBlockTimestamp = null,
|
|
504
554
|
xmrNodes = null, moneroNetwork = null, fetchImpl = globalThis.fetch } = {}) {
|
|
505
555
|
if (typeof factory !== "string" || !ADDRESS_RE.test(factory)) throw bad("factory");
|
|
506
556
|
const chain = numberField(chainId, "chainId");
|
|
507
557
|
const max = numberField(maxIndex, "maxIndex") || 1000;
|
|
508
558
|
const missesAllowed = numberField(gap, "gap") || 5;
|
|
509
|
-
|
|
559
|
+
// THE CHAIN SAYS WHERE ITS ORDERS CAN START. An explicit fromBlock wins; otherwise the caller can pass the
|
|
560
|
+
// factory's deployment block (the SDK's chain registry exposes it as escrow.deploymentBlock). Zero remains the
|
|
561
|
+
// last resort for a caller that knows the factory existed at genesis.
|
|
562
|
+
if (deploymentBlock !== null && deploymentBlock !== undefined && !Number.isSafeInteger(Number(deploymentBlock))) {
|
|
563
|
+
throw bad("deploymentBlock");
|
|
564
|
+
}
|
|
565
|
+
const startBlock = fromBlock === null || fromBlock === undefined
|
|
566
|
+
? (deploymentBlock === null || deploymentBlock === undefined ? 0 : Number(deploymentBlock))
|
|
567
|
+
: fromBlock;
|
|
568
|
+
const base = { address: factory, fromBlock: startBlock, toBlock };
|
|
510
569
|
let logs = [];
|
|
511
570
|
if (address) {
|
|
512
571
|
const padded = topicAddress(address);
|
|
@@ -553,8 +612,37 @@ export async function findSwaps({ mnemonic, chainId, factory, address = null, ev
|
|
|
553
612
|
const readers = await moneroReadersFromNodes({ nodes: xmrNodes, network: moneroNetwork, fetchImpl });
|
|
554
613
|
if (readers) { tip = tip || readers.tip; header = header || readers.headerTimestamp; }
|
|
555
614
|
}
|
|
615
|
+
// THE ORDER IS NATIVE, AND THE PAYING TOKEN IS NOT ON THE CHAIN. The escrow receives the settlement network's
|
|
616
|
+
// native coin (`amount` is wei); which token the person swapped into it (USDC, USDT, ...) belongs to the DEX/quote
|
|
617
|
+
// record, not to OrderCreated or the clone. `payToken: null` plus a reason is the honest answer here; an invented
|
|
618
|
+
// "USDC" would be a claim the chain does not support.
|
|
619
|
+
for (const order of found) {
|
|
620
|
+
order.settlementToken = "native";
|
|
621
|
+
order.payToken = null;
|
|
622
|
+
order.payTokenReason = "not-on-chain";
|
|
623
|
+
}
|
|
556
624
|
for (const order of found) {
|
|
557
625
|
Object.assign(order, await recoveredStartHeight(order, { evmBlockTime, moneroTip: tip, moneroBlockTimestamp: header, cache }));
|
|
558
626
|
}
|
|
627
|
+
// THE XMR AMOUNT IS IN THE CLONE, NOT IN THE LOG. A caller without a code reader gets null and a named reason;
|
|
628
|
+
// an unreadable or differently-shaped contract does the same. This is data, not a withdrawal verdict.
|
|
629
|
+
for (const order of found) {
|
|
630
|
+
if (typeof evmCode !== "function") {
|
|
631
|
+
order.xmrAmount = null;
|
|
632
|
+
order.xmrAmountReason = "no-code-reader";
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
let code;
|
|
636
|
+
try {
|
|
637
|
+
code = await evmCode(order.escrow);
|
|
638
|
+
} catch {
|
|
639
|
+
order.xmrAmount = null;
|
|
640
|
+
order.xmrAmountReason = "code";
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const read = xmrAmountFromClone(code);
|
|
644
|
+
order.xmrAmount = read.value;
|
|
645
|
+
order.xmrAmountReason = read.reason;
|
|
646
|
+
}
|
|
559
647
|
return { swaps: found, scanned };
|
|
560
648
|
}
|
package/src/swaps.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { createActions } from "./actions.mjs";
|
|
|
27
27
|
// THE ONE RECOVERY KEY (sdk#76): the taker's halves come from it, not from a fresh random pair per swap.
|
|
28
28
|
import {
|
|
29
29
|
commitRecoveryIndex, createRecoveryKeyFile, importRecoveryKey, loadRecoveryKey,
|
|
30
|
-
recoveryKeyFileName, reserveRecoveryIndex, saveRecoveryKey, takerHalves,
|
|
30
|
+
recoveryIndexTaken, recoveryKeyFileName, reserveRecoveryIndex, saveRecoveryKey, takerHalves,
|
|
31
31
|
} from "./recovery-key.mjs";
|
|
32
32
|
// THE XMR ARRIVAL CHECK ON TWO NODES (issue #85) - IN THE CORE, from the same mirrored rule and block parser.
|
|
33
33
|
import * as xmrArrival from "./xmr-arrival.mjs";
|
|
@@ -804,10 +804,23 @@ export function createSwaps({ preflight, http, createRecord, mode = null, xmrNet
|
|
|
804
804
|
const key = saved || importRecoveryKey(supplied);
|
|
805
805
|
if (!saved) saveRecoveryKey(storage, key);
|
|
806
806
|
const chainId = request.order.chainId;
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
: Number(request.recoveryIndex);
|
|
807
|
+
const explicit = request.recoveryIndex !== undefined && request.recoveryIndex !== null;
|
|
808
|
+
let index = explicit ? Number(request.recoveryIndex) : reserveRecoveryIndex(storage, { chainId });
|
|
810
809
|
if (!Number.isSafeInteger(index) || index < 0) throw new SdkError("bad-input", { field: "recoveryIndex" });
|
|
810
|
+
// A BROWSER STORE IS PER DEVICE. The same words opened elsewhere start at the same number, so the factory's own
|
|
811
|
+
// log is asked before the number is used; a taken number is retired locally and the next one is reserved.
|
|
812
|
+
// Without a log seam the previous behaviour is kept - the caller cannot be asked to invent a chain read.
|
|
813
|
+
if (typeof chainLogs === "function" && request.order.factory) {
|
|
814
|
+
let hops = 0;
|
|
815
|
+
while (await recoveryIndexTaken({ mnemonic: key.mnemonic, chainId, index, factory: request.order.factory,
|
|
816
|
+
evmLogs: chainLogs, fromBlock: request.fromBlock === undefined ? 0 : request.fromBlock })) {
|
|
817
|
+
if (explicit) throw new SdkError("bad-input", { field: "recoveryIndex", why: "on-chain" });
|
|
818
|
+
commitRecoveryIndex(storage, index);
|
|
819
|
+
index = reserveRecoveryIndex(storage, { chainId });
|
|
820
|
+
hops += 1;
|
|
821
|
+
if (!Number.isSafeInteger(index) || index < 0 || hops > 1000) throw new SdkError("bad-input", { field: "recoveryIndex", why: "exhausted" });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
811
824
|
const derived = takerHalves({ mnemonic: key.mnemonic, chainId, index });
|
|
812
825
|
keySide = buildOwnSideFromHalves(request.order, { spendHalf: derived.spendHalf, viewHalf: derived.viewHalf }).side;
|
|
813
826
|
recovery = { storage, key, index, newKey: !saved };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// THE REVEALED HALF IS A 32-BYTE SCALAR, NOT AN ADDRESS.
|
|
2
|
+
//
|
|
3
|
+
// `revealedHalf()` returns one ABI word: 32 bytes of the ed25519 scalar the settlement published. The withdrawal path
|
|
4
|
+
// used to read it as an address (`hex.slice(24)`), keeping only the last 20 bytes; `(s_a + s_b)·G` then never matched the
|
|
5
|
+
// order's spend point and every real withdrawal was refused. The parser is separate from the chain read so a guard can
|
|
6
|
+
// hand it the exact 32-byte word without a wallet or an RPC.
|
|
7
|
+
const WORD_RE = /^[0-9a-f]{64}$/;
|
|
8
|
+
|
|
9
|
+
export function revealedHalfWord(word) {
|
|
10
|
+
const raw = String(word === undefined || word === null ? "" : word).trim();
|
|
11
|
+
const hex = (raw.startsWith("0x") || raw.startsWith("0X") ? raw.slice(2) : raw).toLowerCase();
|
|
12
|
+
if (!WORD_RE.test(hex) || /^0+$/.test(hex)) return null;
|
|
13
|
+
return "0x" + hex;
|
|
14
|
+
}
|
package/src/sweep-restore.mjs
CHANGED
|
@@ -142,7 +142,7 @@ export async function restoreWallet(request = {}) {
|
|
|
142
142
|
nodeUrl: requestWindow.nodeUrl, fromHeight, toHeight: requestWindow.toHeight, fetchImpl: request.fetchImpl,
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
-
windowReport = { used: false, skipped: null, fromHeight, toHeight: Number(requestWindow.toHeight),
|
|
145
|
+
windowReport = { used: false, skipped: null, fallback: false, fromHeight, toHeight: Number(requestWindow.toHeight),
|
|
146
146
|
blocks: Math.max(0, Number(requestWindow.toHeight) - fromHeight + 1), txs: hashes.length };
|
|
147
147
|
if (hashes.length && typeof driver.scanTxs === "function") {
|
|
148
148
|
await step("setRestoreHeight", () => driver.setRestoreHeight(tipHeight));
|
|
@@ -192,6 +192,9 @@ export async function restoreWallet(request = {}) {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// THE WINDOW DID NOT ANSWER: the full scan below is the fallback. Say so on the report, so the caller does not
|
|
196
|
+
// have to infer it from `used: false` and a balance.
|
|
197
|
+
if (windowReport) windowReport.fallback = true;
|
|
195
198
|
await step("setRestoreHeight", () => driver.setRestoreHeight(scanHeight));
|
|
196
199
|
// A RESCAN IS REQUIRED, NOT OPTIONAL: without it the height only applies to a wallet that has not scanned yet,
|
|
197
200
|
// and a wallet restored from a key has. A driver that cannot rescan is the caller's mistake, named here.
|
package/src/sweep.mjs
CHANGED
|
@@ -10,6 +10,9 @@ import * as engine from "./engine.mjs";
|
|
|
10
10
|
import { restoreWallet } from "./sweep-restore.mjs";
|
|
11
11
|
import { moneroWalletAdapter } from "./adapters/monero-wallet.mjs";
|
|
12
12
|
import { checkDestination } from "./destination.mjs";
|
|
13
|
+
import { revealedHalfWord } from "./sweep-half.mjs";
|
|
14
|
+
import { takerHalves } from "./recovery-key.mjs";
|
|
15
|
+
import { jointAddress } from "./order.mjs";
|
|
13
16
|
|
|
14
17
|
// The same boundary as when the file was issued: a passphrase shorter than twelve characters will not open the file.
|
|
15
18
|
const MIN_PASSPHRASE = 12;
|
|
@@ -19,6 +22,71 @@ const REVEALED_HALF_SELECTOR = "0x3f267bb6";
|
|
|
19
22
|
|
|
20
23
|
const contentsOf = (file) => (typeof file === "string" ? file : file && typeof file.contents === "string" ? file.contents : null);
|
|
21
24
|
|
|
25
|
+
const bare = (value) => String(value == null ? "" : value).replace(/^0x/i, "").toLowerCase();
|
|
26
|
+
|
|
27
|
+
// A FOUND ORDER AND THE KEY, WITHOUT A FILE. The recovery page used to write the order into the SDK's own payload
|
|
28
|
+
// (buildPayload) in memory; that rule belongs here, next to the withdrawal it feeds. The page keeps the order and
|
|
29
|
+
// the key; this function decides which side is the person, checks the chain's points against the key, builds the
|
|
30
|
+
// joint address and returns the same payload a recovery file would carry.
|
|
31
|
+
const payloadFromOrder = async (request) => {
|
|
32
|
+
const order = request && request.order;
|
|
33
|
+
if (!order || typeof order !== "object") throw new SdkError("bad-input", { field: "order" });
|
|
34
|
+
if (typeof request.mnemonic !== "string" || !request.mnemonic.trim()) throw new SdkError("bad-input", { field: "mnemonic" });
|
|
35
|
+
const chainId = Number(request.chainId);
|
|
36
|
+
if (!Number.isSafeInteger(chainId) || chainId <= 0) throw new SdkError("bad-input", { field: "chainId" });
|
|
37
|
+
const index = Number(order.index);
|
|
38
|
+
if (!Number.isSafeInteger(index) || index < 0) throw new SdkError("bad-input", { field: "order.index" });
|
|
39
|
+
const network = typeof request.network === "string" ? request.network.trim() : "";
|
|
40
|
+
const moneroNetwork = typeof request.moneroNetwork === "string" ? request.moneroNetwork.trim() : "";
|
|
41
|
+
if (!network) throw new SdkError("bad-input", { field: "network" });
|
|
42
|
+
if (!moneroNetwork) throw new SdkError("bad-input", { field: "moneroNetwork" });
|
|
43
|
+
const passphrase = typeof request.passphrase === "string" ? request.passphrase : "";
|
|
44
|
+
if (passphrase && passphrase.length < MIN_PASSPHRASE) {
|
|
45
|
+
throw new SdkError("bad-input", { field: "passphrase", min: MIN_PASSPHRASE });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const side = order.side === "locker" ? "buy" : order.side === "claimer" ? "sell" : null;
|
|
49
|
+
if (!side) throw new SdkError("bad-input", { field: "order.side" });
|
|
50
|
+
const halves = takerHalves({ mnemonic: request.mnemonic, chainId, index });
|
|
51
|
+
const buy = side === "buy";
|
|
52
|
+
if (bare(buy ? order.edPointLocker : order.edPointClaimer) !== bare(halves.spendPoint)) {
|
|
53
|
+
throw new SdkError("bad-input", { field: "order", why: "spend-point" });
|
|
54
|
+
}
|
|
55
|
+
if (bare(order.edViewPointLocker) !== bare(buy ? halves.viewPoint : halves.makerViewPoint)) {
|
|
56
|
+
throw new SdkError("bad-input", { field: "order", why: buy ? "own-view-point" : "maker-view-point" });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const otherSpendPoint = "0x" + bare(buy ? order.edPointClaimer : order.edPointLocker);
|
|
60
|
+
const otherViewPoint = "0x" + bare(halves.makerViewPoint);
|
|
61
|
+
const joint = jointAddress({
|
|
62
|
+
ownSpendHalf: halves.spendHalf, ownViewHalf: halves.viewHalf,
|
|
63
|
+
otherSpendPoint, otherViewPoint, network: moneroNetwork,
|
|
64
|
+
});
|
|
65
|
+
const escrow = { address: order.escrow, readyBy: order.readyBy, t1: order.t1 };
|
|
66
|
+
const payload = engine.recoveryFile.buildPayload({
|
|
67
|
+
swap: {
|
|
68
|
+
id: order.escrow, side, network, moneroNetwork,
|
|
69
|
+
createdAt: order.blockTime ? Number(order.blockTime) * 1000 : null,
|
|
70
|
+
payToken: null, payAmount: null, xmrAmount: null, rate: null, maker: null, quoteSignature: null,
|
|
71
|
+
escrow, timeline: null,
|
|
72
|
+
},
|
|
73
|
+
swapWallet: {
|
|
74
|
+
source: "halves", network: moneroNetwork, address: joint.address,
|
|
75
|
+
spendHalf: halves.spendHalf, viewHalf: halves.viewHalf,
|
|
76
|
+
otherSpendPoint, otherViewPoint, otherViewHalf: halves.makerViewHalf,
|
|
77
|
+
escrowAddress: order.escrow,
|
|
78
|
+
},
|
|
79
|
+
escrow,
|
|
80
|
+
receiveAddress: null,
|
|
81
|
+
restoreHeight: Number(order.startHeight) > 0 ? Number(order.startHeight) : null,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
payload,
|
|
85
|
+
contents: await engine.recoveryFile.createRecoveryFile(payload, passphrase),
|
|
86
|
+
address: joint.address,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
22
90
|
// A LOW-LEVEL WALLET ERROR IS STILL A REFUSAL, AND EVERY REFUSAL CARRIES A CODE.
|
|
23
91
|
//
|
|
24
92
|
// The library brings its own exceptions with no code at all - monero-ts answers "failed to get hashes" while
|
|
@@ -68,6 +136,13 @@ export function createSweep({ http, config, call = null }) {
|
|
|
68
136
|
const NETWORK_ENUM = { mainnet: "MAINNET", stagenet: "STAGENET", testnet: "TESTNET" };
|
|
69
137
|
|
|
70
138
|
const api = {
|
|
139
|
+
// BUILD THE WITHDRAWAL MATERIAL FROM A FOUND ORDER AND THE KEY. No recovery file, no temporary payload in the
|
|
140
|
+
// page: the returned `contents` is the same file `plan`/`keys` already accept, and `payload` is for callers
|
|
141
|
+
// that want the fields directly.
|
|
142
|
+
async fromOrder(request = {}) {
|
|
143
|
+
return await payloadFromOrder(request);
|
|
144
|
+
},
|
|
145
|
+
|
|
71
146
|
// BUILD THE WITHDRAWAL WALLET from the library namespace the caller has. The page used to pass a `createWallet`
|
|
72
147
|
// callback that mapped the enum itself; that mapping is here now, so a caller passes `moneroTs` and the keys.
|
|
73
148
|
wallet(request = {}) {
|
|
@@ -164,10 +239,10 @@ export function createSweep({ http, config, call = null }) {
|
|
|
164
239
|
if (escrowAddress && typeof call === "function") {
|
|
165
240
|
try {
|
|
166
241
|
const word = String((await call({ to: escrowAddress, data: REVEALED_HALF_SELECTOR })) || "");
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
242
|
+
const half = revealedHalfWord(word);
|
|
243
|
+
if (half !== null) {
|
|
169
244
|
revealed = true;
|
|
170
|
-
revealedValue =
|
|
245
|
+
revealedValue = half;
|
|
171
246
|
}
|
|
172
247
|
} catch { revealed = false; }
|
|
173
248
|
}
|
|
@@ -276,8 +351,13 @@ export function createSweep({ http, config, call = null }) {
|
|
|
276
351
|
async sign(request) {
|
|
277
352
|
const plan = await api.plan(request);
|
|
278
353
|
if (!request || typeof request.to !== "string" || !request.to) throw new SdkError("bad-input", { field: "to" });
|
|
279
|
-
|
|
280
|
-
|
|
354
|
+
// ONE ADAPTER FOR RESTORE AND SIGN. A caller that already holds the opened and synced wallet passes it as
|
|
355
|
+
// `wallet`; a caller that wants the SDK to build one passes `connectWallet`.
|
|
356
|
+
const wallet = request.wallet !== undefined && request.wallet !== null
|
|
357
|
+
? request.wallet
|
|
358
|
+
: (typeof request.connectWallet === "function"
|
|
359
|
+
? await request.connectWallet(plan)
|
|
360
|
+
: (() => { throw new SdkError("bad-input", { field: "connectWallet" }); })());
|
|
281
361
|
const role = ["open", "sync", "unlockedBalance", "sweepUnlocked", "relay", "close"];
|
|
282
362
|
const missing = role.filter((name) => !wallet || typeof wallet[name] !== "function");
|
|
283
363
|
if (missing.length) throw new SdkError("bad-input", { field: "wallet", missing });
|
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.51.0";
|