@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninsei-labs/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -38,13 +38,30 @@
|
|
|
38
38
|
"check:destination": "node tools/check-destination.mjs",
|
|
39
39
|
"check:codes-meta": "node tools/check-codes-meta.mjs",
|
|
40
40
|
"check:arrival-read": "node tools/check-arrival-read.mjs",
|
|
41
|
+
"check:sale-auth": "node tools/check-sale-auth.mjs",
|
|
42
|
+
"check:backup-confirmation": "node tools/check-backup-confirmation.mjs",
|
|
43
|
+
"check:key-view": "node tools/check-key-view.mjs",
|
|
44
|
+
"check:arrival-consensus": "node tools/check-arrival-consensus.mjs",
|
|
45
|
+
"check:arrival-real": "node tools/check-arrival-real.mjs",
|
|
46
|
+
"check:order-proof": "node tools/check-order-proof.mjs",
|
|
41
47
|
"check:daemon-address": "node tools/check-daemon-address.mjs",
|
|
42
48
|
"check:ci-scripts": "node tools/check-ci-scripts.mjs",
|
|
43
49
|
"check:factory-selectors": "node tools/check-factory-selectors.mjs",
|
|
44
50
|
"check:fee-estimate": "node tools/check-fee-estimate.mjs",
|
|
45
51
|
"check:deadline-scale": "node tools/check-deadline-scale.mjs",
|
|
46
52
|
"check:node-probe": "node tools/check-node-probe.mjs",
|
|
47
|
-
"check:recovery-key": "node tools/check.mjs --only=recovery-key"
|
|
53
|
+
"check:recovery-key": "node tools/check.mjs --only=recovery-key",
|
|
54
|
+
"check:recovery-open": "node tools/check-recovery-open.mjs",
|
|
55
|
+
"check:contract-vectors": "node tools/check-contract-vectors.mjs",
|
|
56
|
+
"check:live-contract": "node tools/check-live-contract.mjs",
|
|
57
|
+
"check:anvil-fork": "node tools/check-anvil-fork.mjs",
|
|
58
|
+
"check:dex": "node tools/check.mjs --only=cow,cow-refusal,kyber,kyber-refusal,uniswapx,uniswapx-refusal,uniswap-relay,uniswap-relay-refusal",
|
|
59
|
+
"check:anvil-uniswap": "node tools/check-anvil-uniswap.mjs",
|
|
60
|
+
"check:atomic-flow": "node tools/check-atomic-flow.mjs",
|
|
61
|
+
"check:packed-consumer": "node tools/check-packed-consumer.mjs",
|
|
62
|
+
"check:quote-binding": "node tools/check-quote-binding.mjs",
|
|
63
|
+
"check:types-consumer": "node node_modules/typescript/bin/tsc -p tools/ts-consumer/tsconfig.json",
|
|
64
|
+
"check:monero-live": "node tools/check-monero-live.mjs"
|
|
48
65
|
},
|
|
49
66
|
"types": "./index.d.ts",
|
|
50
67
|
"exports": {
|
|
@@ -126,5 +143,8 @@
|
|
|
126
143
|
"@scure/bip39": "2.4.0",
|
|
127
144
|
"keccak256": "^1.0.6"
|
|
128
145
|
},
|
|
129
|
-
"sideEffects": false
|
|
146
|
+
"sideEffects": false,
|
|
147
|
+
"devDependencies": {
|
|
148
|
+
"typescript": "7.0.2"
|
|
149
|
+
}
|
|
130
150
|
}
|
|
@@ -25,16 +25,25 @@
|
|
|
25
25
|
// with a code and params. Neither `null` nor a zero in place of the answer: an empty address and a zero balance
|
|
26
26
|
// look like real data, and that is exactly why they are not returned.
|
|
27
27
|
import { SdkError } from "../errors.mjs";
|
|
28
|
+
import { signedFeeTotal } from "../sweep-fees.mjs";
|
|
28
29
|
import { config as engineConfig, xmrAddress } from "../engine.mjs";
|
|
29
30
|
|
|
30
31
|
const XMR_VM = "monero";
|
|
31
32
|
// The wallet session label - the same as the withdrawal page's. This is not a secret and not an address, but a session name.
|
|
32
33
|
const DEFAULT_PASSWORD = "ninsei-wallet-session";
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
// THE STANDARD NAMES ARE PART OF THE PROTOCOL, NOT OF A DEPLOYMENT. XMR_NETWORKS names what the demo is configured
|
|
36
|
+
// for (stagenet in CI, fakechain in regtest); a mainnet or testnet wallet is still a valid Monero wallet and the
|
|
37
|
+
// withdrawal page takes the network from its own node. Refuse only a name that is not a Monero network at all.
|
|
38
|
+
const STANDARD_XMR_NETWORKS = ["mainnet", "stagenet", "testnet", "fakechain"];
|
|
39
|
+
|
|
40
|
+
/** Monero network names declared by the core's registry plus the standard names. */
|
|
35
41
|
const knownNetworks = () => {
|
|
36
42
|
const declared = engineConfig.XMR_NETWORKS || {};
|
|
37
|
-
return [...new Set(
|
|
43
|
+
return [...new Set([
|
|
44
|
+
...Object.values(declared).filter((v) => typeof v === "string" && v),
|
|
45
|
+
...STANDARD_XMR_NETWORKS,
|
|
46
|
+
])].sort();
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
/**
|
|
@@ -183,11 +192,9 @@ export function moneroWalletAdapter({
|
|
|
183
192
|
},
|
|
184
193
|
|
|
185
194
|
// Optional, like every withdrawal adapter: a library that reports no fee answers null rather than a zero.
|
|
195
|
+
// A split sweep pays a fee on EVERY signed transaction; the total is what leaves the remainder.
|
|
186
196
|
async fee(signed) {
|
|
187
|
-
|
|
188
|
-
if (!tx || typeof tx.getFee !== "function") return null;
|
|
189
|
-
const value = tx.getFee();
|
|
190
|
-
return value === null || value === undefined ? null : String(value);
|
|
197
|
+
return signedFeeTotal(signed);
|
|
191
198
|
},
|
|
192
199
|
|
|
193
200
|
// THE MONERO WALLET HAS NO EVENTS (monero-ts has no on/removeListener): a subscriber receives a notification
|
package/src/adapters/monero.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// sending is a separate call. That way the interface has a point where it can show the person what exactly will
|
|
9
9
|
// leave, BEFORE sending rather than after.
|
|
10
10
|
import { SdkError } from "../errors.mjs";
|
|
11
|
+
import { signedFeeTotal } from "../sweep-fees.mjs";
|
|
11
12
|
|
|
12
13
|
export function moneroWallet({ createWallet, networkType, serverUri, password = "ninsei-sweep-session", fields = {} }) {
|
|
13
14
|
const build = typeof createWallet === "function" ? createWallet : null;
|
|
@@ -41,15 +42,12 @@ export function moneroWallet({ createWallet, networkType, serverUri, password =
|
|
|
41
42
|
return await wallet.sweepUnlocked({ address, relay });
|
|
42
43
|
},
|
|
43
44
|
// THE FEE OF WHAT WAS SIGNED, when the library reports one. `sweepUnlocked` answers with the signed
|
|
44
|
-
// transactions; monero-ts puts the fee on each transaction
|
|
45
|
-
// method as "unknown", so this adapter says what it can and nothing
|
|
45
|
+
// transactions; monero-ts puts the fee on each transaction and a split sweep pays every one of them. Optional
|
|
46
|
+
// by contract - the core treats a missing method as "unknown", so this adapter says what it can and nothing
|
|
47
|
+
// more; one unreadable fee makes the total unknown, not partial.
|
|
46
48
|
async fee(signed) {
|
|
47
49
|
need();
|
|
48
|
-
|
|
49
|
-
const tx = list[0];
|
|
50
|
-
if (!tx || typeof tx.getFee !== "function") return null;
|
|
51
|
-
const value = tx.getFee();
|
|
52
|
-
return value === null || value === undefined ? null : String(value);
|
|
50
|
+
return signedFeeTotal(signed);
|
|
53
51
|
},
|
|
54
52
|
|
|
55
53
|
async relay(signed) {
|
package/src/codes.mjs
CHANGED
|
@@ -92,6 +92,9 @@ export const ERROR_CODES = [
|
|
|
92
92
|
// through - server-unavailable, while the service answered and did not accept - server-refused (with the
|
|
93
93
|
// status and its own reason). The advice to the interface differs: "retry" versus "investigate".
|
|
94
94
|
"server-refused",
|
|
95
|
+
// THE TRANSACTION WAS SENT BUT THE RECEIPT DOES NOT PROVE AN ESCROW: no receipt yet, no expected OrderCreated,
|
|
96
|
+
// or a receipt from another factory. The tx hash is the handle for a retry read; nothing was saved as funded.
|
|
97
|
+
"funding-pending",
|
|
95
98
|
// A TIMEOUT WAITING FOR XMR TO ARRIVE - a separate code. "We did not wait long enough" is a state of the
|
|
96
99
|
// money, not a breakdown and not an empty response: mixing it with other refusals, the interface could not
|
|
97
100
|
// tell "not arrived yet" from "broken".
|
|
@@ -167,6 +170,7 @@ export const ERROR_META = Object.freeze({
|
|
|
167
170
|
"storage-unavailable": { retryable: true, side: "service" },
|
|
168
171
|
"server-unavailable": { retryable: true, side: "service" },
|
|
169
172
|
"server-refused": { retryable: false, side: "service" },
|
|
173
|
+
"funding-pending": { retryable: true, side: "chain" },
|
|
170
174
|
"xmr-timeout": { retryable: true, side: "chain" },
|
|
171
175
|
"xmr-locked": { retryable: true, side: "chain" },
|
|
172
176
|
"xmr-nodes-disagree": { retryable: true, side: "chain" },
|
package/src/config.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as engine from "./engine.mjs";
|
|
9
9
|
import { CHECK_CODES, ERROR_CODES, STEP_CODES } from "./codes.mjs";
|
|
10
10
|
import { fail } from "./errors.mjs";
|
|
11
|
-
import { implementedVms, legFor } from "./legs/index.mjs";
|
|
11
|
+
import { implementedVms, legFor, profileOfChain } from "./legs/index.mjs";
|
|
12
12
|
import { routeProviders } from "./legs/evm.mjs";
|
|
13
13
|
|
|
14
14
|
// TOKENS ARE TAKEN BY AN ENGINE FUNCTION, NOT FROM A NETWORK FIELD: `tokensOf` returns the native coin first,
|
|
@@ -41,7 +41,10 @@ const tokensOfNetwork = (chainId) => {
|
|
|
41
41
|
const chainConfig = (chain) => ({
|
|
42
42
|
id: chain.id,
|
|
43
43
|
name: chain.name,
|
|
44
|
-
vm: chain.vm ||
|
|
44
|
+
vm: chain.vm || null,
|
|
45
|
+
// WHAT THIS SETTLEMENT SIDE LOOKS LIKE: address format, contract/program/UTXO and ref kind. It is metadata and
|
|
46
|
+
// does not claim a driver exists; `implementedVms` and `legFor` decide that. Finality belongs to a chain.
|
|
47
|
+
settlementProfile: profileOfChain(chain.id),
|
|
45
48
|
chainId: chain.chainId,
|
|
46
49
|
nativeSymbol: chain.native && chain.native.symbol ? chain.native.symbol : null,
|
|
47
50
|
tokens: tokensOfNetwork(chain.chainId).map((t) => ({
|
|
@@ -108,7 +111,7 @@ export const DEFAULT_XMR_NODES = Object.freeze({
|
|
|
108
111
|
{ url: "https://mainnet.xmr.kernal.eu:18089", operator: "kernal.eu" },
|
|
109
112
|
],
|
|
110
113
|
stagenet: [
|
|
111
|
-
{ url: "https://xmr.
|
|
114
|
+
{ url: "https://xmr-stagenet.ninsei.finance", operator: "Ninsei" },
|
|
112
115
|
{ url: "https://stagenet.xmr.kernal.eu:38089", operator: "kernal.eu" },
|
|
113
116
|
],
|
|
114
117
|
});
|
package/src/decline.mjs
CHANGED
|
@@ -41,7 +41,12 @@ const hexOf = (bytes) => {
|
|
|
41
41
|
return out;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* keccak256("Declined(address)") - topic0 of a decline's log. The escrow indexes `claimer`, so a log emitted by the
|
|
46
|
+
* current contracts also carries it in topic1; the event signature (and therefore topic0) does not change. Logs from
|
|
47
|
+
* escrows deployed before the indexing carry the claimer in data instead, which is why this module matches only
|
|
48
|
+
* topic0 and does not assume a claimer position.
|
|
49
|
+
*/
|
|
45
50
|
export const DECLINED_TOPIC0 = hexOf(keccak256(DECLINE_EVENT_SIGNATURE()));
|
|
46
51
|
|
|
47
52
|
// THE NODE'S OWN TWO FIELDS for a declined swap: `{ status: "declined", code: "compliance" }`. The code is the
|
package/src/feeEstimate.mjs
CHANGED
|
@@ -13,13 +13,16 @@ const DEFAULT_TIMEOUT_MS = 8000;
|
|
|
13
13
|
|
|
14
14
|
const text = (value) => (typeof value === "string" ? value.trim() : "");
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* A PER-BYTE price -> the fee for a transaction of `bytes`/weight. Monero's `get_fee_estimate` returns `fee` and
|
|
18
|
+
* `fees` in atomic units per byte; dividing by 1024 made every estimate 1024 times too low.
|
|
19
|
+
*/
|
|
20
|
+
export function feeForBytes(feePerByte, bytes) {
|
|
21
|
+
let perByte;
|
|
19
22
|
let size;
|
|
20
|
-
try {
|
|
21
|
-
if (
|
|
22
|
-
return (
|
|
23
|
+
try { perByte = BigInt(feePerByte); size = BigInt(bytes); } catch { throw new RangeError("feeForBytes: whole numbers only"); }
|
|
24
|
+
if (perByte < 0n || size < 0n) throw new RangeError("feeForBytes: negative input");
|
|
25
|
+
return (perByte * size).toString();
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
const atomicField = (source, ...names) => {
|
|
@@ -34,8 +37,9 @@ const atomicField = (source, ...names) => {
|
|
|
34
37
|
/**
|
|
35
38
|
* ASK A NODE WHAT A TRANSACTION WILL COST.
|
|
36
39
|
*
|
|
37
|
-
* The answer is a price per
|
|
38
|
-
* assembled transaction; `feeForBytes` is the arithmetic that gets there. Never throws - a refusal names
|
|
40
|
+
* The answer is a price per byte (and the daemon's own priority list, and its quantization mask). The exact sum
|
|
41
|
+
* needs the assembled transaction; `feeForBytes` is the arithmetic that gets there. Never throws - a refusal names
|
|
42
|
+
* the problem.
|
|
39
43
|
*/
|
|
40
44
|
export async function moneroFeeEstimate({ url, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
41
45
|
// TRAILING SLASHES WITHOUT A REGEX: a pattern anchored on a run of slashes is flagged as a slow expression on
|
|
@@ -65,9 +69,12 @@ export async function moneroFeeEstimate({ url, fetchImpl = globalThis.fetch, tim
|
|
|
65
69
|
try { body = await response.json(); } catch { return { ok: false, why: "not-json" }; }
|
|
66
70
|
if (!body || body.error) return { ok: false, why: "rpc", rpc: body && body.error ? body.error.code ?? null : null };
|
|
67
71
|
const result = body.result || {};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
// THE DAEMON'S OWN UNITS. `fee` and each entry of `fees` are atomic units PER BYTE; `quantization_mask` is the
|
|
73
|
+
// unit the final fee is rounded to. `fee_per_kb` was never sent, so the old read found nothing on a real node and
|
|
74
|
+
// the old fallback then divided per-byte values by 1024.
|
|
75
|
+
const feePerByte = atomicField(result, "fee", "feePerByte");
|
|
76
|
+
const quantizationMask = atomicField(result, "quantization_mask", "quantizationMask");
|
|
77
|
+
const list = result.fees ?? result.feesPerByte ?? null;
|
|
71
78
|
let perPriority = null;
|
|
72
79
|
if (Array.isArray(list)) {
|
|
73
80
|
perPriority = [];
|
|
@@ -75,6 +82,6 @@ export async function moneroFeeEstimate({ url, fetchImpl = globalThis.fetch, tim
|
|
|
75
82
|
try { perPriority.push(BigInt(value).toString()); } catch { perPriority = null; break; }
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
|
-
if (
|
|
79
|
-
return { ok: true, url: base,
|
|
85
|
+
if (feePerByte === null && !perPriority) return { ok: false, why: "no-estimate" };
|
|
86
|
+
return { ok: true, url: base, feePerByte, perPriority, quantizationMask };
|
|
80
87
|
}
|
package/src/index.mjs
CHANGED
|
@@ -198,10 +198,11 @@ export function createNinsei(options) {
|
|
|
198
198
|
// THE STORAGE THE INTERFACE BROUGHT BECOMES THE CORE'S STORAGE: unfinished swaps and the agent session live
|
|
199
199
|
// where the interface decided (browser storage, memory, a server). Without the option the core takes its
|
|
200
200
|
// default: web storage if it is present in the environment, otherwise memory.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
// ONE CLIENT, ONE STORE. A second client with another adapter used to replace the module-wide store, so the
|
|
202
|
+
// first client listed the second's swaps and could delete them. Even a client that brought no adapter binds the
|
|
203
|
+
// current one, so a later explicit store cannot take it over.
|
|
204
|
+
engine.store.bindStorage(o.storage || engine.store.storage());
|
|
205
|
+
if (o.storage) engine.auth.setSessionStore(o.storage);
|
|
205
206
|
const http = createHttp({ apiBase: config.apiBase, fetchImpl: o.fetch, timeoutMs: config.limits.httpTimeoutMs });
|
|
206
207
|
// THE CHAIN READ AND THE ALLOW-LIST GO INTO THE QUOTE CHECK: without the read the signature certifies itself,
|
|
207
208
|
// so the same evmCall seam the lock and the actions use is handed to the quotes too.
|
package/src/legs/index.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// What must not be in a driver: no user strings, no DOM. A driver is chain reads and writes plus knowing what the
|
|
11
11
|
// network is measured in for this VM.
|
|
12
12
|
import * as evm from "./evm.mjs";
|
|
13
|
+
export { profileOf, VM_PROFILES } from "./profiles.mjs";
|
|
14
|
+
import { profileOf } from "./profiles.mjs";
|
|
13
15
|
|
|
14
16
|
export const LEGS = { evm };
|
|
15
17
|
|
|
@@ -37,6 +39,12 @@ export const vmOfChain = (chainId) => {
|
|
|
37
39
|
return chain && chain.vm ? chain.vm : null;
|
|
38
40
|
};
|
|
39
41
|
|
|
42
|
+
/** The VM profile for a network, or null when the network or the profile is unknown. */
|
|
43
|
+
export const profileOfChain = (chainId) => {
|
|
44
|
+
const vm = vmOfChain(chainId);
|
|
45
|
+
return vm ? profileOf(vm) : null;
|
|
46
|
+
};
|
|
47
|
+
|
|
40
48
|
/**
|
|
41
49
|
* The leg driver for a network. A network without a driver is a NAMED refusal: you can see which vm is not
|
|
42
50
|
* supported and which are. This is exactly the rule "no network is silently assumed to be EVM".
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// WHAT A SETTLEMENT SIDE LOOKS LIKE, WITHOUT ASSERTING THAT A DRIVER EXISTS.
|
|
2
|
+
//
|
|
3
|
+
// The profile is metadata, not an implementation. It tells the SDK and the interface which address and transaction
|
|
4
|
+
// shapes a VM uses, and whether settlement is a contract, a program or a UTXO script. The driver registry
|
|
5
|
+
// (./index.mjs) stays the source of truth for what is RUNNABLE today.
|
|
6
|
+
//
|
|
7
|
+
// Bitcoin is listed because the planned order is Tron, Solana, then Bitcoin; a UTXO settlement side is a
|
|
8
|
+
// script/HTLC, not an account with an escrow contract. A profile is not a promise that the chain is selectable:
|
|
9
|
+
// `escrow: null` and the driver registry decide that. Finality is not here: it is a chain policy, not a VM shape,
|
|
10
|
+
// and a generic "12 confirmations" is wrong for an L2. Add it to the chain that first needs it.
|
|
11
|
+
export const VM_PROFILES = Object.freeze({
|
|
12
|
+
// EVM: 20-byte hex addresses, a contract escrow, a transaction hash.
|
|
13
|
+
evm: Object.freeze({
|
|
14
|
+
vm: "evm",
|
|
15
|
+
addressFormat: "hex-20",
|
|
16
|
+
settlementKind: "contract",
|
|
17
|
+
refKind: "address",
|
|
18
|
+
txRef: "hash",
|
|
19
|
+
}),
|
|
20
|
+
// TVM: base58check addresses on the wire (with a 0x41 hex form internally), a contract escrow and energy/bandwidth
|
|
21
|
+
// instead of gas.
|
|
22
|
+
tron: Object.freeze({
|
|
23
|
+
vm: "tron",
|
|
24
|
+
addressFormat: "base58check",
|
|
25
|
+
settlementKind: "contract",
|
|
26
|
+
refKind: "address",
|
|
27
|
+
txRef: "hash",
|
|
28
|
+
}),
|
|
29
|
+
// SVM: base58 addresses, a program with PDA accounts, a transaction signature.
|
|
30
|
+
solana: Object.freeze({
|
|
31
|
+
vm: "solana",
|
|
32
|
+
addressFormat: "base58",
|
|
33
|
+
settlementKind: "program",
|
|
34
|
+
refKind: "pda",
|
|
35
|
+
txRef: "signature",
|
|
36
|
+
}),
|
|
37
|
+
// UTXO: no account and no escrow contract. Settlement is an HTLC/adaptor-script output, addressed by a script
|
|
38
|
+
// or an address, and identified by a txid. A future Bitcoin-family chain uses the same shape with its own
|
|
39
|
+
// network parameters. Taproot outputs (the likely shape of an adaptor-signature leg) are bech32m (BIP350), so
|
|
40
|
+
// that format is named here next to the legacy bech32 (v0) and base58check forms.
|
|
41
|
+
bitcoin: Object.freeze({
|
|
42
|
+
vm: "bitcoin",
|
|
43
|
+
addressFormat: "base58check-or-bech32-or-bech32m",
|
|
44
|
+
settlementKind: "utxo-script",
|
|
45
|
+
refKind: "script",
|
|
46
|
+
txRef: "txid",
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** The profile for a VM, or null. An unknown VM is never silently treated as EVM. */
|
|
51
|
+
export const profileOf = (vm) => VM_PROFILES[vm] || null;
|
package/src/lock.mjs
CHANGED
|
@@ -99,12 +99,10 @@ export async function send(request, wallet, deps = {}) {
|
|
|
99
99
|
send: (tx) => wallet.send(tx),
|
|
100
100
|
// NORMALISING THE RECEIPT ARGUMENT'S SHAPE - HERE, AT THE CORE'S BOUNDARY, NOT AT EVERY CALLER.
|
|
101
101
|
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// normalise the argument to a string here: funding.js is foreign code and must not be edited, and every
|
|
107
|
-
// adapter should not repeat the normalisation.
|
|
102
|
+
// funding.js calls the receipt with an OBJECT { hash } (waitReceipt), while the adapter contract declares
|
|
103
|
+
// receipt(txHash: string). A strict adapter (sdk/src/adapters/evm-wallet.mjs) answers bad-input to an object,
|
|
104
|
+
// the refusal is SWALLOWED by the receipt wait, and the address would silently fall back. We normalise the
|
|
105
|
+
// argument to a string at this boundary so every adapter sees one shape.
|
|
108
106
|
receipt: (arg) => wallet.receipt(typeof arg === "string" ? arg : (arg && typeof arg.hash === "string" ? arg.hash : arg)),
|
|
109
107
|
});
|
|
110
108
|
} catch (error) {
|
package/src/order.mjs
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// THE COUNTERPARTY'S SIDE AND THE JOINT ADDRESS.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// THREE CHECKS, ALL MANDATORY BEFORE SIGNING - after it there is nothing to fix:
|
|
4
4
|
//
|
|
5
5
|
// 1) the DLEQ proof stands in the CONTEXT OF THIS order: the counterparty proved for our terms, not for others.
|
|
6
6
|
// The context is the same one the quote signature is bound to (see quotes.firm);
|
|
7
7
|
// 2) the proven ed25519 point is THE VERY ONE that will go into the contract and from which the address is
|
|
8
8
|
// assembled. Without this check the counterparty could prove one thing and substitute another: on our side
|
|
9
9
|
// everything would look sound, and the discrepancy would surface when claiming the XMR, with nothing left to
|
|
10
|
-
// fix
|
|
10
|
+
// fix;
|
|
11
|
+
// 3) the proven secp256k1 point matches `commitHalfClaimer`, the commitment the contract compares on claim and
|
|
12
|
+
// refund. A proof without this comparison binds the wrong half and the escrow rejects it after the other side
|
|
13
|
+
// has paid.
|
|
11
14
|
//
|
|
12
|
-
//
|
|
15
|
+
// All three are done by the engine (order.js: the verifying code is the same for us and for the counterparty). The
|
|
13
16
|
// core has no cryptography of its own and should not: a copy would silently drift from the original.
|
|
14
17
|
import { SdkError } from "./errors.mjs";
|
|
15
18
|
import { primitives } from "./primitives.mjs";
|
|
@@ -86,6 +89,43 @@ const sameFormAsPoint = (proof) => (proof && typeof proof === "object" && proof.
|
|
|
86
89
|
// "no point", not "point in another form".
|
|
87
90
|
const pointForm = (v) => (typeof v === "string" && v ? norm(v) : v);
|
|
88
91
|
|
|
92
|
+
// THE COUNTERPARTY'S VIEW MATERIAL HAS TWO SPELLINGS, AND THEY MUST AGREE. A quote carries `viewHalfClaimer`
|
|
93
|
+
// (and sometimes `viewHalf`), a point as `viewPoint` or `viewPub`. The guard and the builder used to prefer
|
|
94
|
+
// different names: the guard took the half first and ignored the point, the builder took the point first. A
|
|
95
|
+
// counterparty could then supply a half and a different point, the address was built from one and the recovery
|
|
96
|
+
// words from the other. Normalise once, refuse conflicting aliases, and derive the point from the half when only
|
|
97
|
+
// the half is present.
|
|
98
|
+
export function normalizeCounterpartyView(counterparty) {
|
|
99
|
+
const cp = counterparty && typeof counterparty === "object" ? counterparty : {};
|
|
100
|
+
const halves = [cp.viewHalf, cp.viewHalfClaimer].filter((v) => v !== undefined && v !== null && v !== "");
|
|
101
|
+
let half = null;
|
|
102
|
+
if (halves.length) {
|
|
103
|
+
try { half = BigInt(String(halves[0])); } catch { throw new SdkError("bad-input", { field: "counterparty", why: "view-half" }); }
|
|
104
|
+
for (const other of halves.slice(1)) {
|
|
105
|
+
let n = null;
|
|
106
|
+
try { n = BigInt(String(other)); } catch { n = null; }
|
|
107
|
+
if (n === null || n !== half) throw new SdkError("bad-input", { field: "counterparty", why: "conflicting-view-half" });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const points = [cp.viewPoint, cp.viewPub].filter((v) => v !== undefined && v !== null && v !== "");
|
|
111
|
+
let point = null;
|
|
112
|
+
if (points.length) {
|
|
113
|
+
point = pointForm(points[0]);
|
|
114
|
+
if (!isPoint(point)) throw new SdkError("bad-input", { field: "counterparty", why: "view-point" });
|
|
115
|
+
for (const other of points.slice(1)) {
|
|
116
|
+
if (pointForm(other) !== point) throw new SdkError("bad-input", { field: "counterparty", why: "conflicting-view-point" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (half !== null) {
|
|
120
|
+
let derived = null;
|
|
121
|
+
try { derived = pointForm(builder().pointOf(half)); } catch { derived = null; }
|
|
122
|
+
if (!derived) throw new SdkError("bad-input", { field: "counterparty", why: "view-half" });
|
|
123
|
+
if (point !== null && point !== derived) throw new SdkError("bad-input", { field: "counterparty", why: "view-half-point-mismatch" });
|
|
124
|
+
point = derived;
|
|
125
|
+
}
|
|
126
|
+
return { half: half === null ? null : halves[0], point };
|
|
127
|
+
}
|
|
128
|
+
|
|
89
129
|
/**
|
|
90
130
|
* Our side of the order. It is built by the engine's builder: two halves, their points, the DLEQ PROOF (it is
|
|
91
131
|
* bound to the order terms and so is not computed without the context) and the side's encryption key.
|
|
@@ -136,11 +176,12 @@ export function acceptCounterparty({ context, side = null, counterparty = null,
|
|
|
136
176
|
// (rfq/orderQuote.mjs). While only `edPub`/`viewPub` were accepted, the facade's own side was NOT accepted:
|
|
137
177
|
// start answered bad-input { field: "counterparty", missing: ["edPub"] } to its own answer. The engine knew
|
|
138
178
|
// this mapping (sdk/src/swap-flow.mjs: edPub: quote.edPointClaimer). We accept BOTH names.
|
|
179
|
+
const view = normalizeCounterpartyView(counterparty);
|
|
139
180
|
accepted = {
|
|
140
181
|
proof: sameFormAsPoint(counterparty.proof),
|
|
141
182
|
edPub: pointForm(counterparty.edPub || counterparty.edPoint || counterparty.edPointClaimer || pub.ed || null),
|
|
142
|
-
viewPoint:
|
|
143
|
-
viewHalf:
|
|
183
|
+
viewPoint: view.point,
|
|
184
|
+
viewHalf: view.half,
|
|
144
185
|
// THE COUNTERPARTY'S SPEND HALF: HIS commitment (commitHalfClaimer) is computed from it where it did not
|
|
145
186
|
// arrive ready-made. Without the half the commitment cannot be obtained - and that is named null, not invented.
|
|
146
187
|
half: counterparty.half !== undefined ? counterparty.half : null,
|
|
@@ -155,13 +196,18 @@ export function acceptCounterparty({ context, side = null, counterparty = null,
|
|
|
155
196
|
// not what the page runs on.
|
|
156
197
|
const mock = builder().newSide(context, true);
|
|
157
198
|
accepted = { proof: sameFormAsPoint(mock.proof), edPub: norm(mock.pub.ed), viewPoint: norm(mock.viewPub),
|
|
158
|
-
viewHalf: mock.viewHalf, half: mock.half,
|
|
199
|
+
viewHalf: mock.viewHalf, half: mock.half,
|
|
200
|
+
// THE STAND-IN DOES CARRY A COMMITMENT: it is derived from the half it just built, by the same formula
|
|
201
|
+
// the contract uses. The proof check below is therefore the real one, not a path that skips it.
|
|
202
|
+
commitHalfClaimer: norm(builder().halfCommitment(mock.half)), sealed: null, providerId: null, standIn: true, mock };
|
|
159
203
|
} else {
|
|
160
204
|
throw new SdkError("bad-input", { field: "counterparty" });
|
|
161
205
|
}
|
|
162
206
|
const missing = [];
|
|
163
207
|
if (!accepted.proof) missing.push("proof");
|
|
164
208
|
if (!accepted.edPub) missing.push("edPub");
|
|
209
|
+
// THE COMMITMENT IS NOT OPTIONAL. Without it the proof says nothing about the half the contract will compare.
|
|
210
|
+
if (!accepted.commitHalfClaimer) missing.push("commitHalfClaimer");
|
|
165
211
|
if (missing.length) throw new SdkError("bad-input", { field: "counterparty", missing });
|
|
166
212
|
// OPENING THE SEALED PACKAGE - BY THE SAME ENGINE CALL, and the key is OURS (the package is addressed to us).
|
|
167
213
|
// The envelope's composition is locked separately (www/js/atomic/halfenc.js, HALFENC_LOCK_REASON): in the live
|
|
@@ -169,7 +215,7 @@ export function acceptCounterparty({ context, side = null, counterparty = null,
|
|
|
169
215
|
// with a refusal rather than passing itself off as a working key handover.
|
|
170
216
|
const ownEncPriv = accepted.sealed && side && side.enc ? side.enc.priv : null;
|
|
171
217
|
let verdict = null;
|
|
172
|
-
try { verdict = builder().verifySide({ proof: accepted.proof, pub: { ed: accepted.edPub } }, context, accepted.sealed, ownEncPriv); }
|
|
218
|
+
try { verdict = builder().verifySide({ proof: accepted.proof, pub: { ed: accepted.edPub }, commitHalfClaimer: accepted.commitHalfClaimer }, context, accepted.sealed, ownEncPriv); }
|
|
173
219
|
catch { throw new SdkError("bad-input", { field: "counterparty" }); }
|
|
174
220
|
if (!verdict || verdict.ok !== true) {
|
|
175
221
|
throw new SdkError("quote-refused", { step: "proof", why: verdict && verdict.reason ? "rejected" : "unknown" });
|
package/src/public/networks.d.ts
CHANGED
|
@@ -21,10 +21,23 @@ export interface ChainUniswap {
|
|
|
21
21
|
[field: string]: unknown;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** What a settlement side looks like: address format, account/UTXO settlement and reference shape. Metadata, not a
|
|
25
|
+
* claim that a driver exists. */
|
|
26
|
+
export interface SettlementProfile {
|
|
27
|
+
vm: string;
|
|
28
|
+
addressFormat: string;
|
|
29
|
+
settlementKind: "contract" | "program" | "utxo-script" | string;
|
|
30
|
+
refKind: "address" | "pda" | "script" | string;
|
|
31
|
+
txRef: "hash" | "signature" | "txid" | string;
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
export interface Chain {
|
|
25
35
|
id: string;
|
|
26
36
|
name: string;
|
|
27
|
-
|
|
37
|
+
/** A registry chain names its VM; the type allows null so an unknown network is never read as EVM. */
|
|
38
|
+
vm: string | null;
|
|
39
|
+
/** Present when the VM has a profile; unknown VMs are null, never silently EVM. */
|
|
40
|
+
settlementProfile?: SettlementProfile | null;
|
|
28
41
|
chainId: number;
|
|
29
42
|
caip2?: string;
|
|
30
43
|
testnet?: boolean;
|
package/src/public/recovery.d.ts
CHANGED
|
@@ -275,6 +275,8 @@ export declare const RECOVERY_KEY_STORE: string;
|
|
|
275
275
|
export declare const RECOVERY_KEY_FILE_MAGIC: string;
|
|
276
276
|
/** The domain half derivation binds to; the chain id and swap number join it. */
|
|
277
277
|
export declare const RECOVERY_KEY_DOMAIN: string;
|
|
278
|
+
/** The salt-based derivation domain; the chain, factory, role and order salt join it. */
|
|
279
|
+
export declare const RECOVERY_KEY_DOMAIN_V2: string;
|
|
278
280
|
/** The domain of the counterparty's view half, derived from the same one key. */
|
|
279
281
|
export declare const MAKER_VIEW_HALF_DOMAIN: string;
|
|
280
282
|
/** The encrypted backup envelope version. */
|
|
@@ -290,6 +292,8 @@ export interface RecoveryKey {
|
|
|
290
292
|
readonly mnemonic: string;
|
|
291
293
|
readonly fingerprint: string;
|
|
292
294
|
readonly createdAt: string;
|
|
295
|
+
/** True only after the person confirmed that the key is saved; a key in storage is not proof of that. */
|
|
296
|
+
readonly backupConfirmed?: boolean;
|
|
293
297
|
}
|
|
294
298
|
|
|
295
299
|
/** A derived swap number: the halves cross JSON as hex scalars. */
|
|
@@ -305,6 +309,15 @@ export interface TakerHalves {
|
|
|
305
309
|
readonly spendCommitment: string;
|
|
306
310
|
}
|
|
307
311
|
|
|
312
|
+
/** A salt-based derivation: no number is reserved, and the public order salt is the input. */
|
|
313
|
+
export interface TakerHalvesV2 extends Omit<TakerHalves, "index"> {
|
|
314
|
+
readonly index: null;
|
|
315
|
+
readonly derivation: "salt-v2";
|
|
316
|
+
readonly factory: string;
|
|
317
|
+
readonly role: string;
|
|
318
|
+
readonly salt: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
308
321
|
/** One order found by the key's commitments. */
|
|
309
322
|
export interface FoundSwap {
|
|
310
323
|
readonly escrow: string;
|
|
@@ -338,12 +351,17 @@ export interface FoundSwap {
|
|
|
338
351
|
* the expected clone. */
|
|
339
352
|
readonly xmrAmount: string | null;
|
|
340
353
|
readonly xmrAmountReason: "event" | "no-code-reader" | "code" | "not-clone" | null;
|
|
341
|
-
|
|
354
|
+
/** The v1 swap number; null for a salt-based v2 order. */
|
|
355
|
+
readonly index: number | null;
|
|
356
|
+
/** Which derivation found this order. v1 orders keep recovering through the index path. */
|
|
357
|
+
readonly derivation: "index-v1" | "salt-v2";
|
|
358
|
+
/** The factory the order was read from; the v2 binding needs it. */
|
|
359
|
+
readonly factory: string;
|
|
342
360
|
readonly side: "locker" | "claimer";
|
|
343
361
|
/** The Monero height a withdrawal may start from, derived from the order's block time. Null when the caller did
|
|
344
362
|
* not pass the readers; `startHeightReason` then names why. */
|
|
345
363
|
readonly startHeight: number | null;
|
|
346
|
-
readonly startHeightReason: "no-block-number" | "no-height-readers" | "monero-nodes-unreachable" | "monero-network-mismatch" | "no-block-time" | "monero-header" | null;
|
|
364
|
+
readonly startHeightReason: "no-block-number" | "no-height-readers" | "monero-nodes-unreachable" | "monero-network-mismatch" | "no-block-time" | "block-time-read" | "monero-header" | null;
|
|
347
365
|
}
|
|
348
366
|
|
|
349
367
|
/** A storage adapter: the same shape the SDK store takes. */
|
|
@@ -361,8 +379,16 @@ export declare function importRecoveryKey(mnemonic: string, options?: { createdA
|
|
|
361
379
|
export declare function fingerprintOf(mnemonic: string): string;
|
|
362
380
|
/** The binding for a chain and swap number: `ninsei-taker-v1|<chainId>|<index>`. */
|
|
363
381
|
export declare function recoveryBinding(chainId: number, index: number): string;
|
|
382
|
+
/** The salt-based binding: `ninsei-taker-v2|<chainId>|<factory>|<role>|<salt>`. */
|
|
383
|
+
export declare function recoveryBindingV2(request: { chainId: number; factory: string; role?: string; salt: string }): string;
|
|
384
|
+
/** Normalises a 32-byte salt from an event word, a hex string, a bigint or a safe integer. */
|
|
385
|
+
export declare function recoverySaltOf(value: string | number | bigint): string;
|
|
386
|
+
/** A fresh 32-byte public salt for one salt-based order. */
|
|
387
|
+
export declare function newRecoverySalt(): string;
|
|
364
388
|
/** Derives the taker spend and view halves for one swap number. */
|
|
365
389
|
export declare function takerHalves(request: { mnemonic: string; chainId: number; index: number }): TakerHalves;
|
|
390
|
+
/** Derives the taker halves from a public per-order salt; no index is reserved. */
|
|
391
|
+
export declare function takerHalvesV2(request: { mnemonic: string; chainId: number; factory: string; role?: string; salt: string }): TakerHalvesV2;
|
|
366
392
|
/** The stable backup name, with `DO-NOT-DELETE` in it on purpose. */
|
|
367
393
|
export declare function recoveryKeyFileName(request: { mnemonic?: string; fingerprint?: string; createdAt?: string | Date }): string;
|
|
368
394
|
/** A plain backup file: readable header above the words. */
|
|
@@ -372,9 +398,9 @@ export declare function createRecoveryKeyFile(request: { mnemonic: string; creat
|
|
|
372
398
|
/** Reads a plain or encrypted backup file and validates the words. */
|
|
373
399
|
export declare function openRecoveryKeyFile(contents: string, passphrase?: string): Promise<RecoveryKey>;
|
|
374
400
|
/** Saves the key through a storage adapter. */
|
|
375
|
-
export declare function saveRecoveryKey(storage: RecoveryKeyStorage, key: RecoveryKey & { nextIndex?: number; reserved?: Record<string, unknown>; retired?: Record<string, unknown
|
|
401
|
+
export declare function saveRecoveryKey(storage: RecoveryKeyStorage, key: RecoveryKey & { nextIndex?: number; reserved?: Record<string, unknown>; retired?: Record<string, unknown>; backupConfirmed?: boolean }): RecoveryKey;
|
|
376
402
|
/** Reads the key from a storage adapter, or null. */
|
|
377
|
-
export declare function loadRecoveryKey(storage: RecoveryKeyStorage): (RecoveryKey & { nextIndex: number; reserved: Record<string, unknown>; retired: Record<string, unknown
|
|
403
|
+
export declare function loadRecoveryKey(storage: RecoveryKeyStorage): (RecoveryKey & { nextIndex: number; reserved: Record<string, unknown>; retired: Record<string, unknown>; backupConfirmed?: boolean }) | null;
|
|
378
404
|
/** Reserves a swap number; the caller releases it if the request is abandoned. */
|
|
379
405
|
export declare function reserveRecoveryIndex(storage: RecoveryKeyStorage, request: { chainId: number; swapId?: string | null }): number;
|
|
380
406
|
/** True when an order already holds this number's commitment on chain. A page that starts swaps without the SDK's
|
|
@@ -403,7 +429,9 @@ export declare function findSwaps(request: {
|
|
|
403
429
|
/** The factory's deployment block from the chain registry; used when `fromBlock` is omitted. */
|
|
404
430
|
deploymentBlock?: number | null;
|
|
405
431
|
toBlock?: number | string;
|
|
432
|
+
/** Deprecated and ignored: discovery no longer stops after a run of empty numbers. */
|
|
406
433
|
gap?: number;
|
|
434
|
+
/** The last index to try. Defaults to the allocator's ceiling; a smaller value returns `complete: false`. */
|
|
407
435
|
maxIndex?: number;
|
|
408
436
|
/** Reads the settlement block timestamp, in seconds: (blockNumber) -> seconds. */
|
|
409
437
|
evmBlockTime?: (blockNumber: number) => Promise<number>;
|
|
@@ -420,4 +448,13 @@ export declare function findSwaps(request: {
|
|
|
420
448
|
moneroNetwork?: string | null;
|
|
421
449
|
/** The fetch used for those node requests; absent - the global fetch. */
|
|
422
450
|
fetchImpl?: typeof fetch;
|
|
423
|
-
}): Promise<{
|
|
451
|
+
}): Promise<{
|
|
452
|
+
swaps: FoundSwap[];
|
|
453
|
+
scanned: number;
|
|
454
|
+
/** The last index tried, or null when no index was scanned. */
|
|
455
|
+
searchedUpTo: number | null;
|
|
456
|
+
/** True only when the whole allocator range was searched. */
|
|
457
|
+
complete: boolean;
|
|
458
|
+
/** `max-index` when a smaller caller-supplied bound stopped the search. */
|
|
459
|
+
incompleteReason?: string;
|
|
460
|
+
}>;
|
package/src/public/recovery.mjs
CHANGED
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
export {
|
|
26
26
|
RECOVERY_KEY_ALG,
|
|
27
27
|
RECOVERY_KEY_DOMAIN,
|
|
28
|
+
RECOVERY_KEY_DOMAIN_V2,
|
|
28
29
|
RECOVERY_KEY_ENVELOPE_VERSION,
|
|
29
30
|
RECOVERY_KEY_FILE_MAGIC,
|
|
30
31
|
RECOVERY_KEY_ITER,
|
|
@@ -39,13 +40,17 @@ export {
|
|
|
39
40
|
fingerprintOf,
|
|
40
41
|
importRecoveryKey,
|
|
41
42
|
loadRecoveryKey,
|
|
43
|
+
newRecoverySalt,
|
|
42
44
|
openRecoveryKeyFile,
|
|
43
45
|
recoveryBinding,
|
|
46
|
+
recoveryBindingV2,
|
|
44
47
|
recoveryIndexTaken,
|
|
48
|
+
recoverySaltOf,
|
|
45
49
|
recoveryKeyFileName,
|
|
46
50
|
recoveryKeyText,
|
|
47
51
|
releaseRecoveryIndex,
|
|
48
52
|
reserveRecoveryIndex,
|
|
49
53
|
saveRecoveryKey,
|
|
50
54
|
takerHalves,
|
|
55
|
+
takerHalvesV2,
|
|
51
56
|
} from "../recovery-key.mjs";
|