@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,345 @@
|
|
|
1
|
+
// A TEST EVM WALLET FOR NODE: KEY FROM THE ENVIRONMENT, REFUSAL ON A PRODUCTION NETWORK.
|
|
2
|
+
//
|
|
3
|
+
// WHY A SEPARATE ADAPTER. The SDK had only the browser wallet (evm-wallet.mjs): it receives the page's ready-made
|
|
4
|
+
// EIP-1193 provider and signs via eth_sendTransaction - that is, the person's wallet does the signing. From Node
|
|
5
|
+
// there is nothing to sign with, and a live run on a local chain was impossible: the SDK refused at the very first
|
|
6
|
+
// step that needed a signature. This file closes exactly that hole - and only it.
|
|
7
|
+
//
|
|
8
|
+
// THIS IS A TOOL OF THE TEST CONTOUR, NOT A USER WALLET. The name shows it (node-test-*), and from that come two
|
|
9
|
+
// rules that are not up for discussion here:
|
|
10
|
+
//
|
|
11
|
+
// 1) THE KEY COMES ONLY FROM THE ENVIRONMENT OR FROM A FILE, the path to which also lives in the environment.
|
|
12
|
+
// This module does NOT read command-line arguments AT ALL: a key in argv is visible in the shell history and
|
|
13
|
+
// in the process list, that is, it leaks before anything is signed. The key's value is never printed: only the
|
|
14
|
+
// address, transaction hashes and amounts go out.
|
|
15
|
+
//
|
|
16
|
+
// 2) ON A PRODUCTION NETWORK THE ADAPTER DOES NOT SIGN. The rule is not its own: it is already declared by the
|
|
17
|
+
// engine (sdk/src/swap-flow.mjs: production = chain.escrow.mode === "live" && !chain.testnet) and the page
|
|
18
|
+
// itself uses the same marker. A private "production network" marker would silently drift from the engine, and
|
|
19
|
+
// then the test key would sign real money. So the rule is taken from the network registry, not written here.
|
|
20
|
+
//
|
|
21
|
+
// DEPENDENCIES. The signature is assembled the same way as the provider node's (rfq/evmSend.mjs): @noble/curves
|
|
22
|
+
// (secp256k1) and keccak256 - they are in the repository's devDependencies and already used in the project. There
|
|
23
|
+
// is neither DOM nor node:fs at the top level here: the adapters directory is the only place where the environment
|
|
24
|
+
// is allowed, but a static node:fs import would break the package's browser build, so the file is read via a
|
|
25
|
+
// dynamic import and only inside the key-loading function.
|
|
26
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
27
|
+
import keccakPkg from "keccak256";
|
|
28
|
+
import { SdkError } from "../errors.mjs";
|
|
29
|
+
import { config as engineConfig } from "../engine.mjs";
|
|
30
|
+
|
|
31
|
+
const keccak256 = (bytes) => new Uint8Array(keccakPkg(Buffer.from(bytes)));
|
|
32
|
+
|
|
33
|
+
// THE ENVIRONMENT VARIABLE NAMES - HERE, IN ONE PLACE: the run and the adapter must look at the same name,
|
|
34
|
+
// otherwise the key "is there" but the adapter does not see it.
|
|
35
|
+
export const KEY_ENV = "ARRAKIS_TEST_EVM_KEY";
|
|
36
|
+
export const KEY_FILE_ENV = "ARRAKIS_TEST_EVM_KEY_FILE";
|
|
37
|
+
export const RPC_ENV = "ARRAKIS_TEST_EVM_RPC";
|
|
38
|
+
export const CHAIN_ENV = "ARRAKIS_TEST_EVM_CHAIN";
|
|
39
|
+
export const CLAIMER_ENV = "ARRAKIS_TEST_EVM_CLAIMER";
|
|
40
|
+
|
|
41
|
+
// AN EXPLICIT WHITELIST OF TEST NETWORKS - HERE, NOT IN THE ENGINE, and WHY. The engine's "production network" rule
|
|
42
|
+
// (sdk/src/swap-flow.mjs: escrow.mode === "live" && !testnet) today does not trigger ON ANY recorded network: for
|
|
43
|
+
// the registry's production networks the escrow is marked "simulated", that is, by the letter of the rule they are
|
|
44
|
+
// not production. So the test key would also sign on mainnet - the engine would not notice, and the engine's rule
|
|
45
|
+
// must not be changed (it is shared by the page and all legs). So the "signing is ALLOWED here" marker is set
|
|
46
|
+
// separately and EXPLICITLY: only what is listed, and nothing more. First - the local anvil by chainId 31337.
|
|
47
|
+
export const TEST_CHAIN_IDS = Object.freeze([31337]);
|
|
48
|
+
|
|
49
|
+
const envOf = (name) => {
|
|
50
|
+
const box = typeof process !== "undefined" && process ? process.env : null;
|
|
51
|
+
const value = box ? box[name] : undefined;
|
|
52
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
53
|
+
};
|
|
54
|
+
export const envValue = envOf;
|
|
55
|
+
|
|
56
|
+
// --- ENCODINGS -----------------------------------------------------------------------------------------
|
|
57
|
+
const hexToBytes = (hex) => {
|
|
58
|
+
const h = String(hex || "").replace(/^0x/, "");
|
|
59
|
+
if (h.length === 0) return new Uint8Array(0);
|
|
60
|
+
if (h.length % 2) throw new SdkError("bad-input", { field: "hex", why: "odd" });
|
|
61
|
+
if (!/^[0-9a-fA-F]+$/.test(h)) throw new SdkError("bad-input", { field: "hex", why: "not-hex" });
|
|
62
|
+
return Uint8Array.from(Buffer.from(h, "hex"));
|
|
63
|
+
};
|
|
64
|
+
const bytesToHex = (bytes) => "0x" + Buffer.from(bytes).toString("hex");
|
|
65
|
+
// Integers in RLP are encoded MINIMALLY, and zero is the empty string. A non-canonical encoding (an extra leading
|
|
66
|
+
// zero) makes the node reject the WHOLE transaction, even though the signature is valid.
|
|
67
|
+
const numToBytes = (value) => {
|
|
68
|
+
const v = BigInt(value);
|
|
69
|
+
if (v < 0n) throw new SdkError("bad-input", { field: "value", why: "negative" });
|
|
70
|
+
if (v === 0n) return new Uint8Array(0);
|
|
71
|
+
let hex = v.toString(16);
|
|
72
|
+
if (hex.length % 2) hex = "0" + hex;
|
|
73
|
+
return hexToBytes(hex);
|
|
74
|
+
};
|
|
75
|
+
const intFromBytes = (bytes) => numToBytes(BigInt(bytesToHex(bytes)));
|
|
76
|
+
const encodeLength = (len, offset) => {
|
|
77
|
+
if (len < 56) return Uint8Array.from([offset + len]);
|
|
78
|
+
const lenBytes = numToBytes(len);
|
|
79
|
+
return Uint8Array.from([offset + 55 + lenBytes.length, ...lenBytes]);
|
|
80
|
+
};
|
|
81
|
+
const rlpItem = (item) => {
|
|
82
|
+
if (item instanceof Uint8Array) {
|
|
83
|
+
if (item.length === 1 && item[0] < 0x80) return item;
|
|
84
|
+
return Uint8Array.from([...encodeLength(item.length, 0x80), ...item]);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(item)) {
|
|
87
|
+
const body = item.flatMap((x) => [...rlpItem(x)]);
|
|
88
|
+
return Uint8Array.from([...encodeLength(body.length, 0xc0), ...body]);
|
|
89
|
+
}
|
|
90
|
+
throw new SdkError("bad-input", { field: "rlp" });
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** The account address for a private key: the last 20 bytes of the keccak256 of the UNcompressed point. */
|
|
94
|
+
export function addressOfPrivateKey(privateKeyHex) {
|
|
95
|
+
const pub = secp256k1.getPublicKey(hexToBytes(privateKeyHex), false);
|
|
96
|
+
return "0x" + Buffer.from(keccak256(pub.slice(1))).toString("hex").slice(-40);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* SIGNING A LEGACY TRANSACTION (type 0, EIP-155). Legacy specifically, not 1559: it has no dependency on fee
|
|
101
|
+
* history, and a local chain accepts it everywhere. Three things that are easy to get silently wrong (all three
|
|
102
|
+
* were already caught in rfq/evmSend.mjs by checking against the Foundry reference, and all three are repeated
|
|
103
|
+
* here deliberately):
|
|
104
|
+
* prehash: false - Ethereum signs the ALREADY computed keccak hash; the library by default hashes the passed
|
|
105
|
+
* data once more, and the signature becomes valid in form and completely foreign;
|
|
106
|
+
* recovery is the FIRST byte of the 65-byte signature (not the last);
|
|
107
|
+
* r and s go into RLP with minimal encoding, not full 32 bytes.
|
|
108
|
+
*/
|
|
109
|
+
export function signLegacyTx({ privateKey, nonce, gasPrice, gasLimit, to, data = "0x", value = 0, chainId }) {
|
|
110
|
+
const signing = rlpItem([
|
|
111
|
+
numToBytes(nonce), numToBytes(gasPrice), numToBytes(gasLimit), hexToBytes(to),
|
|
112
|
+
numToBytes(value), hexToBytes(data), numToBytes(chainId), new Uint8Array(0), new Uint8Array(0),
|
|
113
|
+
]);
|
|
114
|
+
const digest = keccak256(signing);
|
|
115
|
+
const sig = secp256k1.sign(digest, hexToBytes(privateKey), { format: "recovered", prehash: false });
|
|
116
|
+
if (sig.length !== 65) throw new SdkError("bad-input", { field: "signature", got: sig.length });
|
|
117
|
+
const recovery = Number(sig[0]);
|
|
118
|
+
if (recovery !== 0 && recovery !== 1) throw new SdkError("bad-input", { field: "signature", why: "recovery" });
|
|
119
|
+
const r = intFromBytes(sig.slice(1, 33));
|
|
120
|
+
const s = intFromBytes(sig.slice(33, 65));
|
|
121
|
+
const v = BigInt(recovery) + 35n + BigInt(chainId) * 2n;
|
|
122
|
+
const raw = rlpItem([
|
|
123
|
+
numToBytes(nonce), numToBytes(gasPrice), numToBytes(gasLimit), hexToBytes(to),
|
|
124
|
+
numToBytes(value), hexToBytes(data), numToBytes(v), r, s,
|
|
125
|
+
]);
|
|
126
|
+
return { raw: bytesToHex(raw), hash: bytesToHex(keccak256(raw)) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- LOADING THE KEY ------------------------------------------------------------------------------------
|
|
130
|
+
/**
|
|
131
|
+
* The key from the environment or from a file. THE LOADER LIVES NEXT TO THE ADAPTER ON PURPOSE: it is the only
|
|
132
|
+
* entry point for the key, and if the caller could pass the key as an argument, the rule "never from argv" would
|
|
133
|
+
* rest on an honest word.
|
|
134
|
+
* Returns { privateKey, address, from } - from names the source ("env" or "file"), the key itself goes out only in
|
|
135
|
+
* this field and is never printed anywhere.
|
|
136
|
+
*/
|
|
137
|
+
export async function testPrivateKeyFromEnv() {
|
|
138
|
+
let raw = envOf(KEY_ENV);
|
|
139
|
+
let from = "env";
|
|
140
|
+
if (!raw) {
|
|
141
|
+
const file = envOf(KEY_FILE_ENV);
|
|
142
|
+
if (!file) throw new SdkError("bad-input", { field: "privateKey", missing: [KEY_ENV, KEY_FILE_ENV] });
|
|
143
|
+
// DYNAMIC IMPORT: a static node:fs would end up in the package's browser build, though it is needed only here.
|
|
144
|
+
const fs = await import("node:fs");
|
|
145
|
+
raw = String(fs.readFileSync(file, "utf8")).trim().split(/\r?\n/)[0].trim();
|
|
146
|
+
from = "file";
|
|
147
|
+
}
|
|
148
|
+
const hex = raw.replace(/^0x/, "");
|
|
149
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hex)) throw new SdkError("bad-input", { field: "privateKey", why: "shape" });
|
|
150
|
+
return { privateKey: "0x" + hex.toLowerCase(), address: addressOfPrivateKey(hex), from };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- A READ PROVIDER ------------------------------------------------------------------------------------
|
|
154
|
+
/**
|
|
155
|
+
* An EIP-1193-compatible provider over HTTP JSON-RPC. Needed where the SDK reads the chain with the leg driver
|
|
156
|
+
* (leg.balances -> eth_getBalance/eth_call) and as the reader for the lock step. THERE IS DELIBERATELY NO SIGNING
|
|
157
|
+
* HERE: eth_sendTransaction answers with a refusal, because this adapter must sign with its own key, not the node -
|
|
158
|
+
* otherwise the key would end up with the node, and the rule "key only from the environment" would stop meaning
|
|
159
|
+
* anything.
|
|
160
|
+
*/
|
|
161
|
+
export function evmRpcProvider(url) {
|
|
162
|
+
if (typeof url !== "string" || !/^https?:\/\//.test(url)) throw new SdkError("bad-input", { field: "rpcUrl" });
|
|
163
|
+
const call = async (method, params = []) => {
|
|
164
|
+
const res = await fetch(url, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "content-type": "application/json" },
|
|
167
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
168
|
+
});
|
|
169
|
+
if (!res.ok) throw new SdkError("server-unavailable", { rpc: method, status: res.status });
|
|
170
|
+
const body = await res.json();
|
|
171
|
+
if (body && body.error) throw new SdkError("server-unavailable", { rpc: method, why: String(body.error.message || "").slice(0, 80) });
|
|
172
|
+
return body ? body.result : null;
|
|
173
|
+
};
|
|
174
|
+
const provider = {
|
|
175
|
+
url,
|
|
176
|
+
request({ method, params = [] }) {
|
|
177
|
+
if (method === "eth_sendTransaction") {
|
|
178
|
+
throw new SdkError("not-implemented", { surface: "provider.eth_sendTransaction" });
|
|
179
|
+
}
|
|
180
|
+
if (method === "eth_requestAccounts") return Promise.resolve([]);
|
|
181
|
+
return call(method, params);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
return provider;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* CONTRACT READ FOR THE LOCK SEAM: { to, data } -> hex. Exactly the shape the engine expects (www/js/evm/
|
|
189
|
+
* session.js, readContract), so it is dropped into the SDK WITHOUT an adapter. This is a CHAIN READ, not a node
|
|
190
|
+
* substitution: the calls go to the real node at the rpcUrl address.
|
|
191
|
+
*/
|
|
192
|
+
export function evmRpcReader({ url }) {
|
|
193
|
+
const provider = evmRpcProvider(url);
|
|
194
|
+
return ({ to, data } = {}) => {
|
|
195
|
+
if (!to || !data) throw new SdkError("bad-input", { field: "eth_call" });
|
|
196
|
+
return provider.request({ method: "eth_call", params: [{ to, data }, "latest"] });
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- THE PRODUCTION-NETWORK MARKER ----------------------------------------------------------------------
|
|
201
|
+
/**
|
|
202
|
+
* A PRODUCTION NETWORK - BY THE ENGINE'S RULE, NOT ITS OWN. The marker is taken from www/js/core/config.js (the
|
|
203
|
+
* testnet field) and sdk/src/swap-flow.mjs (escrow.mode === "live"): on a production network there is a live
|
|
204
|
+
* escrow and NO testnet marker. An unknown network is also considered production: there is nothing to prove it is a
|
|
205
|
+
* test one, and an error in that direction costs a test key signing over real money.
|
|
206
|
+
*/
|
|
207
|
+
export function isProductionChain(chainId) {
|
|
208
|
+
const chain = typeof engineConfig.chainByChainId === "function" ? engineConfig.chainByChainId(Number(chainId)) : null;
|
|
209
|
+
if (!chain) return { production: true, reason: "unknown-chain", chain: null };
|
|
210
|
+
const escrow = chain.escrow && typeof chain.escrow === "object" ? chain.escrow : null;
|
|
211
|
+
const production = Boolean(escrow && escrow.mode === "live" && !chain.testnet);
|
|
212
|
+
return { production, reason: production ? "production" : "testnet", chain };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- THE ADAPTER ----------------------------------------------------------------------------------------
|
|
216
|
+
/**
|
|
217
|
+
* A test EVM wallet: the same interface as the page's wallet (vm, connect, disconnect, address, chainId, subscribe,
|
|
218
|
+
* driver + send/receipt), but the signature is its own and the chain is read directly.
|
|
219
|
+
*
|
|
220
|
+
* IT ACCEPTS ONLY AN ALREADY-LOADED KEY: the loader is testPrivateKeyFromEnv above, and there is no other entry.
|
|
221
|
+
* rpcUrl and chainId come from outside, but as VALUES (not the key), and the adapter keeps no factory address at
|
|
222
|
+
* all: the addresses come from the SDK/engine's network registry, and a second source of truth about addresses must
|
|
223
|
+
* not be introduced.
|
|
224
|
+
*/
|
|
225
|
+
export function nodeTestEvmWallet({ privateKey, rpcUrl, chainId = null, id = "node-test" } = {}) {
|
|
226
|
+
const hex = String(privateKey || "").replace(/^0x/, "");
|
|
227
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hex)) throw new SdkError("bad-input", { field: "privateKey" });
|
|
228
|
+
const key = "0x" + hex.toLowerCase();
|
|
229
|
+
const owner = addressOfPrivateKey(key);
|
|
230
|
+
const provider = evmRpcProvider(rpcUrl);
|
|
231
|
+
let connected = false;
|
|
232
|
+
|
|
233
|
+
const remoteChainId = async () => Number(BigInt(await provider.request({ method: "eth_chainId" })));
|
|
234
|
+
|
|
235
|
+
// REFUSAL ON A PRODUCTION NETWORK - ONE POINT THROUGH WHICH EVERY SIGNATURE PASSES. The check stands here, not in
|
|
236
|
+
// connect(), deliberately: connect may happen once and early, while the node's network may change later (or turn
|
|
237
|
+
// out to be the wrong one at the moment of signing). We ask the network OF THE NODE right before signing.
|
|
238
|
+
const assertTestContour = async () => {
|
|
239
|
+
const remote = await remoteChainId();
|
|
240
|
+
// A NETWORK OUTSIDE THE WHITELIST IS A REFUSAL NAMING THE NETWORK, BEFORE ANY SIGNATURE. This is the FIRST line
|
|
241
|
+
// of defense and it does not depend on how the network is marked in the registry (the production marker is the
|
|
242
|
+
// second line below, and today it may not trigger).
|
|
243
|
+
if (!TEST_CHAIN_IDS.includes(Number(remote))) {
|
|
244
|
+
throw new SdkError("wrong-chain", {
|
|
245
|
+
why: "test-contour-network-not-allowed", chainId: remote, allowed: [...TEST_CHAIN_IDS],
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// THE SECOND LINE - THE ENGINE'S PRODUCTION-NETWORK RULE (not rewritten here, taken from the registry). It stays
|
|
249
|
+
// for the case where a network from the list is later marked production: then the signature still stops.
|
|
250
|
+
const view = isProductionChain(remote);
|
|
251
|
+
if (view.production) {
|
|
252
|
+
throw new SdkError("wrong-chain", {
|
|
253
|
+
why: "test-contour-refuses-live-network", chainId: remote, reason: view.reason,
|
|
254
|
+
escrowMode: view.chain && view.chain.escrow ? view.chain.escrow.mode : null,
|
|
255
|
+
testnet: view.chain ? Boolean(view.chain.testnet) : null,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (chainId !== null && Number(chainId) !== remote) {
|
|
259
|
+
throw new SdkError("wrong-chain", { got: remote, want: Number(chainId) });
|
|
260
|
+
}
|
|
261
|
+
return remote;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
vm: "evm",
|
|
266
|
+
id,
|
|
267
|
+
// The address going out is what is anyway visible in every transaction. The key is not handed out by any method.
|
|
268
|
+
async connect(options = {}) {
|
|
269
|
+
const want = options && options.chainId !== undefined && options.chainId !== null ? Number(options.chainId) : chainId;
|
|
270
|
+
const remote = await remoteChainId();
|
|
271
|
+
if (want !== null && want !== undefined && Number(want) !== remote) {
|
|
272
|
+
throw new SdkError("wrong-chain", { got: remote, want: Number(want) });
|
|
273
|
+
}
|
|
274
|
+
connected = true;
|
|
275
|
+
},
|
|
276
|
+
async disconnect() {
|
|
277
|
+
connected = false;
|
|
278
|
+
},
|
|
279
|
+
address() {
|
|
280
|
+
return owner;
|
|
281
|
+
},
|
|
282
|
+
async chainId() {
|
|
283
|
+
return remoteChainId();
|
|
284
|
+
},
|
|
285
|
+
// NO SUBSCRIPTION: a local run has no "wallet changed network" event, while a poll timer would keep the process
|
|
286
|
+
// alive after the last transaction. We return an unsubscribe function, as the adapter interface requires.
|
|
287
|
+
subscribe() {
|
|
288
|
+
return () => {};
|
|
289
|
+
},
|
|
290
|
+
driver() {
|
|
291
|
+
return provider;
|
|
292
|
+
},
|
|
293
|
+
/**
|
|
294
|
+
* SENDING: sign with its own key and eth_sendRawTransaction. The nonce is taken by pending, the gas price - from
|
|
295
|
+
* the node, the gas limit - from the caller (the lock passes its own; if it did not, we estimate with a margin).
|
|
296
|
+
* None of these numbers is invented: a guessed gas limit costs a signature, and a guessed nonce costs a lost
|
|
297
|
+
* transaction.
|
|
298
|
+
*/
|
|
299
|
+
async send({ to, data = "0x", value = 0n, gas = null }) {
|
|
300
|
+
if (!connected) throw new SdkError("wallet-not-connected");
|
|
301
|
+
if (typeof to !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(to)) throw new SdkError("bad-input", { field: "send.to" });
|
|
302
|
+
const remote = await assertTestContour();
|
|
303
|
+
const valueWei = BigInt(value === undefined || value === null ? 0 : value);
|
|
304
|
+
const nonce = Number(BigInt(await provider.request({ method: "eth_getTransactionCount", params: [owner, "pending"] })));
|
|
305
|
+
const gasPrice = BigInt(await provider.request({ method: "eth_gasPrice" }));
|
|
306
|
+
let gasLimit = gas === null || gas === undefined ? null : BigInt(gas);
|
|
307
|
+
if (gasLimit === null) {
|
|
308
|
+
const est = BigInt(await provider.request({
|
|
309
|
+
method: "eth_estimateGas",
|
|
310
|
+
params: [{ from: owner, to, value: "0x" + valueWei.toString(16), data }],
|
|
311
|
+
}));
|
|
312
|
+
gasLimit = (est * 12n) / 10n;
|
|
313
|
+
}
|
|
314
|
+
const signed = signLegacyTx({
|
|
315
|
+
privateKey: key, nonce, gasPrice, gasLimit, to, data,
|
|
316
|
+
value: valueWei, chainId: remote,
|
|
317
|
+
});
|
|
318
|
+
const hash = await provider.request({ method: "eth_sendRawTransaction", params: [signed.raw] });
|
|
319
|
+
if (typeof hash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(hash)) {
|
|
320
|
+
throw new SdkError("server-unavailable", { step: "send-raw" });
|
|
321
|
+
}
|
|
322
|
+
// THE HASH IS CHECKED AGAINST THE LOCALLY COMPUTED ONE. A discrepancy means the node accepted a transaction
|
|
323
|
+
// different from the one we signed - and that must be found out here, not from missing money.
|
|
324
|
+
if (hash.toLowerCase() !== signed.hash.toLowerCase()) {
|
|
325
|
+
throw new SdkError("contract-reverted", { step: "hash-mismatch", node: hash, local: signed.hash });
|
|
326
|
+
}
|
|
327
|
+
return hash;
|
|
328
|
+
},
|
|
329
|
+
/**
|
|
330
|
+
* THE RECEIPT: asked once and returned as is (null - not in a block yet). THE ARGUMENT SHAPE IS ACCEPTED BOTH
|
|
331
|
+
* WAYS: a hash string and an object { hash }. This is not "just in case": the adapter contract declares
|
|
332
|
+
* receipt(txHash: string), while the lock path in the SDK calls the receipt as receipt({ hash }) - the engine's
|
|
333
|
+
* waitReceipt passes an object (www/js/evm/funding.js), and lock.mjs passes it into the adapter without
|
|
334
|
+
* normalisation. A strict adapter answers bad-input to this, the refusal is swallowed by the wait, and the
|
|
335
|
+
* escrow address is silently replaced with the PREDICTED one - that is, the "address from the receipt" check
|
|
336
|
+
* stops working without saying so.
|
|
337
|
+
*/
|
|
338
|
+
async receipt(txHash) {
|
|
339
|
+
const hash = typeof txHash === "string" ? txHash : (txHash && typeof txHash.hash === "string" ? txHash.hash : null);
|
|
340
|
+
if (!hash) throw new SdkError("bad-input", { field: "receipt.txHash" });
|
|
341
|
+
const r = await provider.request({ method: "eth_getTransactionReceipt", params: [hash] });
|
|
342
|
+
return r || null;
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
package/src/codes.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// CODE TABLES. The single place where the codes are named: they are read by the SDK, by the check and by the README.
|
|
2
|
+
//
|
|
3
|
+
// Why codes and not texts: wording, translations and tone are the interface's business. The core must name a
|
|
4
|
+
// state so that it can be translated and coloured without parsing a string (issue #32, requirement 2).
|
|
5
|
+
|
|
6
|
+
export const STEP_CODES = [
|
|
7
|
+
"awaiting_funding",
|
|
8
|
+
"funded",
|
|
9
|
+
"maker_locking",
|
|
10
|
+
"xmr_locked",
|
|
11
|
+
"ready",
|
|
12
|
+
"claimed",
|
|
13
|
+
"swept",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const TERMINAL_CODES = ["success", "refunded_eth", "xmr_returned", "closed"];
|
|
17
|
+
|
|
18
|
+
export const CHECK_CODES = [
|
|
19
|
+
"balance",
|
|
20
|
+
"gas",
|
|
21
|
+
"allowance",
|
|
22
|
+
"consent",
|
|
23
|
+
"network",
|
|
24
|
+
"settlement-network",
|
|
25
|
+
"stale",
|
|
26
|
+
"deadline",
|
|
27
|
+
"range",
|
|
28
|
+
"size",
|
|
29
|
+
"escrow-state",
|
|
30
|
+
"escrow-points",
|
|
31
|
+
"counterparty",
|
|
32
|
+
"slots",
|
|
33
|
+
"unchecked",
|
|
34
|
+
"unknown",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const ERROR_CODES = [
|
|
38
|
+
"not-implemented",
|
|
39
|
+
"bad-input",
|
|
40
|
+
"wallet-not-connected",
|
|
41
|
+
"wallet-rejected",
|
|
42
|
+
"wrong-chain",
|
|
43
|
+
"quote-unavailable",
|
|
44
|
+
"quote-refused",
|
|
45
|
+
"quote-stale",
|
|
46
|
+
// THE ORDER-QUOTE SIGNATURE (EIP-712, quote format v7). Four different refusals, because the interface owes the
|
|
47
|
+
// person a different piece of advice for each:
|
|
48
|
+
// quote-signature-invalid - the signature does not recover to any address: it is malformed or made for other bytes;
|
|
49
|
+
// quote-key-mismatch - the signature is valid but belongs to a key other than the one the quote names;
|
|
50
|
+
// quote-field-unbound - a field the signature must cover is absent, so nothing can be verified;
|
|
51
|
+
// quote-expired - the quote's own validUntil has passed.
|
|
52
|
+
"quote-signature-invalid",
|
|
53
|
+
"quote-key-mismatch",
|
|
54
|
+
"quote-field-unbound",
|
|
55
|
+
"quote-expired",
|
|
56
|
+
// WHO OWNS THE KEY - ASKED OF THE CHAIN (maker registry, providerOf). The signature alone proves only that
|
|
57
|
+
// SOME key signed; these four name what the registry (and the interface's policy) said:
|
|
58
|
+
// quote-registry-unchecked - there was no way to ask the chain, so the claim stays unproven;
|
|
59
|
+
// quote-key-unknown - the registry has no provider for this key (nobody owns it);
|
|
60
|
+
// quote-provider-mismatch - the registry maps the key to a DIFFERENT provider than the quote names;
|
|
61
|
+
// quote-provider-not-allowed - the chain confirms the provider, but this interface does not admit it (policy).
|
|
62
|
+
"quote-registry-unchecked",
|
|
63
|
+
"quote-key-unknown",
|
|
64
|
+
"quote-provider-mismatch",
|
|
65
|
+
"quote-provider-not-allowed",
|
|
66
|
+
// THE FACTORY THAT WILL VERIFY THE QUOTE AND WHERE ITS FEE GOES (issue #103). The factory is taken from
|
|
67
|
+
// the provider's REGISTRY RECORD and its CODE is checked against the interface's known builds (extcodehash):
|
|
68
|
+
// quote-factory-unchecked - no way to read the factory / its code, so the claim stays unproven;
|
|
69
|
+
// quote-factory-unknown - the record names no factory (zero);
|
|
70
|
+
// quote-factory-mismatch - the signed quote names a DIFFERENT factory than the record;
|
|
71
|
+
// quote-factory-code-unknown - the factory's code is not a build this interface knows.
|
|
72
|
+
"quote-factory-unchecked",
|
|
73
|
+
"quote-factory-unknown",
|
|
74
|
+
"quote-factory-mismatch",
|
|
75
|
+
"quote-factory-code-unknown",
|
|
76
|
+
"recovery-declined",
|
|
77
|
+
"recovery-failed",
|
|
78
|
+
"storage-unavailable",
|
|
79
|
+
"server-unavailable",
|
|
80
|
+
// A REFUSAL BY OUR SERVICE, AND IT DELIBERATELY DIFFERS FROM A CONNECTION FAILURE: the connection never got
|
|
81
|
+
// through - server-unavailable, while the service answered and did not accept - server-refused (with the
|
|
82
|
+
// status and its own reason). The advice to the interface differs: "retry" versus "investigate".
|
|
83
|
+
"server-refused",
|
|
84
|
+
// A TIMEOUT WAITING FOR XMR TO ARRIVE - a separate code. "We did not wait long enough" is a state of the
|
|
85
|
+
// money, not a breakdown and not an empty response: mixing it with other refusals, the interface could not
|
|
86
|
+
// tell "not arrived yet" from "broken".
|
|
87
|
+
"xmr-timeout",
|
|
88
|
+
// XMR IS ON THE ADDRESS BUT BARRED BY THE TRANSACTION'S unlock_time (issue #84). Distinct from `xmr-timeout`:
|
|
89
|
+
// that one means "nothing arrived within the wait", this one means "money is there and CANNOT be spent until a
|
|
90
|
+
// height or a date". Marking ready under it would hand the escrow to the maker for funds he cannot be paid from -
|
|
91
|
+
// so the core refuses with its own code, and the `until` boundary travels in params.
|
|
92
|
+
"xmr-locked",
|
|
93
|
+
// THE XMR ARRIVAL CHECK ON TWO NODES (issue #85). The decision "the XMR arrived" is made by the client, and a
|
|
94
|
+
// single node is a single truth that can lie. Each code names a different reason the button must stay closed:
|
|
95
|
+
// xmr-nodes-disagree - a synced node has no such transaction in that block or a different block hash there;
|
|
96
|
+
// xmr-no-nodes - every node in the list is unreachable, so nothing can be confirmed;
|
|
97
|
+
// xmr-underpaid - the arrived amount is less than the expected one;
|
|
98
|
+
// xmr-too-late - less time remains before the ready deadline than the required confirmations take.
|
|
99
|
+
"xmr-nodes-disagree",
|
|
100
|
+
"xmr-no-nodes",
|
|
101
|
+
"xmr-underpaid",
|
|
102
|
+
"xmr-too-late",
|
|
103
|
+
"contract-reverted",
|
|
104
|
+
"insufficient-funds",
|
|
105
|
+
// CLAIM GAS FOR THE XMR SELLER (issue #78). The seller comes from Monero and has no coin on the EVM side to pay
|
|
106
|
+
// for the claim, so the maker may send it - but only when asked for, and only after the core has CONFIRMED the
|
|
107
|
+
// gas actually arrived. The three codes name the three states the interface must not blur together:
|
|
108
|
+
// gas-refused-by-maker - the maker declines to pay the claim gas, and the refusal is shown BEFORE the deal;
|
|
109
|
+
// gas-not-arrived - the gas was ordered but the recipient's balance did not grow;
|
|
110
|
+
// gas-too-little - gas arrived, but below what a claim costs (an unmeasured requirement counts here too).
|
|
111
|
+
"gas-refused-by-maker",
|
|
112
|
+
"gas-not-arrived",
|
|
113
|
+
"gas-too-little",
|
|
114
|
+
"unknown",
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// GUARD KIND -> CHECK CODE. Kinds come from the engine (www/js/core/preSignGuards.js), codes are our public
|
|
118
|
+
// vocabulary. The table is checked by machine: every kind the engine can return must be here, otherwise the
|
|
119
|
+
// interface gets a code that is not in the contract.
|
|
120
|
+
export const KIND_TO_CHECK = {
|
|
121
|
+
// size and range
|
|
122
|
+
"in-range": "range",
|
|
123
|
+
"above-max": "range",
|
|
124
|
+
"below-min": "range",
|
|
125
|
+
"range-unstated": "range",
|
|
126
|
+
"size-unstated": "size",
|
|
127
|
+
"amount-unstated": "size",
|
|
128
|
+
"unit-mismatch": "range",
|
|
129
|
+
// money
|
|
130
|
+
"balance-enough": "balance",
|
|
131
|
+
"balance-short": "balance",
|
|
132
|
+
"balance-unchecked": "balance",
|
|
133
|
+
"gas-enough": "gas",
|
|
134
|
+
"gas-short": "gas",
|
|
135
|
+
"reserve-unstated": "gas",
|
|
136
|
+
"native-unchecked": "gas",
|
|
137
|
+
// token allowance
|
|
138
|
+
"not-needed": "allowance",
|
|
139
|
+
"allowed": "allowance",
|
|
140
|
+
"allowance-short": "allowance",
|
|
141
|
+
"allowance-unchecked": "allowance",
|
|
142
|
+
"consent-missing": "consent",
|
|
143
|
+
// network
|
|
144
|
+
"network-match": "network",
|
|
145
|
+
"network-unknown": "network",
|
|
146
|
+
"wallet-unknown": "network",
|
|
147
|
+
"wallet-network": "network",
|
|
148
|
+
"settlement-network": "settlement-network",
|
|
149
|
+
// counterparty: a stand-in chain or a stand-in counterparty in the sandbox
|
|
150
|
+
"real-provider": "counterparty",
|
|
151
|
+
"stand-in-provider": "counterparty",
|
|
152
|
+
"standin-on-production": "counterparty",
|
|
153
|
+
// quote freshness
|
|
154
|
+
"fresh": "stale",
|
|
155
|
+
"stale": "stale",
|
|
156
|
+
"no-ttl": "stale",
|
|
157
|
+
"untimed": "stale",
|
|
158
|
+
"expired": "stale",
|
|
159
|
+
// deadlines
|
|
160
|
+
"deadlines-ok": "deadline",
|
|
161
|
+
"deadlines-unstated": "deadline",
|
|
162
|
+
"ready-by-in-past": "deadline",
|
|
163
|
+
"t1-not-after-ready-by": "deadline",
|
|
164
|
+
"ready-window-too-short": "deadline",
|
|
165
|
+
"claim-window-too-short": "deadline",
|
|
166
|
+
// A DEADLINE TOO FAR AHEAD (issue #88 - a concurrent edit in this tree): the order's t1 is a year away, and the
|
|
167
|
+
// ETH cannot be returned until then. It shares the `deadline` code with the other two extremes.
|
|
168
|
+
"deadline-too-far": "deadline",
|
|
169
|
+
// escrow state
|
|
170
|
+
"new": "escrow-state",
|
|
171
|
+
"already-funded": "escrow-state",
|
|
172
|
+
"already-claimed": "escrow-state",
|
|
173
|
+
"already-refunded": "escrow-state",
|
|
174
|
+
"state-unchecked": "escrow-state",
|
|
175
|
+
"off-step": "escrow-state",
|
|
176
|
+
// points and halves
|
|
177
|
+
"points-match": "escrow-points",
|
|
178
|
+
"point-mismatch": "escrow-points",
|
|
179
|
+
"commit-mismatch": "escrow-points",
|
|
180
|
+
"points-unchecked": "escrow-points",
|
|
181
|
+
// order book slots
|
|
182
|
+
"slots-match": "slots",
|
|
183
|
+
"slots-unchecked": "slots",
|
|
184
|
+
// misc
|
|
185
|
+
"unchecked": "unchecked",
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const checkCodeFor = (kind) => KIND_TO_CHECK[kind] || "unknown";
|