@ninsei-labs/sdk 0.52.0 → 0.54.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 +266 -0
- package/README.md +83 -5
- 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 +154 -26
- package/package.json +35 -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 +7 -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 +53 -5
- package/src/public/recovery.mjs +6 -0
- package/src/public/xmr.d.ts +26 -8
- package/src/quotes.mjs +410 -3
- package/src/recovery-key.mjs +217 -39
- package/src/recovery.mjs +9 -1
- package/src/relayFeatures.mjs +191 -0
- package/src/relayFirmClient.mjs +86 -0
- package/src/relayQuote.mjs +209 -0
- package/src/relayRequest.mjs +188 -0
- package/src/relayTerms.mjs +145 -0
- package/src/relayView.mjs +110 -0
- package/src/swap-flow.mjs +3 -2
- package/src/swaps.mjs +402 -80
- 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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// THE VIEW MATERIAL A RELAY REQUEST CARRIES.
|
|
2
|
+
//
|
|
3
|
+
// The relay `firm/1` exchange does not send a Monero view scalar in clear text. The client chooses the view
|
|
4
|
+
// halves, keeps them in its recovery material, and encrypts only the node's share to the X25519 key in the
|
|
5
|
+
// signed public features. The profile is fixed by the relay wire spec: HPKE RFC 9180 base mode, DHKEM(X25519,
|
|
6
|
+
// HKDF-SHA256), HKDF-SHA256 and ChaCha20-Poly1305.
|
|
7
|
+
//
|
|
8
|
+
// This module owns the profile, not the request shape. It names one encryption and one decryption, both taking
|
|
9
|
+
// the application `info` as bytes, so the caller cannot accidentally use a different suite or a different
|
|
10
|
+
// interpretation of the context. The relay client builds `info` with `relayViewInfo`.
|
|
11
|
+
import { CipherSuite, DhkemX25519HkdfSha256, HkdfSha256 } from "@hpke/core";
|
|
12
|
+
import { Chacha20Poly1305 } from "@hpke/chacha20poly1305";
|
|
13
|
+
import { SdkError } from "./errors.mjs";
|
|
14
|
+
|
|
15
|
+
const utf8 = (value) => new TextEncoder().encode(String(value));
|
|
16
|
+
|
|
17
|
+
const bytesOf = (value) => {
|
|
18
|
+
if (value instanceof Uint8Array) return value;
|
|
19
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
20
|
+
throw new SdkError("bad-input", { field: "viewEnvelope" });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const bytesToHex = (bytes) => {
|
|
24
|
+
let out = "0x";
|
|
25
|
+
for (const byte of bytesOf(bytes)) out += byte.toString(16).padStart(2, "0");
|
|
26
|
+
return out;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const hexToBytes = (value, width, field) => {
|
|
30
|
+
const text = String(value === undefined || value === null ? "" : value).replace(/^0x/, "");
|
|
31
|
+
if (!new RegExp("^[0-9a-fA-F]{" + (width * 2) + "}$").test(text)) {
|
|
32
|
+
throw new SdkError("bad-input", { field });
|
|
33
|
+
}
|
|
34
|
+
const out = new Uint8Array(width);
|
|
35
|
+
for (let i = 0; i < width; i += 1) out[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16);
|
|
36
|
+
return out;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const suite = () => new CipherSuite({
|
|
40
|
+
kem: new DhkemX25519HkdfSha256(),
|
|
41
|
+
kdf: new HkdfSha256(),
|
|
42
|
+
aead: new Chacha20Poly1305(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// The relay binds encryption to the exact terms, the client half and both view points. The prefix and the
|
|
46
|
+
// order of the five words are part of the wire format; a different order still decrypts, which is why it is
|
|
47
|
+
// built here once instead of at each call site.
|
|
48
|
+
export const RELAY_VIEW_INFO_PREFIX = "ninsei-relay-view-v1";
|
|
49
|
+
|
|
50
|
+
export function relayViewInfo({ termsDigest, commitHalf, edPoint, edViewPointClient, edViewPointNode } = {}) {
|
|
51
|
+
const words = [
|
|
52
|
+
["termsDigest", termsDigest],
|
|
53
|
+
["commitHalf", commitHalf],
|
|
54
|
+
["edPoint", edPoint],
|
|
55
|
+
["edViewPointClient", edViewPointClient],
|
|
56
|
+
["edViewPointNode", edViewPointNode],
|
|
57
|
+
].map(([field, value]) => hexToBytes(value, 32, field));
|
|
58
|
+
const prefix = utf8(RELAY_VIEW_INFO_PREFIX);
|
|
59
|
+
const out = new Uint8Array(prefix.length + words.reduce((n, word) => n + word.length, 0));
|
|
60
|
+
out.set(prefix, 0);
|
|
61
|
+
let at = prefix.length;
|
|
62
|
+
for (const word of words) { out.set(word, at); at += word.length; }
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// One sender context, one Seal. The relay spec forbids reusing an ephemeral key for another encryption, so
|
|
67
|
+
// the context is created here and never exposed.
|
|
68
|
+
export async function sealViewEnvelope({ hpkeKey, info, plaintext } = {}) {
|
|
69
|
+
// The two plaintext lengths are part of the wire contract: ask carries the node's view scalar, bid carries
|
|
70
|
+
// the node's scalar followed by the client's. A caller that passes another length is refused before any
|
|
71
|
+
// network or cryptographic work.
|
|
72
|
+
const publicKey = hexToBytes(hpkeKey, 32, "hpkeKey");
|
|
73
|
+
const body = bytesOf(plaintext);
|
|
74
|
+
if (body.length !== 32 && body.length !== 64) {
|
|
75
|
+
throw new SdkError("bad-input", { field: "plaintext" });
|
|
76
|
+
}
|
|
77
|
+
if (!(info instanceof Uint8Array)) throw new SdkError("bad-input", { field: "info" });
|
|
78
|
+
const ctx = suite();
|
|
79
|
+
let key = null;
|
|
80
|
+
try {
|
|
81
|
+
key = await ctx.kem.deserializePublicKey(publicKey);
|
|
82
|
+
const sender = await ctx.createSenderContext({ recipientPublicKey: key, info });
|
|
83
|
+
const ciphertext = bytesOf(await sender.seal(body));
|
|
84
|
+
return { enc: bytesToHex(sender.enc), ciphertext: bytesToHex(ciphertext) };
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof SdkError) throw error;
|
|
87
|
+
throw new SdkError("bad-input", { field: "hpkeKey" });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The recipient side exists for checks and for a node that embeds this package. It is not needed by a client
|
|
92
|
+
// that only asks for a quote, but keeping both halves in one module makes a symmetric test possible.
|
|
93
|
+
export async function openViewEnvelope({ privateKey, enc, ciphertext, info } = {}) {
|
|
94
|
+
const secret = hexToBytes(privateKey, 32, "privateKey");
|
|
95
|
+
const encapsulated = hexToBytes(enc, 32, "enc");
|
|
96
|
+
const text = String(ciphertext === undefined || ciphertext === null ? "" : ciphertext).replace(/^0x/, "");
|
|
97
|
+
const width = text.length / 2;
|
|
98
|
+
if (width !== 48 && width !== 80) throw new SdkError("bad-input", { field: "ciphertext" });
|
|
99
|
+
const body = hexToBytes(ciphertext, width, "ciphertext");
|
|
100
|
+
if (!(info instanceof Uint8Array)) throw new SdkError("bad-input", { field: "info" });
|
|
101
|
+
const ctx = suite();
|
|
102
|
+
try {
|
|
103
|
+
const key = await ctx.kem.deserializePrivateKey(secret, true);
|
|
104
|
+
const recipient = await ctx.createRecipientContext({ recipientKey: key, enc: encapsulated, info });
|
|
105
|
+
return bytesOf(await recipient.open(body));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof SdkError) throw error;
|
|
108
|
+
throw new SdkError("bad-input", { field: "ciphertext" });
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/swap-flow.mjs
CHANGED
|
@@ -193,7 +193,8 @@ export async function runAtomicOrder({
|
|
|
193
193
|
// string, and that turned the check into a ritual: the stand-in's proof was bound to a different string,
|
|
194
194
|
// that is, to nothing. Now exactly the path a real provider would take is checked.
|
|
195
195
|
const mock = await worker.buildSide(orderContext);
|
|
196
|
-
counter = { proof: mock.proof, edPub: mock.pub.ed, mock, viewHalf: mock.viewHalf, viewPoint: mock.viewPub
|
|
196
|
+
counter = { proof: mock.proof, edPub: mock.pub.ed, mock, viewHalf: mock.viewHalf, viewPoint: mock.viewPub,
|
|
197
|
+
commitHalfClaimer: (await worker.halfCommitment(mock.half)).commitment };
|
|
197
198
|
}
|
|
198
199
|
// THE COUNTERPARTY CHECK REMAINS ONE AND IT IS THE MAIN ONE: the DLEQ proof in the context of this order.
|
|
199
200
|
// It confirms that both public points stand for ONE scalar - that is, that the half declared for the Monero
|
|
@@ -203,7 +204,7 @@ export async function runAtomicOrder({
|
|
|
203
204
|
// receive: without that check a correct proof would guarantee nothing about the chain.
|
|
204
205
|
say("verify", "dleq");
|
|
205
206
|
const verdict = await worker.verifyCounterparty(orderContext, {
|
|
206
|
-
proof: counter.proof, edPub: counter.edPub,
|
|
207
|
+
proof: counter.proof, edPub: counter.edPub, commitHalfClaimer: counter.commitHalfClaimer,
|
|
207
208
|
}, null);
|
|
208
209
|
if (!verdict.ok) throw new SdkError("quote-refused", { step: "proof", why: "rejected" });
|
|
209
210
|
|