@ninsei-labs/sdk 0.43.1 → 0.44.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 +61 -0
- package/README.md +1 -1
- package/engine/core/deadlineScale.js +59 -0
- package/engine/core/xmrAmount.js +51 -0
- package/engine/monero/arrivalRead.js +95 -0
- package/engine/monero/daemonAddress.js +82 -0
- package/engine/monero/nodeNetwork.js +15 -0
- package/index.d.ts +149 -0
- package/package.json +22 -3
- package/src/adapters/monero.mjs +12 -0
- package/src/codes.mjs +56 -0
- package/src/destination.mjs +46 -0
- package/src/feeEstimate.mjs +80 -0
- package/src/index.mjs +3 -3
- package/src/probes.mjs +73 -0
- package/src/provider/envFile.mjs +106 -0
- package/src/provider/identity.mjs +43 -0
- package/src/provider/sign.mjs +164 -0
- package/src/public/core.d.ts +37 -0
- package/src/public/core.mjs +16 -0
- package/src/public/order.d.ts +12 -0
- package/src/public/order.mjs +5 -0
- package/src/public/provider.d.ts +93 -0
- package/src/public/provider.mjs +39 -0
- package/src/public/xmr.d.ts +155 -0
- package/src/public/xmr.mjs +34 -0
- package/src/recovery.mjs +72 -1
- package/src/sweep-restore.mjs +130 -0
- package/src/sweep.mjs +90 -1
- package/src/version.mjs +1 -1
package/src/public/core.mjs
CHANGED
|
@@ -4,3 +4,19 @@
|
|
|
4
4
|
// consumer forced to import it from either would learn a false lesson about where it lives.
|
|
5
5
|
|
|
6
6
|
export { parseAmount } from "../../engine/core/format.js";
|
|
7
|
+
|
|
8
|
+
// XMR AMOUNTS IN ATOMIC UNITS: 12 decimals, bigint, no rounding. `formatXmr` for display and for a field,
|
|
9
|
+
// `parseXmr` for what a person typed.
|
|
10
|
+
export { formatXmr, parseXmr } from "../../engine/core/xmrAmount.js";
|
|
11
|
+
|
|
12
|
+
// SECONDS AT THE CHAIN, MILLISECONDS EVERYWHERE ELSE: the scale of a deadline, and the one conversion at the boundary.
|
|
13
|
+
export {
|
|
14
|
+
DEADLINE_SCALE,
|
|
15
|
+
SECONDS_SCALE_MAX,
|
|
16
|
+
MS_SCALE_MIN,
|
|
17
|
+
scaleOf,
|
|
18
|
+
isSecondsScale,
|
|
19
|
+
isMsScale,
|
|
20
|
+
msFromOrderSeconds,
|
|
21
|
+
assertMsScale,
|
|
22
|
+
} from "../../engine/core/deadlineScale.js";
|
package/src/public/order.d.ts
CHANGED
|
@@ -159,3 +159,15 @@ export declare function signQuoteDigest(request: {
|
|
|
159
159
|
* verified at all. Null is a refusal for the caller to name, never consent.
|
|
160
160
|
*/
|
|
161
161
|
export declare function recoverQuoteKey(request: { readonly crypto: QuoteCrypto; readonly quote: OrderQuote }): string | null;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Names of the signed-set fields the quote does not carry, bound to the SDK's own primitives: the same answer as
|
|
165
|
+
* `quoteMissingFields`, offered so a verifier does not have to pass crypto it already trusts twice.
|
|
166
|
+
*/
|
|
167
|
+
export declare function orderQuoteMissingFields(quote: Partial<OrderQuote> | null | undefined): string[];
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The address that signed the quote, recovered with the SDK's own primitives, or null when the quote cannot be
|
|
171
|
+
* verified at all. Null is a refusal for the caller to name, never consent.
|
|
172
|
+
*/
|
|
173
|
+
export declare function recoverOrderQuoteKey(quote: OrderQuote): string | null;
|
package/src/public/order.mjs
CHANGED
|
@@ -20,3 +20,8 @@ export {
|
|
|
20
20
|
recoverQuoteKey,
|
|
21
21
|
signQuoteDigest,
|
|
22
22
|
} from "../quoteEip712Spec.mjs";
|
|
23
|
+
|
|
24
|
+
// THE BOUND VERIFICATION HELPERS. The spec above is handed crypto by the caller; these two bind it to the SDK's
|
|
25
|
+
// own primitives, so a consumer verifies a maker's quote without assembling the binding itself. Verification
|
|
26
|
+
// only: the provider's signing key never enters this package.
|
|
27
|
+
export { orderQuoteMissingFields, recoverOrderQuoteKey } from "../quoteEip712.mjs";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// DECLARATIONS FOR THE `./provider` ENTRY: the node-side helpers, Node only.
|
|
2
|
+
//
|
|
3
|
+
// These declarations name what `src/public/provider.mjs` re-exports: identity, signing and env files. Every
|
|
4
|
+
// signature is copied from the modules the barrel re-exports (`src/provider/identity.mjs`, `sign.mjs`,
|
|
5
|
+
// `envFile.mjs`), and a symbol the barrel does not re-export is not declared here.
|
|
6
|
+
//
|
|
7
|
+
// NOT FOR A BROWSER. These functions use `node:crypto`, `node:fs` and `node:path`; a page must import from the
|
|
8
|
+
// browser entries instead.
|
|
9
|
+
|
|
10
|
+
/** The shape of a provider code: `lp-` and eight lowercase hex digits, derived from the provider's address. */
|
|
11
|
+
export declare const PROVIDER_CODE_RE: RegExp;
|
|
12
|
+
|
|
13
|
+
/** The longest display name a provider may set. */
|
|
14
|
+
export declare const DISPLAY_NAME_MAX: number;
|
|
15
|
+
|
|
16
|
+
/** The provider code of an address: stable for the address, independent of any key. Throws on a malformed address. */
|
|
17
|
+
export declare function providerCode(address: string): string;
|
|
18
|
+
|
|
19
|
+
/** A display name the operator may keep or replace: a word and four hex digits. */
|
|
20
|
+
export declare function randomDisplayName(): string;
|
|
21
|
+
|
|
22
|
+
/** The canonical JSON a signature covers: keys sorted at every level, so two writers agree on the bytes. */
|
|
23
|
+
export declare function canonical(payload: unknown): string;
|
|
24
|
+
|
|
25
|
+
/** A signed package: the payload's fields plus the detached signature and the key identity that made it. */
|
|
26
|
+
export interface SignedPayload {
|
|
27
|
+
readonly [field: string]: unknown;
|
|
28
|
+
readonly signature: string;
|
|
29
|
+
readonly keyId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Signs the canonical form of the payload with a PEM private key. */
|
|
33
|
+
export declare function signPayload(payload: Record<string, unknown>, privateKeyPem: string, keyId?: string): SignedPayload;
|
|
34
|
+
|
|
35
|
+
/** Verifies a signed package against a PEM public key. False means refused, never "probably fine". */
|
|
36
|
+
export declare function verifyPayload(pkg: SignedPayload, publicKeyPem: string): boolean;
|
|
37
|
+
|
|
38
|
+
/** The public key file that belongs to a private key file: `x.key` becomes `x.pub`. */
|
|
39
|
+
export declare function publicKeyPath(keyFile: string): string;
|
|
40
|
+
|
|
41
|
+
/** Creates a key pair on disk and writes the public half beside it. Refuses to overwrite unless `force`. */
|
|
42
|
+
export declare function generateKeyFiles(options?: { readonly keyFile?: string; readonly force?: boolean }): {
|
|
43
|
+
readonly key?: string;
|
|
44
|
+
readonly file?: string;
|
|
45
|
+
readonly why?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** The raw seed bytes of a PEM private key, for consumers that must not keep the PEM. */
|
|
49
|
+
export declare function privateKeySeedBytes(privateKeyPem: string): Uint8Array;
|
|
50
|
+
|
|
51
|
+
/** Reads a PEM private key from a file. */
|
|
52
|
+
export declare function loadPrivateKey(keyFile: string): string;
|
|
53
|
+
|
|
54
|
+
/** The base64 SPKI form of a public key, from either a public or a private PEM. */
|
|
55
|
+
export declare function publicKeyBase64From(publicKeyPem: string): string;
|
|
56
|
+
|
|
57
|
+
/** The base64 public key that belongs to a private key. */
|
|
58
|
+
export declare function signingPublicKeyBase64(privateKeyPem: string): string;
|
|
59
|
+
|
|
60
|
+
/** The base64 public key held in a public key file, or a named reason when it is missing. */
|
|
61
|
+
export declare function publicKeyFileBase64(pubFile: string): { readonly key: string | null; readonly why?: string };
|
|
62
|
+
|
|
63
|
+
/** What the node reports about its own key: the key, its file, and whether the two halves agree. */
|
|
64
|
+
export declare function nodePublicKeyReport(options?: {
|
|
65
|
+
readonly privateKeyPem?: string | null;
|
|
66
|
+
readonly keyFile?: string | null;
|
|
67
|
+
readonly pubFile?: string | null;
|
|
68
|
+
}): { readonly key: string | null; readonly file?: string | null; readonly matches?: boolean | null; readonly why?: string };
|
|
69
|
+
|
|
70
|
+
/** The settings files looked up, in order, when none is named. */
|
|
71
|
+
export declare const ENV_CANDIDATES: readonly string[];
|
|
72
|
+
|
|
73
|
+
/** The first settings file that exists under `cwd`, or null. */
|
|
74
|
+
export declare function findEnvFile(cwd: string, candidates?: readonly string[]): string | null;
|
|
75
|
+
|
|
76
|
+
/** A Windows path found in a settings file on a non-Windows system, or null when the file is usable. */
|
|
77
|
+
export declare function foreignPathProblem(
|
|
78
|
+
file: string,
|
|
79
|
+
values: Record<string, string>,
|
|
80
|
+
platform?: string,
|
|
81
|
+
): { readonly file: string; readonly key: string; readonly value: string } | null;
|
|
82
|
+
|
|
83
|
+
/** The sentence that explains a foreign path to an operator. */
|
|
84
|
+
export declare function foreignPathMessage(problem: { readonly file: string; readonly key: string; readonly value: string }): string;
|
|
85
|
+
|
|
86
|
+
/** Reads a settings file into a plain object of its values. */
|
|
87
|
+
export declare function readEnvFile(path: string): Record<string, string>;
|
|
88
|
+
|
|
89
|
+
/** Substitutes `${STACK_DATA}` and `${ROOT}` in a setting value. */
|
|
90
|
+
export declare function substituteEnvVars(value: unknown, paths: { readonly dataDir: string; readonly root: string }): string;
|
|
91
|
+
|
|
92
|
+
/** Applies a settings file to a target object (the process environment by default) and returns the names set. */
|
|
93
|
+
export declare function applyEnvFile(path: string, target?: Record<string, string | undefined>): string[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// THE PROVIDER SIDE, NODE ONLY: identity, signing and env files for the machine that runs the node.
|
|
2
|
+
//
|
|
3
|
+
// Separate entry, like ./adapters, because these helpers reach the filesystem and node:crypto. A browser
|
|
4
|
+
// consumer must never pull them in, and a Node consumer gets them with one import.
|
|
5
|
+
//
|
|
6
|
+
// WHY THEY LIVE HERE. The demo's backend verifies a maker's quote and builds an allow list from the provider's
|
|
7
|
+
// key; the node signs tickets with the same key. One implementation in the package means the identity rule and
|
|
8
|
+
// the key-path rule are not re-derived per consumer.
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
PROVIDER_CODE_RE,
|
|
12
|
+
DISPLAY_NAME_MAX,
|
|
13
|
+
providerCode,
|
|
14
|
+
randomDisplayName,
|
|
15
|
+
} from "../provider/identity.mjs";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
canonical,
|
|
19
|
+
signPayload,
|
|
20
|
+
verifyPayload,
|
|
21
|
+
publicKeyPath,
|
|
22
|
+
generateKeyFiles,
|
|
23
|
+
privateKeySeedBytes,
|
|
24
|
+
loadPrivateKey,
|
|
25
|
+
publicKeyBase64From,
|
|
26
|
+
signingPublicKeyBase64,
|
|
27
|
+
publicKeyFileBase64,
|
|
28
|
+
nodePublicKeyReport,
|
|
29
|
+
} from "../provider/sign.mjs";
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
ENV_CANDIDATES,
|
|
33
|
+
findEnvFile,
|
|
34
|
+
foreignPathProblem,
|
|
35
|
+
foreignPathMessage,
|
|
36
|
+
readEnvFile,
|
|
37
|
+
substituteEnvVars,
|
|
38
|
+
applyEnvFile,
|
|
39
|
+
} from "../provider/envFile.mjs";
|
package/src/public/xmr.d.ts
CHANGED
|
@@ -316,3 +316,158 @@ export declare function sellWindowSec(request?: {
|
|
|
316
316
|
readonly soft?: number;
|
|
317
317
|
readonly hard?: number;
|
|
318
318
|
}): number;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* WHAT NETWORK A NODE SAYS IT IS ON, from its `get_info` answer. The wallet must be built from this value: a
|
|
322
|
+
* mainnet wallet against a stagenet node looks like a broken withdrawal. An answer that is none of `mainnet`,
|
|
323
|
+
* `stagenet`, `testnet` comes back as null - a default would build a wallet for a chain the node may not serve.
|
|
324
|
+
*/
|
|
325
|
+
export declare function networkFromNode(info: unknown): "mainnet" | "stagenet" | "testnet" | null;
|
|
326
|
+
|
|
327
|
+
/** An address read as its network and its public keys. An integrated address's payment id is not returned. */
|
|
328
|
+
export interface AddressKeys {
|
|
329
|
+
readonly network: "mainnet" | "stagenet" | "testnet" | null;
|
|
330
|
+
/** The first payload byte, kept for callers that meet a prefix the table does not know. */
|
|
331
|
+
readonly netByte: number;
|
|
332
|
+
readonly spendPub: string;
|
|
333
|
+
readonly viewPub: string;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** AN ADDRESS -> ITS NETWORK AND PUBLIC KEYS. Throws when the text is not a Monero address. */
|
|
337
|
+
export declare function decodeAddressKeys(address: string): AddressKeys;
|
|
338
|
+
|
|
339
|
+
/** A SCALAR AS LITTLE-ENDIAN HEX, low byte first - the form the daemon expects. */
|
|
340
|
+
export declare function scalarToLeHex(scalar: bigint | number | string, bytes?: number): string;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* WHETHER A VIEW KEY IS THE ONE THE ADDRESS NAMES: true / false, or null when there is no public key to compare.
|
|
344
|
+
* `halves` is what `createHalves` returns, so the comparison uses the same arithmetic as the address build.
|
|
345
|
+
*/
|
|
346
|
+
export declare function viewKeyMatchesAddress(
|
|
347
|
+
viewScalar: bigint | number | string,
|
|
348
|
+
expectedViewPubHex: string,
|
|
349
|
+
halves: { publicHalves(scalar: bigint | number | string): { ed: string } },
|
|
350
|
+
): boolean | null;
|
|
351
|
+
|
|
352
|
+
/** A boundary up to this value is a block height; from it on, a unix time. */
|
|
353
|
+
export declare const UNLOCK_TIME_BLOCK_MAX: number;
|
|
354
|
+
|
|
355
|
+
export interface UnlockState {
|
|
356
|
+
readonly code: "unlocked" | "locked_height" | "locked_time";
|
|
357
|
+
readonly locked: boolean;
|
|
358
|
+
readonly untilHeight: number | null;
|
|
359
|
+
readonly untilTime: number | null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* WHETHER A COIN IS SPENDABLE. A height that is unknown does not open the coin: without the height there is no
|
|
364
|
+
* fact that would. `nowSec` defaults to the wall clock.
|
|
365
|
+
*/
|
|
366
|
+
export declare function unlockStateOf(
|
|
367
|
+
unlockTime: number | bigint | string,
|
|
368
|
+
options?: { height?: number | null; nowSec?: number | null },
|
|
369
|
+
): UnlockState;
|
|
370
|
+
|
|
371
|
+
/** The names that count as loopback: 127.0.0.1, ::1, localhost - not all of 127.0.0.0/8. */
|
|
372
|
+
export declare const LOOPBACK_HOSTS: ReadonlySet<string>;
|
|
373
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
374
|
+
|
|
375
|
+
/** An address read as scheme and host, or null when the text is not a URL. */
|
|
376
|
+
export declare function hostOfUrl(url: string): { scheme: string; host: string } | null;
|
|
377
|
+
|
|
378
|
+
export type DaemonCode = "daemon-not-https" | "daemon-address-unreadable" | "daemon-scheme-unknown";
|
|
379
|
+
export declare const DAEMON_CODES: Readonly<{ notHttps: DaemonCode; unreadable: DaemonCode; schemeUnknown: DaemonCode }>;
|
|
380
|
+
export declare const DAEMON_PLAIN_HTTP_NETWORKS: readonly string[];
|
|
381
|
+
export declare const DAEMON_NETWORK_MAINNET: string;
|
|
382
|
+
|
|
383
|
+
/** A setting in one shape: 0/false/no/off switches it off; anything else, a typo included, leaves it on. */
|
|
384
|
+
export declare function plainLoopbackAllowed(raw: unknown): boolean;
|
|
385
|
+
|
|
386
|
+
export interface DaemonAddressJudgement {
|
|
387
|
+
readonly ok: boolean;
|
|
388
|
+
/** False for an empty value: nothing to judge, not a refusal. */
|
|
389
|
+
readonly configured: boolean;
|
|
390
|
+
readonly scheme: string | null;
|
|
391
|
+
readonly host: string | null;
|
|
392
|
+
readonly loopback: boolean;
|
|
393
|
+
readonly plainLoopbackAllowed: boolean;
|
|
394
|
+
/** True when a remote plain-http daemon was accepted because the network is stagenet or testnet. */
|
|
395
|
+
readonly plainHttpAccepted: boolean;
|
|
396
|
+
readonly network: string;
|
|
397
|
+
readonly code: DaemonCode | null;
|
|
398
|
+
readonly reason: string | null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* MAY THIS DAEMON ADDRESS BE USED. https anywhere; http on loopback; http to a remote host only on stagenet and
|
|
403
|
+
* testnet. An unknown network is judged as mainnet.
|
|
404
|
+
*/
|
|
405
|
+
export declare function judgeDaemonAddress(
|
|
406
|
+
raw: string | null | undefined,
|
|
407
|
+
options?: { network?: string; allowPlainLoopback?: boolean },
|
|
408
|
+
): DaemonAddressJudgement;
|
|
409
|
+
|
|
410
|
+
/** A per-KB price -> the fee for a transaction of `bytes`, rounded UP. Throws on a non-integer or a negative input. */
|
|
411
|
+
export declare function feeForBytes(feePerKB: bigint | number | string, bytes: bigint | number | string): string;
|
|
412
|
+
|
|
413
|
+
export interface FeeEstimateOk {
|
|
414
|
+
readonly ok: true;
|
|
415
|
+
readonly url: string;
|
|
416
|
+
/** The price per kilobyte for the next block, in atomic units, or null when the daemon did not send it. */
|
|
417
|
+
readonly feePerKB: string | null;
|
|
418
|
+
readonly medianFeePerKB: string | null;
|
|
419
|
+
/** The daemon's own priority list, in its order, or null. */
|
|
420
|
+
readonly perPriority: readonly string[] | null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface FeeEstimateRefusal {
|
|
424
|
+
readonly ok: false;
|
|
425
|
+
/** no-url | no-fetch | timeout | unreachable | http | not-json | rpc | no-estimate */
|
|
426
|
+
readonly why: string;
|
|
427
|
+
readonly status?: number | null;
|
|
428
|
+
readonly rpc?: number | null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* ASK A NODE WHAT A TRANSACTION WILL COST (`get_fee_estimate`). The exact sum needs the assembled transaction, so
|
|
433
|
+
* this is a price per kilobyte; `feeForBytes` is the only arithmetic that turns it into a fee. Never throws.
|
|
434
|
+
*/
|
|
435
|
+
export declare function moneroFeeEstimate(options: {
|
|
436
|
+
url: string;
|
|
437
|
+
fetchImpl?: typeof fetch;
|
|
438
|
+
timeoutMs?: number;
|
|
439
|
+
}): Promise<FeeEstimateOk | FeeEstimateRefusal>;
|
|
440
|
+
|
|
441
|
+
/** The answer to a preflight: usable, or the named reason it is not. */
|
|
442
|
+
export interface NodeProbeOk {
|
|
443
|
+
readonly ok: true;
|
|
444
|
+
readonly url: string;
|
|
445
|
+
readonly height: number;
|
|
446
|
+
readonly network: "mainnet" | "stagenet" | "testnet";
|
|
447
|
+
readonly nettype: string | null;
|
|
448
|
+
readonly synchronized: boolean;
|
|
449
|
+
readonly version: string | null;
|
|
450
|
+
readonly status: string | null;
|
|
451
|
+
readonly ms: number;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export interface NodeProbeRefusal {
|
|
455
|
+
readonly ok: false;
|
|
456
|
+
/** no-url | no-fetch | timeout | unreachable | http | not-json | rpc | no-height | nettype */
|
|
457
|
+
readonly why: string;
|
|
458
|
+
readonly status?: number | null;
|
|
459
|
+
readonly nettype?: string | null;
|
|
460
|
+
readonly rpc?: number | null;
|
|
461
|
+
readonly ms?: number;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* ASK A NODE FOR `get_info`: the height, the network it names, whether it is synchronized. Never throws - the
|
|
466
|
+
* answer names the problem (`unreachable`, `timeout`, `nettype`, ...). `fetchImpl` is injected so a Node consumer
|
|
467
|
+
* or a test can pass its own; `timeoutMs` abandons the request (8 seconds by default).
|
|
468
|
+
*/
|
|
469
|
+
export declare function probeMoneroNode(options: {
|
|
470
|
+
url: string;
|
|
471
|
+
fetchImpl?: typeof fetch;
|
|
472
|
+
timeoutMs?: number;
|
|
473
|
+
}): Promise<NodeProbeOk | NodeProbeRefusal>;
|
package/src/public/xmr.mjs
CHANGED
|
@@ -27,3 +27,37 @@ export { createDleq2 } from "../../engine/atomic/dleq2.js";
|
|
|
27
27
|
export { scalarFromMoneroHex, spendKeysFromRevealedHalf } from "../../engine/monero/halvesSweep.js";
|
|
28
28
|
|
|
29
29
|
export { SELL_WINDOW, sellWindowSec } from "../../engine/core/sellWindow.js";
|
|
30
|
+
|
|
31
|
+
// WHAT NETWORK A NODE SAYS IT IS ON: the wallet must be built from that answer, and an unknown answer is null -
|
|
32
|
+
// a default here would build a wallet for a chain the node may not serve.
|
|
33
|
+
export { networkFromNode } from "../../engine/monero/nodeNetwork.js";
|
|
34
|
+
|
|
35
|
+
// READING AN ARRIVAL WITHOUT A WALLET: the address and its public keys, the little-endian scalar form the daemon
|
|
36
|
+
// expects, whether a view key matches the address, and whether the coin is still locked by unlock_time.
|
|
37
|
+
export {
|
|
38
|
+
decodeAddressKeys,
|
|
39
|
+
scalarToLeHex,
|
|
40
|
+
viewKeyMatchesAddress,
|
|
41
|
+
unlockStateOf,
|
|
42
|
+
UNLOCK_TIME_BLOCK_MAX,
|
|
43
|
+
} from "../../engine/monero/arrivalRead.js";
|
|
44
|
+
|
|
45
|
+
// WHERE A MONERO NODE MAY LIVE: https anywhere, http on loopback, http to a remote host only on stagenet/testnet -
|
|
46
|
+
// one policy for the node and for the page that lets an operator type an address.
|
|
47
|
+
export {
|
|
48
|
+
DAEMON_CODES,
|
|
49
|
+
DAEMON_NETWORK_MAINNET,
|
|
50
|
+
DAEMON_PLAIN_HTTP_NETWORKS,
|
|
51
|
+
LOOPBACK_HOSTS,
|
|
52
|
+
hostOfUrl,
|
|
53
|
+
isLoopbackHost,
|
|
54
|
+
judgeDaemonAddress,
|
|
55
|
+
plainLoopbackAllowed,
|
|
56
|
+
} from "../../engine/monero/daemonAddress.js";
|
|
57
|
+
|
|
58
|
+
// WHAT A TRANSACTION WILL COST: the daemon's price per kilobyte, by priority, and the arithmetic that turns it into
|
|
59
|
+
// a fee. The page learned the fee only after signing.
|
|
60
|
+
export { moneroFeeEstimate, feeForBytes } from "../feeEstimate.mjs";
|
|
61
|
+
|
|
62
|
+
// IS THE NODE USABLE, AND WHAT DOES IT SAY ABOUT ITSELF: one get_info request, an answer that names why not.
|
|
63
|
+
export { probeMoneroNode } from "../probes.mjs";
|
package/src/recovery.mjs
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
//
|
|
4
4
|
// The file arrives from the interface as a string or as `{ contents }`: the SDK knows neither about
|
|
5
5
|
// <input type=file> nor about buffers - that is the interface's business.
|
|
6
|
+
//
|
|
7
|
+
// THE RESTORE HEIGHT IS A RULE, NOT A LOOKUP THE PAGE PERFORMS. Two sources exist: the height written into the
|
|
8
|
+
// file when it was issued, and the height the service learned when it registered the wallet (it watches the
|
|
9
|
+
// address, so its row is often more precise). The order between them is the package's: the service's height wins
|
|
10
|
+
// when it is known, the file's is the fallback, and NO height is a named state - not a refusal, because the
|
|
11
|
+
// person can still type one.
|
|
6
12
|
import * as engine from "./engine.mjs";
|
|
7
13
|
import { SdkError } from "./errors.mjs";
|
|
8
14
|
|
|
@@ -12,7 +18,7 @@ const contentsOf = (file) => {
|
|
|
12
18
|
return null;
|
|
13
19
|
};
|
|
14
20
|
|
|
15
|
-
export function createRecovery() {
|
|
21
|
+
export function createRecovery({ http = null, config = null } = {}) {
|
|
16
22
|
return {
|
|
17
23
|
async open(file, passphrase) {
|
|
18
24
|
const contents = contentsOf(file);
|
|
@@ -36,5 +42,70 @@ export function createRecovery() {
|
|
|
36
42
|
swapId: typeof payload.swapId === "string" ? payload.swapId : null,
|
|
37
43
|
};
|
|
38
44
|
},
|
|
45
|
+
|
|
46
|
+
// WHICH HEIGHT TO SCAN FROM, AND WHERE IT CAME FROM. The page used to run this lookup itself: it read the
|
|
47
|
+
// file, then asked the service by address. Two readers of one rule is how a page and a package drift, so the
|
|
48
|
+
// lookup lives here and the answer names its source.
|
|
49
|
+
//
|
|
50
|
+
// A SERVICE THAT DOES NOT ANSWER IS NOT A REFUSAL. The file's height is still a height; with neither, the
|
|
51
|
+
// answer is `source: "none"` and the interface asks the person. The problem is a token, never a message.
|
|
52
|
+
async restoreHeight(request = {}) {
|
|
53
|
+
const contents = contentsOf(request.file);
|
|
54
|
+
if (contents === null) throw new SdkError("bad-input", { field: "file" });
|
|
55
|
+
if (typeof request.passphrase !== "string" || !request.passphrase) {
|
|
56
|
+
throw new SdkError("bad-input", { field: "passphrase" });
|
|
57
|
+
}
|
|
58
|
+
// THE ENVELOPE IS PARSED HERE: `restoreFromFile` takes the OBJECT, while the file arrives as text. Doing it
|
|
59
|
+
// in one place keeps "not JSON" a named stage instead of a library message.
|
|
60
|
+
let envelope;
|
|
61
|
+
try {
|
|
62
|
+
envelope = JSON.parse(contents);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new SdkError("recovery-failed", { stage: "parse" });
|
|
65
|
+
}
|
|
66
|
+
// ONLY THE PAYLOAD IS NEEDED, NOT A RESTORED WALLET: the height and the address live in the decrypted
|
|
67
|
+
// payload, while restoring a wallet would rebuild keys from the halves - work this answer does not use, and
|
|
68
|
+
// work that needs the browser's crypto vendor.
|
|
69
|
+
let payload;
|
|
70
|
+
try {
|
|
71
|
+
payload = await engine.recoveryFile.decryptFile(envelope, request.passphrase);
|
|
72
|
+
engine.recoveryFile.validatePayload(payload);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new SdkError("recovery-failed", { stage: "restore" });
|
|
75
|
+
}
|
|
76
|
+
const wallet = payload.moneroWallet || {};
|
|
77
|
+
const address = typeof wallet.address === "string" && wallet.address ? wallet.address : null;
|
|
78
|
+
const fileHeight = Number(wallet.restoreHeight) || 0;
|
|
79
|
+
|
|
80
|
+
let appHeight = 0;
|
|
81
|
+
let appAsked = false;
|
|
82
|
+
let appProblem = null;
|
|
83
|
+
if (http && config && address) {
|
|
84
|
+
appAsked = true;
|
|
85
|
+
try {
|
|
86
|
+
const answer = await http.json(config.route("swaps"));
|
|
87
|
+
const rows = Array.isArray(answer) ? answer : ((answer && answer.swaps) || []);
|
|
88
|
+
const row = rows.find((x) => String((x && x.address) || "") === String(address)) || null;
|
|
89
|
+
appHeight = Number(row && row.restoreHeight) || 0;
|
|
90
|
+
if (!row) appProblem = "no-row";
|
|
91
|
+
} catch {
|
|
92
|
+
appProblem = "unreachable";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const height = appHeight > 0 ? appHeight : fileHeight;
|
|
97
|
+
const source = appHeight > 0 ? "app" : (fileHeight > 0 ? "file" : "none");
|
|
98
|
+
return {
|
|
99
|
+
address,
|
|
100
|
+
height,
|
|
101
|
+
source,
|
|
102
|
+
fileHeight,
|
|
103
|
+
appHeight,
|
|
104
|
+
appAsked,
|
|
105
|
+
// Why there is no height, when there is none: a token the interface turns into words. Null when a height
|
|
106
|
+
// was found, so a reader cannot mistake "found" for "asked and failed".
|
|
107
|
+
appProblem: source === "none" ? (appProblem || "no-height") : null,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
39
110
|
};
|
|
40
111
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// RESTORING THE WITHDRAWAL WALLET: THE PASS BEFORE ANYTHING IS SIGNED.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS IS IN THE PACKAGE. The sweep page used to run this itself: set the restore height ten blocks below
|
|
4
|
+
// the one the file names, rescan, sync, read the balance, and - if the balance was non-zero while nothing was
|
|
5
|
+
// spendable - rescan the spent status and sync again. The second pass was not a guess: coins are locked for ten
|
|
6
|
+
// blocks after they arrive, and the spent status of outputs restored from a key is not known until the wallet
|
|
7
|
+
// asks the chain. Both are facts about wallet2, not about a page, and a node or a CLI needs the same pass.
|
|
8
|
+
//
|
|
9
|
+
// WHAT IS NOT HERE: creating the wallet, opening it and closing it. The wallet arrives as the same withdrawal
|
|
10
|
+
// adapter the signing step takes (`sync`, `unlockedBalance`, `driver`); who built it and where the WASM and the
|
|
11
|
+
// worker live is the caller's business.
|
|
12
|
+
//
|
|
13
|
+
// REFUSALS CARRY CODES. A missing method, a missing height and a library error all come out as `SdkError`; a
|
|
14
|
+
// low-level exception never escapes with `code: null`, because the interface has no phrase for it and a check
|
|
15
|
+
// cannot assert it.
|
|
16
|
+
import { SdkError } from "./errors.mjs";
|
|
17
|
+
|
|
18
|
+
// THE SECOND PASS IS BOUNDED: a wallet that reports a balance but no spendable coins twice is not helped by a
|
|
19
|
+
// third pass, and repeating it would only make an empty answer slower to arrive.
|
|
20
|
+
const PASSES = 2;
|
|
21
|
+
// HOW FAR BELOW THE NAMED HEIGHT THE SCAN STARTS. A gap in an arrival block is worth more than the ten blocks it
|
|
22
|
+
// costs: scanning from the exact height has already missed an arrival one block earlier.
|
|
23
|
+
const SCAN_BACK = 10;
|
|
24
|
+
|
|
25
|
+
const asAmount = (value, field) => {
|
|
26
|
+
try {
|
|
27
|
+
return BigInt(value);
|
|
28
|
+
} catch {
|
|
29
|
+
throw new SdkError("bad-input", { field });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const driverOf = (wallet) => {
|
|
34
|
+
if (!wallet || typeof wallet.driver !== "function") throw new SdkError("bad-input", { field: "wallet.driver" });
|
|
35
|
+
const driver = wallet.driver();
|
|
36
|
+
if (!driver || typeof driver !== "object") throw new SdkError("bad-input", { field: "wallet.driver" });
|
|
37
|
+
const missing = ["setRestoreHeight", "getBalance"].filter((name) => typeof driver[name] !== "function");
|
|
38
|
+
if (missing.length) throw new SdkError("bad-input", { field: "wallet.driver", missing });
|
|
39
|
+
return driver;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// A WALLET STEP THAT FAILS IS A REFUSAL WITH A CODE, never raw library text. The surface is named so a reader can
|
|
43
|
+
// tell which call went wrong.
|
|
44
|
+
const step = async (surface, fn) => {
|
|
45
|
+
try {
|
|
46
|
+
return await fn();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof SdkError) throw error;
|
|
49
|
+
throw new SdkError("wallet-rejected", { surface, why: String((error && error.message) || error) });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const heightOf = async (driver, name) => {
|
|
54
|
+
if (typeof driver[name] !== "function") return null;
|
|
55
|
+
try {
|
|
56
|
+
const value = await driver[name]();
|
|
57
|
+
const number = Number(value);
|
|
58
|
+
return Number.isFinite(number) ? number : null;
|
|
59
|
+
} catch {
|
|
60
|
+
// A missing height is not a refusal: the balance is the answer this pass exists for.
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Restore the wallet from a height and report what it holds.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} request
|
|
69
|
+
* @param {object} request.wallet the withdrawal adapter: `sync`, `unlockedBalance`, `driver`
|
|
70
|
+
* @param {number} request.fromHeight the height the file names; the scan starts a little below it
|
|
71
|
+
* @param {number} [request.scanBack] how far below, default 10
|
|
72
|
+
* @param {boolean} [request.rescanWhenLocked] run the second pass, default true
|
|
73
|
+
* @returns {Promise<{total: string, unlocked: string, scanHeight: number, walletHeight: number|null,
|
|
74
|
+
* nodeHeight: number|null, passes: number, lockedPass: boolean}>}
|
|
75
|
+
*/
|
|
76
|
+
export async function restoreWallet(request = {}) {
|
|
77
|
+
const wallet = request.wallet;
|
|
78
|
+
if (!wallet || typeof wallet !== "object") throw new SdkError("bad-input", { field: "wallet" });
|
|
79
|
+
const missing = ["sync", "unlockedBalance"].filter((name) => typeof wallet[name] !== "function");
|
|
80
|
+
if (missing.length) throw new SdkError("bad-input", { field: "wallet", missing });
|
|
81
|
+
|
|
82
|
+
const fromHeight = Number(request.fromHeight);
|
|
83
|
+
if (!Number.isFinite(fromHeight) || fromHeight <= 0) throw new SdkError("bad-input", { field: "restoreHeight" });
|
|
84
|
+
|
|
85
|
+
const scanBack = Number.isFinite(Number(request.scanBack)) && Number(request.scanBack) >= 0
|
|
86
|
+
? Math.floor(Number(request.scanBack))
|
|
87
|
+
: SCAN_BACK;
|
|
88
|
+
const scanHeight = Math.max(0, Math.floor(fromHeight) - scanBack);
|
|
89
|
+
const driver = driverOf(wallet);
|
|
90
|
+
|
|
91
|
+
await step("setRestoreHeight", () => driver.setRestoreHeight(scanHeight));
|
|
92
|
+
// A RESCAN IS REQUIRED, NOT OPTIONAL: without it the height only applies to a wallet that has not scanned yet,
|
|
93
|
+
// and a wallet restored from a key has. A driver that cannot rescan is the caller's mistake, named here.
|
|
94
|
+
if (typeof driver.rescanBlockchain !== "function") throw new SdkError("bad-input", { field: "wallet.driver.rescanBlockchain" });
|
|
95
|
+
await step("rescanBlockchain", () => driver.rescanBlockchain());
|
|
96
|
+
|
|
97
|
+
// A FAILED SYNC IS BEST-EFFORT, exactly as in the signing step: what blocks a withdrawal is the spendable
|
|
98
|
+
// remainder, and it is read below. The library throws low-level while scanning a chain it cannot read; that must
|
|
99
|
+
// not pre-empt the verdict.
|
|
100
|
+
try { await wallet.sync(); } catch { /* the remainder decides */ }
|
|
101
|
+
|
|
102
|
+
let total = asAmount(await step("getBalance", () => driver.getBalance()), "balance");
|
|
103
|
+
let unlocked = asAmount(await step("unlockedBalance", () => wallet.unlockedBalance()), "unlockedBalance");
|
|
104
|
+
let passes = 1;
|
|
105
|
+
let lockedPass = false;
|
|
106
|
+
|
|
107
|
+
// THE SECOND PASS: coins exist but none is spendable. The spent status is re-checked and the wallet is synced
|
|
108
|
+
// again; the arrival may have been locked by time rather than lost.
|
|
109
|
+
const rescanWhenLocked = request.rescanWhenLocked !== false;
|
|
110
|
+
if (rescanWhenLocked && total > 0n && unlocked === 0n && typeof driver.rescanSpent === "function" && passes < PASSES) {
|
|
111
|
+
lockedPass = true;
|
|
112
|
+
await step("rescanSpent", () => driver.rescanSpent());
|
|
113
|
+
try { await wallet.sync(); } catch { /* as above */ }
|
|
114
|
+
total = asAmount(await step("getBalance", () => driver.getBalance()), "balance");
|
|
115
|
+
unlocked = asAmount(await step("unlockedBalance", () => wallet.unlockedBalance()), "unlockedBalance");
|
|
116
|
+
passes += 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
total: total.toString(),
|
|
121
|
+
unlocked: unlocked.toString(),
|
|
122
|
+
scanHeight,
|
|
123
|
+
walletHeight: await heightOf(driver, "getHeight"),
|
|
124
|
+
nodeHeight: await heightOf(driver, "getDaemonHeight"),
|
|
125
|
+
passes,
|
|
126
|
+
// Whether the second pass RAN - not whether it helped. A reader must be able to tell "nothing was locked" from
|
|
127
|
+
// "it was locked and stayed locked".
|
|
128
|
+
lockedPass,
|
|
129
|
+
};
|
|
130
|
+
}
|