@ldgr/cli 0.2.0 → 0.2.2
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/README.md +4 -3
- package/dist/cli.js +111 -40
- package/dist/wallet-file.d.ts +32 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# @ldgr/cli
|
|
2
2
|
|
|
3
|
-
LDGR's self-custody terminal wallet. Version 0.
|
|
3
|
+
LDGR's self-custody terminal wallet. Version 0.2.0 creates or imports local wallets, lets you select between them, shows addresses and balances, sends native LDGR or a token, and persistently switches the selected wallet between mainnet and testnet.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @ldgr/cli
|
|
7
7
|
ldgr wallet create
|
|
8
|
-
ldgr wallet import
|
|
8
|
+
ldgr wallet import ~/Downloads/ldgr-recovery-key.txt
|
|
9
|
+
ldgr wallet export ~/Desktop/ldgr-recovery-key.txt
|
|
9
10
|
ldgr wallet list
|
|
10
11
|
ldgr wallet select 2
|
|
11
12
|
ldgr address
|
|
@@ -15,4 +16,4 @@ ldgr send-token t1abcdef01234567 ldgr1… 10
|
|
|
15
16
|
ldgr network set testnet
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
The default owner-only wallet store lives at `~/.ldgr/wallet.json`. On first use, an existing `~/.config/ldgr/wallet.json` store is validated, copied to the new owner-only location, and removed from its former location.
|
|
19
|
+
The default owner-only wallet store lives at `~/.ldgr/wallet.json`. On first use, an existing `~/.config/ldgr/wallet.json` store is validated, copied to the new owner-only location, and removed from its former location. At creation/import you can choose passphrase protection or an **unprotected** wallet, which follows the extension model and relies only on the local file's permissions. `wallet list` shows one-based indexes and marks the active wallet; `wallet select <index>` changes it without exposing a secret. `wallet export <path>` writes a `0600` `ldgr-recovery-key.txt` file containing the raw recovery-key value accepted by the extension; `wallet import <path>` reads the same file. Testnet prints a warning on every wallet command and only uses `https://tst.ldgr.ltd`.
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
nativeAssetDefinitionForLedger,
|
|
9
9
|
resolvePublicAssetCode
|
|
10
10
|
} from "@ldgr/protocol";
|
|
11
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
11
12
|
|
|
12
13
|
// src/transfer.ts
|
|
13
14
|
import {
|
|
@@ -49,7 +50,7 @@ async function buildTransferSubmission(input) {
|
|
|
49
50
|
|
|
50
51
|
// src/wallet-file.ts
|
|
51
52
|
import { generateLocalWallet, loadLocalWallet } from "@ldgr/client";
|
|
52
|
-
import { canonicalizeLedgerAddress as canonicalizeLedgerAddress2 } from "@ldgr/protocol";
|
|
53
|
+
import { canonicalizeLedgerAddress as canonicalizeLedgerAddress2, decodeEd25519RecoveryKey } from "@ldgr/protocol";
|
|
53
54
|
import { lstat, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
54
55
|
import { dirname, join } from "node:path";
|
|
55
56
|
var KDF_ITERATIONS = 310000;
|
|
@@ -82,11 +83,21 @@ function parseWalletEntry(value) {
|
|
|
82
83
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
83
84
|
throw new TypeError("Wallet file is not a JSON object");
|
|
84
85
|
const file = value;
|
|
85
|
-
if (file.format !== "ldgr-cli-wallet" || file.version !== 1 || file.network !== "mainnet" && file.network !== "testnet" || file.ledgerId !== 0 && file.ledgerId !== 1 || typeof file.address !== "string" || typeof file.publicKey !== "string"
|
|
86
|
+
if (file.format !== "ldgr-cli-wallet" || file.version !== 1 || file.network !== "mainnet" && file.network !== "testnet" || file.ledgerId !== 0 && file.ledgerId !== 1 || typeof file.address !== "string" || typeof file.publicKey !== "string")
|
|
86
87
|
throw new TypeError("Unsupported wallet file format");
|
|
87
88
|
if (file.network !== networkForLedger(file.ledgerId))
|
|
88
89
|
throw new TypeError("Wallet file network does not match ledgerId");
|
|
89
90
|
canonicalizeLedgerAddress2(file.address, file.ledgerId);
|
|
91
|
+
if (file.protection === "none") {
|
|
92
|
+
if (typeof file.recoveryKey !== "string")
|
|
93
|
+
throw new TypeError("Unsupported wallet file format");
|
|
94
|
+
decodeEd25519RecoveryKey(file.recoveryKey);
|
|
95
|
+
return file;
|
|
96
|
+
}
|
|
97
|
+
if (file.protection !== undefined && file.protection !== "passphrase")
|
|
98
|
+
throw new TypeError("Unsupported wallet file format");
|
|
99
|
+
if (typeof file.ciphertext !== "string" || typeof file.iv !== "string" || !file.kdf || file.kdf.name !== "PBKDF2" || file.kdf.hash !== "SHA-256" || file.kdf.iterations !== KDF_ITERATIONS || typeof file.kdf.salt !== "string")
|
|
100
|
+
throw new TypeError("Unsupported wallet file format");
|
|
90
101
|
return file;
|
|
91
102
|
}
|
|
92
103
|
function parseWalletStore(value) {
|
|
@@ -129,8 +140,24 @@ function legacyDefaultWalletPath(input = {}) {
|
|
|
129
140
|
}
|
|
130
141
|
async function createEncryptedWalletFile(input) {
|
|
131
142
|
const wallet = await generateLocalWallet({ ledgerId: input.ledgerId });
|
|
143
|
+
if (input.passphrase === undefined)
|
|
144
|
+
return createUnprotectedWallet({ wallet, recoveryKey: wallet.recoveryKey });
|
|
132
145
|
return encryptWallet({ wallet, recoveryKey: wallet.recoveryKey, passphrase: input.passphrase });
|
|
133
146
|
}
|
|
147
|
+
function createUnprotectedWallet(input) {
|
|
148
|
+
const file = {
|
|
149
|
+
format: "ldgr-cli-wallet",
|
|
150
|
+
version: 1,
|
|
151
|
+
protection: "none",
|
|
152
|
+
network: networkForLedger(input.wallet.ledgerId),
|
|
153
|
+
ledgerId: input.wallet.ledgerId,
|
|
154
|
+
address: input.wallet.address,
|
|
155
|
+
publicKey: input.wallet.publicKey,
|
|
156
|
+
recoveryKey: input.recoveryKey
|
|
157
|
+
};
|
|
158
|
+
return { file, serialized: `${JSON.stringify(file, null, 2)}
|
|
159
|
+
`, recoveryKey: input.recoveryKey };
|
|
160
|
+
}
|
|
134
161
|
async function encryptWallet(input) {
|
|
135
162
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
136
163
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
@@ -213,7 +240,17 @@ async function migrateDefaultWalletPath(input = {}) {
|
|
|
213
240
|
}
|
|
214
241
|
return { path, migrated: true };
|
|
215
242
|
}
|
|
243
|
+
async function walletFromRecoveryKey(file, recoveryKey) {
|
|
244
|
+
const wallet = await loadLocalWallet({ recoveryKey, ledgerId: file.ledgerId });
|
|
245
|
+
if (wallet.address !== file.address || wallet.publicKey !== file.publicKey)
|
|
246
|
+
throw new Error("wallet identity mismatch");
|
|
247
|
+
return { wallet, recoveryKey };
|
|
248
|
+
}
|
|
216
249
|
async function unlockWalletFile(file, passphrase) {
|
|
250
|
+
if (file.protection === "none")
|
|
251
|
+
return walletFromRecoveryKey(file, file.recoveryKey);
|
|
252
|
+
if (passphrase === undefined)
|
|
253
|
+
throw new Error("This wallet requires a passphrase");
|
|
217
254
|
try {
|
|
218
255
|
const key = await deriveEncryptionKey(passphrase, decodeBase64Url(file.kdf.salt));
|
|
219
256
|
const plaintext = new Uint8Array(await crypto.subtle.decrypt({ name: AES_GCM, iv: arrayBuffer(decodeBase64Url(file.iv)) }, key, arrayBuffer(decodeBase64Url(file.ciphertext))));
|
|
@@ -221,10 +258,7 @@ async function unlockWalletFile(file, passphrase) {
|
|
|
221
258
|
plaintext.fill(0);
|
|
222
259
|
if (typeof value.recoveryKey !== "string")
|
|
223
260
|
throw new Error("missing recovery key");
|
|
224
|
-
|
|
225
|
-
if (wallet.address !== file.address || wallet.publicKey !== file.publicKey)
|
|
226
|
-
throw new Error("wallet identity mismatch");
|
|
227
|
-
return { wallet, recoveryKey: value.recoveryKey };
|
|
261
|
+
return await walletFromRecoveryKey(file, value.recoveryKey);
|
|
228
262
|
} catch {
|
|
229
263
|
throw new Error("Could not unlock wallet: check your passphrase or wallet file");
|
|
230
264
|
}
|
|
@@ -234,6 +268,22 @@ async function readEncryptedWalletFile(path, passphrase) {
|
|
|
234
268
|
const file = store.wallets[store.activeWalletIndex];
|
|
235
269
|
return (await unlockWalletFile(file, passphrase)).wallet;
|
|
236
270
|
}
|
|
271
|
+
async function exportRecoveryKeyFile(input) {
|
|
272
|
+
const store = await readWalletStore(input.walletPath);
|
|
273
|
+
const unlocked = await unlockWalletFile(store.wallets[store.activeWalletIndex], input.passphrase);
|
|
274
|
+
await mkdir(dirname(input.backupPath), { recursive: true, mode: 448 });
|
|
275
|
+
const handle = await open(input.backupPath, "wx", 384).catch((error) => {
|
|
276
|
+
if (error.code === "EEXIST")
|
|
277
|
+
throw new Error(`Recovery-key file already exists: ${input.backupPath}`);
|
|
278
|
+
throw error;
|
|
279
|
+
});
|
|
280
|
+
try {
|
|
281
|
+
await handle.writeFile(unlocked.recoveryKey, "utf8");
|
|
282
|
+
await handle.sync();
|
|
283
|
+
} finally {
|
|
284
|
+
await handle.close();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
237
287
|
async function replaceWalletStore(path, store) {
|
|
238
288
|
await readWalletStore(path);
|
|
239
289
|
const temporaryPath = join(dirname(path), `.${crypto.randomUUID()}.wallet.tmp`);
|
|
@@ -273,8 +323,8 @@ async function addWalletFile(path, file) {
|
|
|
273
323
|
}
|
|
274
324
|
async function importEncryptedWalletFile(input) {
|
|
275
325
|
const wallet = await loadLocalWallet({ recoveryKey: input.recoveryKey, ledgerId: input.ledgerId });
|
|
276
|
-
const
|
|
277
|
-
return { file:
|
|
326
|
+
const created = input.passphrase === undefined ? createUnprotectedWallet({ wallet, recoveryKey: input.recoveryKey }) : await encryptWallet({ wallet, recoveryKey: input.recoveryKey, passphrase: input.passphrase });
|
|
327
|
+
return { file: created.file, index: await addWalletFile(input.path, created.file) };
|
|
278
328
|
}
|
|
279
329
|
async function listWalletFileMetadata(path) {
|
|
280
330
|
const store = await readWalletStore(path);
|
|
@@ -296,14 +346,14 @@ async function switchWalletNetwork(path, passphrase, ledgerId) {
|
|
|
296
346
|
return existing;
|
|
297
347
|
const unlocked = await unlockWalletFile(existing, passphrase);
|
|
298
348
|
const wallet = await loadLocalWallet({ recoveryKey: unlocked.recoveryKey, ledgerId });
|
|
299
|
-
const
|
|
349
|
+
const updated = existing.protection === "none" ? createUnprotectedWallet({ wallet, recoveryKey: unlocked.recoveryKey }) : await encryptWallet({ wallet, recoveryKey: unlocked.recoveryKey, passphrase });
|
|
300
350
|
if (store.wallets.some((entry, index) => index !== store.activeWalletIndex && entry.ledgerId === ledgerId && entry.publicKey === wallet.publicKey)) {
|
|
301
351
|
throw new Error("This recovery key already has a wallet in the selected network; select that wallet instead");
|
|
302
352
|
}
|
|
303
353
|
const wallets = [...store.wallets];
|
|
304
|
-
wallets[store.activeWalletIndex] =
|
|
354
|
+
wallets[store.activeWalletIndex] = updated.file;
|
|
305
355
|
await replaceWalletStore(path, { ...store, wallets });
|
|
306
|
-
return
|
|
356
|
+
return updated.file;
|
|
307
357
|
}
|
|
308
358
|
async function walletFileMetadata(path) {
|
|
309
359
|
const store = await readWalletStore(path);
|
|
@@ -312,11 +362,12 @@ async function walletFileMetadata(path) {
|
|
|
312
362
|
|
|
313
363
|
// src/cli.ts
|
|
314
364
|
var HELP = `
|
|
315
|
-
LDGR CLI 0.2.
|
|
365
|
+
LDGR CLI 0.2.2 — self-custody wallet
|
|
316
366
|
|
|
317
367
|
Usage:
|
|
318
368
|
ldgr wallet create [--testnet] [--wallet <path>]
|
|
319
|
-
ldgr wallet import [--testnet] [--wallet <path>]
|
|
369
|
+
ldgr wallet import [recovery-key-file] [--testnet] [--wallet <path>]
|
|
370
|
+
ldgr wallet export <recovery-key-file> [--wallet <path>]
|
|
320
371
|
ldgr wallet list [--wallet <path>]
|
|
321
372
|
ldgr wallet select <index> [--wallet <path>]
|
|
322
373
|
ldgr address [--wallet <path>]
|
|
@@ -326,13 +377,11 @@ Usage:
|
|
|
326
377
|
ldgr network show [--wallet <path>]
|
|
327
378
|
ldgr network set <mainnet|testnet> [--wallet <path>]
|
|
328
379
|
|
|
329
|
-
The default wallet store is ~/.ldgr/wallet.json.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
\`ldgr network set testnet\` to switch its network persistently. Testnet mode
|
|
335
|
-
prints a warning on every wallet command.
|
|
380
|
+
The default wallet store is ~/.ldgr/wallet.json. A wallet can be
|
|
381
|
+
passphrase-protected or unprotected (local-file permissions only). The CLI
|
|
382
|
+
never accepts recovery-key values or passphrases as arguments or environment
|
|
383
|
+
variables. Use \`ldgr wallet export <path>\` for an extension-compatible
|
|
384
|
+
recovery-key file. Testnet mode prints a warning on every wallet command.
|
|
336
385
|
`;
|
|
337
386
|
function parseArguments(argv) {
|
|
338
387
|
const command = [];
|
|
@@ -380,6 +429,23 @@ async function askPassphrase(message) {
|
|
|
380
429
|
throw new Error("Cancelled");
|
|
381
430
|
return answer;
|
|
382
431
|
}
|
|
432
|
+
async function chooseWalletPassphrase() {
|
|
433
|
+
const protectedWallet = await p.confirm({ message: "Protect this wallet with a passphrase?", initialValue: true });
|
|
434
|
+
if (p.isCancel(protectedWallet))
|
|
435
|
+
throw new Error("Cancelled");
|
|
436
|
+
if (!protectedWallet) {
|
|
437
|
+
p.log.warn("This wallet will be unprotected. Anyone who can read its owner-only local file can spend its funds.");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const passphrase = await askPassphrase("Choose a wallet passphrase");
|
|
441
|
+
const confirmation = await askPassphrase("Repeat the wallet passphrase");
|
|
442
|
+
if (passphrase !== confirmation)
|
|
443
|
+
throw new Error("Passphrases did not match");
|
|
444
|
+
return passphrase;
|
|
445
|
+
}
|
|
446
|
+
async function signingPassphrase(file, message) {
|
|
447
|
+
return file.protection === "none" ? undefined : askPassphrase(message);
|
|
448
|
+
}
|
|
383
449
|
async function confirm2(message) {
|
|
384
450
|
const answer = await p.confirm({ message, initialValue: false });
|
|
385
451
|
if (p.isCancel(answer) || !answer)
|
|
@@ -412,39 +478,34 @@ async function runCreate(path, testnet) {
|
|
|
412
478
|
const ledgerId = testnet ? 0 : 1;
|
|
413
479
|
if (ledgerId === 0)
|
|
414
480
|
showNetworkWarning({ ledgerId });
|
|
415
|
-
const passphrase = await
|
|
416
|
-
const confirmation = await askPassphrase("Repeat the wallet passphrase");
|
|
417
|
-
if (passphrase !== confirmation)
|
|
418
|
-
throw new Error("Passphrases did not match");
|
|
481
|
+
const passphrase = await chooseWalletPassphrase();
|
|
419
482
|
const spinner2 = p.spinner();
|
|
420
|
-
spinner2.start("Generating
|
|
483
|
+
spinner2.start("Generating your wallet");
|
|
421
484
|
const created = await createEncryptedWalletFile({ ledgerId, passphrase });
|
|
422
485
|
const index = await addWalletFile(path, created.file);
|
|
423
|
-
spinner2.stop("
|
|
486
|
+
spinner2.stop(passphrase === undefined ? "Unprotected wallet added" : "Protected wallet added");
|
|
424
487
|
p.note(`Address
|
|
425
488
|
${created.file.address}
|
|
426
489
|
|
|
427
490
|
Recovery key — save this offline now; it will not be shown again
|
|
428
491
|
${created.recoveryKey}`, "Your self-custody backup");
|
|
429
|
-
p.log.warn("This recovery key alone can restore and control this wallet.
|
|
492
|
+
p.log.warn("This recovery key alone can restore and control this wallet. Export it as a file with `ldgr wallet export <path>` and keep it offline.");
|
|
430
493
|
p.outro(`Wallet #${index + 1} selected in ${path}`);
|
|
431
494
|
}
|
|
432
|
-
async function runImport(path, testnet) {
|
|
495
|
+
async function runImport(path, testnet, recoveryKeyPath) {
|
|
433
496
|
const ledgerId = testnet ? 0 : 1;
|
|
434
497
|
if (ledgerId === 0)
|
|
435
498
|
showNetworkWarning({ ledgerId });
|
|
436
|
-
const
|
|
499
|
+
const enteredRecoveryKey = recoveryKeyPath === undefined ? await p.password({
|
|
437
500
|
message: "Enter the recovery key to import",
|
|
438
501
|
validate(value) {
|
|
439
502
|
return value.trim() ? undefined : "Enter a recovery key";
|
|
440
503
|
}
|
|
441
|
-
});
|
|
442
|
-
if (p.isCancel(
|
|
504
|
+
}) : (await readFile2(recoveryKeyPath, "utf8")).trim();
|
|
505
|
+
if (p.isCancel(enteredRecoveryKey) || !enteredRecoveryKey)
|
|
443
506
|
throw new Error("Cancelled");
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
if (passphrase !== confirmation)
|
|
447
|
-
throw new Error("Passphrases did not match");
|
|
507
|
+
const recoveryKey = enteredRecoveryKey;
|
|
508
|
+
const passphrase = await chooseWalletPassphrase();
|
|
448
509
|
const spinner2 = p.spinner();
|
|
449
510
|
spinner2.start("Validating and encrypting imported wallet");
|
|
450
511
|
const imported = await importEncryptedWalletFile({ path, recoveryKey, ledgerId, passphrase });
|
|
@@ -458,12 +519,20 @@ ${networkLabel(imported.file)}`, `Wallet #${imported.index + 1} selected`);
|
|
|
458
519
|
showNetworkWarning(imported.file);
|
|
459
520
|
p.outro(`Wallet #${imported.index + 1} added to ${path}`);
|
|
460
521
|
}
|
|
522
|
+
async function runExport(path, backupPath) {
|
|
523
|
+
const file = await walletFileMetadata(path);
|
|
524
|
+
showNetworkWarning(file);
|
|
525
|
+
const passphrase = await signingPassphrase(file, "Enter your wallet passphrase to export");
|
|
526
|
+
await exportRecoveryKeyFile({ walletPath: path, backupPath, passphrase });
|
|
527
|
+
p.log.warn("Anyone with this recovery-key file can control the wallet. Keep it offline and never share it.");
|
|
528
|
+
p.outro(`Recovery-key file saved at ${backupPath}`);
|
|
529
|
+
}
|
|
461
530
|
async function runWalletList(path) {
|
|
462
531
|
const wallets = await listWalletFileMetadata(path);
|
|
463
532
|
const selected = wallets.find((wallet) => wallet.selected);
|
|
464
533
|
showNetworkWarning(selected);
|
|
465
534
|
const rows = wallets.map((wallet) => [
|
|
466
|
-
`${wallet.selected ? "● Selected" : "○"} #${wallet.index + 1} · ${networkLabel(wallet)}`,
|
|
535
|
+
`${wallet.selected ? "● Selected" : "○"} #${wallet.index + 1} · ${networkLabel(wallet)} · ${wallet.protection === "none" ? "Unprotected" : "Protected"}`,
|
|
467
536
|
wallet.address
|
|
468
537
|
].join(`
|
|
469
538
|
`));
|
|
@@ -515,7 +584,7 @@ async function runSend(path, rawAsset, recipient, amount) {
|
|
|
515
584
|
const client = await selectedClient(file);
|
|
516
585
|
const asset = resolvePublicAssetCode(rawAsset, file.ledgerId);
|
|
517
586
|
const definition = await definitionForAsset(client, asset, file.ledgerId);
|
|
518
|
-
const passphrase = await
|
|
587
|
+
const passphrase = await signingPassphrase(file, "Enter your wallet passphrase to sign");
|
|
519
588
|
const wallet = await readEncryptedWalletFile(path, passphrase);
|
|
520
589
|
const makeSubmission = async () => buildTransferSubmission({
|
|
521
590
|
wallet,
|
|
@@ -573,7 +642,7 @@ ${file.address}`, "Selected network");
|
|
|
573
642
|
if (ledgerId === 0)
|
|
574
643
|
showNetworkWarning({ ledgerId });
|
|
575
644
|
p.log.info("Switching networks derives a different address from the same self-custody key.");
|
|
576
|
-
const passphrase = await
|
|
645
|
+
const passphrase = await signingPassphrase(file, "Enter your wallet passphrase to switch networks");
|
|
577
646
|
await confirm2(`Switch this wallet to ${target}?`);
|
|
578
647
|
const spinner2 = p.spinner();
|
|
579
648
|
spinner2.start("Re-encrypting wallet for the selected network");
|
|
@@ -604,8 +673,10 @@ async function main() {
|
|
|
604
673
|
p.log.info(`Moved legacy wallet store to ${resolved.path}`);
|
|
605
674
|
if (first === "wallet" && second === "create" && third === undefined)
|
|
606
675
|
await runCreate(resolved.path, parsed.walletOnTestnet);
|
|
607
|
-
else if (first === "wallet" && second === "import" &&
|
|
608
|
-
await runImport(resolved.path, parsed.walletOnTestnet);
|
|
676
|
+
else if (first === "wallet" && second === "import" && fourth === undefined)
|
|
677
|
+
await runImport(resolved.path, parsed.walletOnTestnet, third);
|
|
678
|
+
else if (first === "wallet" && second === "export" && third && fourth === undefined)
|
|
679
|
+
await runExport(resolved.path, third);
|
|
609
680
|
else if (first === "wallet" && second === "list" && third === undefined)
|
|
610
681
|
await runWalletList(resolved.path);
|
|
611
682
|
else if (first === "wallet" && second === "select" && third && fourth === undefined)
|
package/dist/wallet-file.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { type LdgrSigner } from "@ldgr/client";
|
|
2
2
|
import { type LedgerId } from "@ldgr/protocol";
|
|
3
3
|
export type WalletNetwork = "mainnet" | "testnet";
|
|
4
|
-
|
|
4
|
+
type WalletIdentity = {
|
|
5
5
|
readonly format: "ldgr-cli-wallet";
|
|
6
6
|
readonly version: 1;
|
|
7
7
|
readonly network: WalletNetwork;
|
|
8
8
|
readonly ledgerId: LedgerId;
|
|
9
9
|
readonly address: string;
|
|
10
10
|
readonly publicKey: string;
|
|
11
|
+
};
|
|
12
|
+
/** Passphrase protection is the legacy format when `protection` is omitted. */
|
|
13
|
+
export type EncryptedWalletFile = WalletIdentity & {
|
|
14
|
+
readonly protection?: "passphrase";
|
|
11
15
|
readonly ciphertext: string;
|
|
12
16
|
readonly iv: string;
|
|
13
17
|
readonly kdf: {
|
|
@@ -17,13 +21,20 @@ export type EncryptedWalletFile = {
|
|
|
17
21
|
readonly salt: string;
|
|
18
22
|
};
|
|
19
23
|
};
|
|
24
|
+
/** Matches the extension's explicit local-only, no-passphrase vault mode. */
|
|
25
|
+
export type UnprotectedWalletFile = WalletIdentity & {
|
|
26
|
+
readonly protection: "none";
|
|
27
|
+
/** Local secret material; protected by the file's owner-only permissions only. */
|
|
28
|
+
readonly recoveryKey: string;
|
|
29
|
+
};
|
|
30
|
+
export type LocalWalletFile = EncryptedWalletFile | UnprotectedWalletFile;
|
|
20
31
|
export type EncryptedWalletStore = {
|
|
21
32
|
readonly format: "ldgr-cli-wallet-store";
|
|
22
33
|
readonly version: 1;
|
|
23
34
|
readonly activeWalletIndex: number;
|
|
24
|
-
readonly wallets: readonly
|
|
35
|
+
readonly wallets: readonly LocalWalletFile[];
|
|
25
36
|
};
|
|
26
|
-
export type WalletFileMetadata =
|
|
37
|
+
export type WalletFileMetadata = LocalWalletFile & {
|
|
27
38
|
readonly index: number;
|
|
28
39
|
readonly selected: boolean;
|
|
29
40
|
};
|
|
@@ -33,7 +44,7 @@ export type DefaultWalletPathInput = {
|
|
|
33
44
|
readonly configHome?: string | undefined;
|
|
34
45
|
};
|
|
35
46
|
export type CreatedEncryptedWallet = {
|
|
36
|
-
readonly file:
|
|
47
|
+
readonly file: LocalWalletFile;
|
|
37
48
|
readonly serialized: string;
|
|
38
49
|
/** Display once during creation; never persist or log this value. */
|
|
39
50
|
readonly recoveryKey: string;
|
|
@@ -43,9 +54,9 @@ export declare function defaultWalletPath(input?: DefaultWalletPathInput): strin
|
|
|
43
54
|
export declare function legacyDefaultWalletPath(input?: DefaultWalletPathInput): string;
|
|
44
55
|
export declare function createEncryptedWalletFile(input: {
|
|
45
56
|
readonly ledgerId: LedgerId;
|
|
46
|
-
readonly passphrase
|
|
57
|
+
readonly passphrase?: string | undefined;
|
|
47
58
|
}): Promise<CreatedEncryptedWallet>;
|
|
48
|
-
export declare function writeNewWalletFile(path: string, file:
|
|
59
|
+
export declare function writeNewWalletFile(path: string, file: LocalWalletFile): Promise<void>;
|
|
49
60
|
/**
|
|
50
61
|
* Moves the validated former XDG default into ~/.ldgr/ on first wallet use.
|
|
51
62
|
* Explicit --wallet paths are deliberately never considered here.
|
|
@@ -54,21 +65,28 @@ export declare function migrateDefaultWalletPath(input?: DefaultWalletPathInput)
|
|
|
54
65
|
readonly path: string;
|
|
55
66
|
readonly migrated: boolean;
|
|
56
67
|
}>;
|
|
57
|
-
export declare function readEncryptedWalletFile(path: string, passphrase
|
|
68
|
+
export declare function readEncryptedWalletFile(path: string, passphrase?: string): Promise<LdgrSigner>;
|
|
69
|
+
/** Writes the exact raw recovery-key text accepted by the extension importer. */
|
|
70
|
+
export declare function exportRecoveryKeyFile(input: {
|
|
71
|
+
readonly walletPath: string;
|
|
72
|
+
readonly backupPath: string;
|
|
73
|
+
readonly passphrase?: string | undefined;
|
|
74
|
+
}): Promise<void>;
|
|
58
75
|
/** Adds a separately encrypted wallet and selects it. */
|
|
59
|
-
export declare function addWalletFile(path: string, file:
|
|
60
|
-
/**
|
|
76
|
+
export declare function addWalletFile(path: string, file: LocalWalletFile): Promise<number>;
|
|
77
|
+
/** Adds canonical recovery material with optional passphrase protection and selects it. */
|
|
61
78
|
export declare function importEncryptedWalletFile(input: {
|
|
62
79
|
readonly path: string;
|
|
63
80
|
readonly recoveryKey: string;
|
|
64
81
|
readonly ledgerId: LedgerId;
|
|
65
|
-
readonly passphrase
|
|
82
|
+
readonly passphrase?: string | undefined;
|
|
66
83
|
}): Promise<{
|
|
67
|
-
readonly file:
|
|
84
|
+
readonly file: LocalWalletFile;
|
|
68
85
|
readonly index: number;
|
|
69
86
|
}>;
|
|
70
87
|
export declare function listWalletFileMetadata(path: string): Promise<readonly WalletFileMetadata[]>;
|
|
71
|
-
export declare function selectWalletFile(path: string, index: number): Promise<
|
|
88
|
+
export declare function selectWalletFile(path: string, index: number): Promise<LocalWalletFile>;
|
|
72
89
|
/** Re-encrypts the same self-custody key for a different LDGR ledger. */
|
|
73
|
-
export declare function switchWalletNetwork(path: string, passphrase: string, ledgerId: LedgerId): Promise<
|
|
74
|
-
export declare function walletFileMetadata(path: string): Promise<
|
|
90
|
+
export declare function switchWalletNetwork(path: string, passphrase: string | undefined, ledgerId: LedgerId): Promise<LocalWalletFile>;
|
|
91
|
+
export declare function walletFileMetadata(path: string): Promise<LocalWalletFile>;
|
|
92
|
+
export {};
|