@ninsei-labs/sdk 0.39.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 +866 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/engine/atomic/dleq2.js +330 -0
- package/engine/atomic/halfenc.js +205 -0
- package/engine/atomic/halves.js +119 -0
- package/engine/atomic/order-client.js +57 -0
- package/engine/atomic/order.js +132 -0
- package/engine/atomic/orderContext.js +70 -0
- package/engine/atomic/orderHalfCommit.js +29 -0
- package/engine/core/chainSource.js +306 -0
- package/engine/core/clock.js +40 -0
- package/engine/core/config.js +885 -0
- package/engine/core/finality.js +93 -0
- package/engine/core/format.js +168 -0
- package/engine/core/preSignGuards.js +495 -0
- package/engine/core/rfqSource.js +193 -0
- package/engine/core/sellWindow.js +43 -0
- package/engine/core/store.js +100 -0
- package/engine/core/swap.js +1153 -0
- package/engine/evm/amounts.js +131 -0
- package/engine/evm/auth.js +156 -0
- package/engine/evm/claimGas.js +87 -0
- package/engine/evm/depositor.js +127 -0
- package/engine/evm/dex.js +836 -0
- package/engine/evm/escrow.js +208 -0
- package/engine/evm/factory.js +149 -0
- package/engine/evm/fees.js +90 -0
- package/engine/evm/funding.js +309 -0
- package/engine/evm/gasReserve.js +88 -0
- package/engine/evm/index.js +123 -0
- package/engine/evm/injected.js +175 -0
- package/engine/evm/permit.js +125 -0
- package/engine/evm/prices.js +179 -0
- package/engine/evm/session.js +777 -0
- package/engine/mock/chain.js +98 -0
- package/engine/monero/address.js +265 -0
- package/engine/monero/halvesSweep.js +68 -0
- package/engine/monero/node.js +110 -0
- package/engine/monero/scan.js +325 -0
- package/engine/monero/swapKeys.js +69 -0
- package/engine/monero/unlock.js +46 -0
- package/engine/monero/wallet.js +168 -0
- package/engine/recovery/recoveryFile.js +363 -0
- package/engine/recovery/restore.js +176 -0
- package/engine/sdk/bridge.js +609 -0
- package/engine/ui/dom.js +90 -0
- package/engine/ui/walletModal.js +132 -0
- package/index.d.ts +1417 -0
- package/package.json +74 -0
- package/src/actions.mjs +72 -0
- package/src/adapters/evm-wallet.mjs +126 -0
- package/src/adapters/index.mjs +21 -0
- package/src/adapters/local-storage.mjs +32 -0
- package/src/adapters/monero-wallet.mjs +185 -0
- package/src/adapters/monero.mjs +51 -0
- package/src/adapters/node-test-evm-wallet.mjs +345 -0
- package/src/codes.mjs +188 -0
- package/src/config.mjs +277 -0
- package/src/engine.mjs +53 -0
- package/src/errors.mjs +24 -0
- package/src/http.mjs +72 -0
- package/src/index.mjs +256 -0
- package/src/legs/evm.mjs +118 -0
- package/src/legs/index.mjs +50 -0
- package/src/levelsWalkSpec.mjs +183 -0
- package/src/lock.mjs +140 -0
- package/src/order.mjs +212 -0
- package/src/preflight.mjs +137 -0
- package/src/primitives.mjs +47 -0
- package/src/quoteEip712.mjs +35 -0
- package/src/quoteEip712Spec.mjs +214 -0
- package/src/quotes.mjs +465 -0
- package/src/record.mjs +65 -0
- package/src/recovery.mjs +40 -0
- package/src/sim.mjs +37 -0
- package/src/swap-flow.mjs +492 -0
- package/src/swaps.mjs +935 -0
- package/src/sweep.mjs +222 -0
- package/src/wallet.mjs +164 -0
- package/src/xmr-arrival.mjs +289 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// EIP-712 TYPED DATA FOR AN ORDER QUOTE - THE SPEC, SHARED BY THE NODE AND THE SDK.
|
|
2
|
+
//
|
|
3
|
+
// WHY THE SAME FILE EXISTS TWICE. The node (rfq/) runs on the provider's machine and never sees www/ or sdk/:
|
|
4
|
+
// it has to start on its own. The SDK is a package and never sees rfq/. So the spec is embodied twice and the
|
|
5
|
+
// two copies are pinned by sha256 (tools/check-quote-eip712.mjs), exactly like the order context. A divergence
|
|
6
|
+
// here would mean one side signs what the other does not verify - the failure this format exists to prevent.
|
|
7
|
+
//
|
|
8
|
+
// WHAT IS BOUND, AND WHY EACH FIELD IS INSIDE THE DIGEST (changing any of them breaks recovery):
|
|
9
|
+
// provider, quoteKey - whose quote it is and which key signed it. For a solo maker the two are equal; a
|
|
10
|
+
// delegated key is a different address, and the registry maps it back to the provider
|
|
11
|
+
// (the chain check itself is a later stage; here both values are only bound).
|
|
12
|
+
// registry - the maker registry named by the order. Bound now so a quote cannot be replayed
|
|
13
|
+
// from one registry into another even before anyone reads `providerOf`.
|
|
14
|
+
// cashier - the fee vault of the order (issue #103): the fee destination is part of the SIGNED
|
|
15
|
+
// terms, so the factory no longer mints one vault per maker and the fee is not
|
|
16
|
+
// scattered across them. Bound so a relayer cannot retarget where the fee accrues.
|
|
17
|
+
// locker, claimer, - the order conditions: the same terms the DLEQ context binds and the escrow's
|
|
18
|
+
// amount, readyBy, termsHash covers. Binding them means the quote is usable for THESE terms only.
|
|
19
|
+
// t1, salt
|
|
20
|
+
// xmrAmount - the XMR side of the deal, in ATOMIC units (#110 step 3). It is IN this list AND in the
|
|
21
|
+
// chain's list (OrderQuote.sol) at once: the field set is ONE, so the provider's
|
|
22
|
+
// signature covers exactly the XMR the chain verifies at order birth. Earlier this field
|
|
23
|
+
// rode in the quote object OUTSIDE the digest on purpose - a field the chain could not
|
|
24
|
+
// see must not be signed; once the contract learned it, it moved in here together with
|
|
25
|
+
// the typehash, the depositor form and the independent vector.
|
|
26
|
+
// commitHalfClaimer, - the provider's own commitment and ed25519 spend point: both are inputs to the
|
|
27
|
+
// edPointClaimer escrow's termsHash, so they are bound and cannot be swapped for another order's.
|
|
28
|
+
// chainId - bound in the domain AND in the message. A quote signed for one network does not
|
|
29
|
+
// verify on another.
|
|
30
|
+
// feeBps, fee, - the provider's commission: the rate, the amount in wei and the recipient. Shown to
|
|
31
|
+
// feeRecipient the customer before signing (#76) and frozen by the signature afterwards.
|
|
32
|
+
// validUntil - the quote's own expiry, IN SECONDS (the order's readyBy/t1 and block.timestamp
|
|
33
|
+
// scale): a quote past it is refused regardless of the price.
|
|
34
|
+
// nonce - replay protection: the very same signed quote cannot be presented twice.
|
|
35
|
+
//
|
|
36
|
+
// The domain's verifyingContract is the escrow FACTORY taken from the order: the contract that will verify the
|
|
37
|
+
// quote in EVM (stage 2). The type order, the domain name and the version are part of the protocol - they may
|
|
38
|
+
// change only together with the quote version.
|
|
39
|
+
export const ORDER_QUOTE_DOMAIN_NAME = "NinseiEscrowFactory";
|
|
40
|
+
export const ORDER_QUOTE_DOMAIN_VERSION = "1";
|
|
41
|
+
// 8: the signed EIP-712 field set grew `xmrAmount` (#110 step 3) - the XMR the level walk gives is now covered by
|
|
42
|
+
// the provider's signature and verified ON CHAIN. This is the request-shape generation label, and it moves WITH the
|
|
43
|
+
// field set (see the note above). The DOMAIN name and the domain version string are UNCHANGED by it: they name the
|
|
44
|
+
// domain (the factory address and the chain), which did not change. So here the label and the domain version string
|
|
45
|
+
// move apart, and that is deliberate - the label tracks the message shape, not the domain identity.
|
|
46
|
+
export const ORDER_QUOTE_VERSION = 8;
|
|
47
|
+
|
|
48
|
+
// The domain type string is assembled from parts ON PURPOSE. A single literal of it is a phrase of several
|
|
49
|
+
// space-separated words, and the SDK check reads such literals as user-facing text. Do not "simplify" it.
|
|
50
|
+
const DOMAIN_TYPE_PARTS = ["EIP712Domain(", "string name,", "string version,", "uint256 chainId,", "address verifyingContract)"];
|
|
51
|
+
export const EIP712_DOMAIN_TYPE = DOMAIN_TYPE_PARTS.join("");
|
|
52
|
+
|
|
53
|
+
// The message, field by field and IN THIS ORDER. The order is the protocol: a rearrangement gives another
|
|
54
|
+
// digest, and every already-issued quote stops verifying at once.
|
|
55
|
+
export const ORDER_QUOTE_FIELDS = [
|
|
56
|
+
["provider", "address"],
|
|
57
|
+
["quoteKey", "address"],
|
|
58
|
+
["registry", "address"],
|
|
59
|
+
["cashier", "address"],
|
|
60
|
+
["locker", "address"],
|
|
61
|
+
["claimer", "address"],
|
|
62
|
+
["amount", "uint256"],
|
|
63
|
+
["readyBy", "uint64"],
|
|
64
|
+
["t1", "uint64"],
|
|
65
|
+
["salt", "bytes32"],
|
|
66
|
+
["commitHalfClaimer", "bytes32"],
|
|
67
|
+
["edPointClaimer", "bytes32"],
|
|
68
|
+
["chainId", "uint256"],
|
|
69
|
+
["feeBps", "uint256"],
|
|
70
|
+
["fee", "uint256"],
|
|
71
|
+
["feeRecipient", "address"],
|
|
72
|
+
["validUntil", "uint64"],
|
|
73
|
+
["nonce", "uint256"],
|
|
74
|
+
["xmrAmount", "uint256"],
|
|
75
|
+
];
|
|
76
|
+
export const ORDER_QUOTE_TYPE = "OrderQuote(" + ORDER_QUOTE_FIELDS.map((f) => f[1] + " " + f[0]).join(",") + ")";
|
|
77
|
+
|
|
78
|
+
// Error tokens are SINGLE WORDS: the SDK forbids multi-word string literals in sdk/src (they would read as
|
|
79
|
+
// user-facing text), so the spec names its failures by token and the caller maps them to its own codes.
|
|
80
|
+
const boom = (token) => { throw new Error(token); };
|
|
81
|
+
|
|
82
|
+
const utf8 = (s) => new TextEncoder().encode(String(s));
|
|
83
|
+
const concat = (...arrays) => {
|
|
84
|
+
const total = arrays.reduce((n, a) => n + a.length, 0);
|
|
85
|
+
const out = new Uint8Array(total);
|
|
86
|
+
let at = 0;
|
|
87
|
+
for (const a of arrays) { out.set(a, at); at += a.length; }
|
|
88
|
+
return out;
|
|
89
|
+
};
|
|
90
|
+
const hexToBytes = (hex) => {
|
|
91
|
+
const h = String(hex).replace(/^0x/, "");
|
|
92
|
+
if (h.length % 2) boom("eip712-odd-hex");
|
|
93
|
+
const out = new Uint8Array(h.length / 2);
|
|
94
|
+
for (let i = 0; i < out.length; i++) {
|
|
95
|
+
const byte = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
96
|
+
if (Number.isNaN(byte)) boom("eip712-bad-hex");
|
|
97
|
+
out[i] = byte;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
};
|
|
101
|
+
const bytesToHex = (bytes) => {
|
|
102
|
+
let s = "0x";
|
|
103
|
+
for (const b of bytes) s += b.toString(16).padStart(2, "0");
|
|
104
|
+
return s;
|
|
105
|
+
};
|
|
106
|
+
const asBigInt = (v) => {
|
|
107
|
+
if (typeof v === "bigint") return v;
|
|
108
|
+
const s = String(v === undefined || v === null ? "" : v).trim();
|
|
109
|
+
if (/^0x[0-9a-fA-F]+$/.test(s)) return BigInt(s);
|
|
110
|
+
if (/^[0-9]+$/.test(s)) return BigInt(s);
|
|
111
|
+
boom("eip712-bad-number");
|
|
112
|
+
};
|
|
113
|
+
const wordUint = (v, bits) => {
|
|
114
|
+
const n = asBigInt(v);
|
|
115
|
+
if (n < 0n) boom("eip712-negative");
|
|
116
|
+
if (n >= (1n << BigInt(bits))) boom("eip712-overflow");
|
|
117
|
+
const out = new Uint8Array(32);
|
|
118
|
+
let x = n;
|
|
119
|
+
for (let i = 31; i >= 0 && x > 0n; i--) { out[i] = Number(x & 0xffn); x >>= 8n; }
|
|
120
|
+
return out;
|
|
121
|
+
};
|
|
122
|
+
// FORM IS NORMALISED, NOT DEMANDED. The quote carries ed25519 points WITHOUT the 0x prefix (that is how the
|
|
123
|
+
// node writes them, see rfq/orderAddress.mjs), while the contract returns them WITH one. Both are the same
|
|
124
|
+
// bytes, so both are accepted and encoded identically - otherwise the node could not sign its own quote.
|
|
125
|
+
const wordAddress = (v) => {
|
|
126
|
+
const s = String(v === undefined || v === null ? "" : v).trim();
|
|
127
|
+
if (!/^(0x)?[0-9a-fA-F]{40}$/.test(s)) boom("eip712-bad-address");
|
|
128
|
+
const out = new Uint8Array(32);
|
|
129
|
+
out.set(hexToBytes(s.replace(/^0x/, "")), 12);
|
|
130
|
+
return out;
|
|
131
|
+
};
|
|
132
|
+
const wordBytes32 = (v) => {
|
|
133
|
+
const s = String(v === undefined || v === null ? "" : v).trim();
|
|
134
|
+
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(s)) boom("eip712-bad-bytes32");
|
|
135
|
+
return hexToBytes(s.replace(/^0x/, ""));
|
|
136
|
+
};
|
|
137
|
+
const encodeField = (type, value) => {
|
|
138
|
+
if (type === "address") return wordAddress(value);
|
|
139
|
+
if (type === "bytes32") return wordBytes32(value);
|
|
140
|
+
if (type === "uint64") return wordUint(value, 64);
|
|
141
|
+
if (type === "uint256") return wordUint(value, 256);
|
|
142
|
+
boom("eip712-bad-type");
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// The fields a signed set cannot do without. An absent field is not "probably zero": the caller must refuse,
|
|
146
|
+
// not verify a partial set (that is the rule the old signatureVerified witness carried).
|
|
147
|
+
export const quoteMissingFields = (quote) => {
|
|
148
|
+
const missing = [];
|
|
149
|
+
for (const [name] of ORDER_QUOTE_FIELDS) {
|
|
150
|
+
const v = quote ? quote[name] : undefined;
|
|
151
|
+
if (v === undefined || v === null || v === "") missing.push(name);
|
|
152
|
+
}
|
|
153
|
+
if (quote && (quote.factory === undefined || quote.factory === null || quote.factory === "")) missing.push("factory");
|
|
154
|
+
if (quote && (quote.signature === undefined || quote.signature === null || quote.signature === "")) missing.push("signature");
|
|
155
|
+
return missing;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// The EIP-712 domain. chainId and verifyingContract are taken LIVE from the quote, not cached: the digest must
|
|
159
|
+
// change when either does, otherwise a quote could move between networks or contracts.
|
|
160
|
+
export function quoteDomainSeparator({ crypto, quote }) {
|
|
161
|
+
return crypto.keccak256(concat(
|
|
162
|
+
crypto.keccak256(utf8(EIP712_DOMAIN_TYPE)),
|
|
163
|
+
crypto.keccak256(utf8(ORDER_QUOTE_DOMAIN_NAME)),
|
|
164
|
+
crypto.keccak256(utf8(ORDER_QUOTE_DOMAIN_VERSION)),
|
|
165
|
+
wordUint(quote.chainId, 256),
|
|
166
|
+
wordAddress(quote.factory),
|
|
167
|
+
));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function quoteStructHash({ crypto, quote }) {
|
|
171
|
+
const parts = [crypto.keccak256(utf8(ORDER_QUOTE_TYPE))];
|
|
172
|
+
for (const [name, type] of ORDER_QUOTE_FIELDS) parts.push(encodeField(type, quote ? quote[name] : undefined));
|
|
173
|
+
return crypto.keccak256(concat(...parts));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The digest the wallet key signs. Anything that changes here changes the recovered address.
|
|
177
|
+
export function quoteDigest({ crypto, quote }) {
|
|
178
|
+
return crypto.keccak256(concat(Uint8Array.from([0x19, 0x01]), quoteDomainSeparator({ crypto, quote }), quoteStructHash({ crypto, quote })));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Signature on the wire: r || s || v with v in {27, 28} - the shape ecrecover (and the registry contract)
|
|
182
|
+
// expects. The curve library returns [recovery, r, s], so the order is normalised here once, not at each call.
|
|
183
|
+
export function signQuoteDigest({ crypto, digest, privateKey }) {
|
|
184
|
+
const sig = crypto.secp256k1.sign(digest, hexToBytes(privateKey), { format: "recovered", prehash: false });
|
|
185
|
+
if (sig.length !== 65) boom("eip712-bad-signature");
|
|
186
|
+
const recovery = Number(sig[0]);
|
|
187
|
+
if (recovery !== 0 && recovery !== 1) boom("eip712-bad-recovery");
|
|
188
|
+
const out = new Uint8Array(65);
|
|
189
|
+
out.set(sig.slice(1, 33), 0);
|
|
190
|
+
out.set(sig.slice(33, 65), 32);
|
|
191
|
+
out[64] = 27 + recovery;
|
|
192
|
+
return bytesToHex(out);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Recover the signer without knowing it beforehand. Returns a lowercase 0x address, or null when the quote is
|
|
196
|
+
// not verifiable at all (missing fields, malformed signature, unusable domain) - the caller turns that into a
|
|
197
|
+
// named refusal, never into consent.
|
|
198
|
+
export function recoverQuoteKey({ crypto, quote }) {
|
|
199
|
+
try {
|
|
200
|
+
if (quoteMissingFields(quote).length) return null;
|
|
201
|
+
const sig = hexToBytes(String(quote.signature).replace(/^0x/, ""));
|
|
202
|
+
if (sig.length !== 65) return null;
|
|
203
|
+
let v = sig[64];
|
|
204
|
+
if (v >= 27) v -= 27;
|
|
205
|
+
if (v !== 0 && v !== 1) return null;
|
|
206
|
+
const digest = quoteDigest({ crypto, quote });
|
|
207
|
+
const pub = crypto.secp256k1.Signature.fromBytes(concat(sig.slice(0, 32), sig.slice(32, 64)))
|
|
208
|
+
.addRecoveryBit(v).recoverPublicKey(digest);
|
|
209
|
+
const raw = pub.toBytes(false);
|
|
210
|
+
return bytesToHex(crypto.keccak256(raw.slice(1)).slice(12));
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|