@ldgr/cli 0.1.1 → 0.2.1
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 +165 -52
- package/dist/wallet-file.d.ts +48 -17
- 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 `~/.
|
|
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) {
|
|
@@ -118,13 +129,35 @@ function defaultWalletPath(input = {}) {
|
|
|
118
129
|
const home = input.home ?? process.env.HOME;
|
|
119
130
|
if (!home)
|
|
120
131
|
throw new Error("Could not determine your home directory; use --wallet <path>");
|
|
121
|
-
|
|
132
|
+
return join(home, ".ldgr", "wallet.json");
|
|
133
|
+
}
|
|
134
|
+
function legacyDefaultWalletPath(input = {}) {
|
|
135
|
+
const home = input.home ?? process.env.HOME;
|
|
136
|
+
if (!home)
|
|
137
|
+
throw new Error("Could not determine your home directory; use --wallet <path>");
|
|
138
|
+
const configHome = input.configHome ?? (process.env.XDG_CONFIG_HOME?.trim() || join(home, ".config"));
|
|
122
139
|
return join(configHome, "ldgr", "wallet.json");
|
|
123
140
|
}
|
|
124
141
|
async function createEncryptedWalletFile(input) {
|
|
125
142
|
const wallet = await generateLocalWallet({ ledgerId: input.ledgerId });
|
|
143
|
+
if (input.passphrase === undefined)
|
|
144
|
+
return createUnprotectedWallet({ wallet, recoveryKey: wallet.recoveryKey });
|
|
126
145
|
return encryptWallet({ wallet, recoveryKey: wallet.recoveryKey, passphrase: input.passphrase });
|
|
127
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
|
+
}
|
|
128
161
|
async function encryptWallet(input) {
|
|
129
162
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
130
163
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
@@ -146,7 +179,7 @@ async function encryptWallet(input) {
|
|
|
146
179
|
return { file, serialized: `${JSON.stringify(file, null, 2)}
|
|
147
180
|
`, recoveryKey: input.recoveryKey };
|
|
148
181
|
}
|
|
149
|
-
async function
|
|
182
|
+
async function writeNewWalletStore(path, store) {
|
|
150
183
|
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
151
184
|
const handle = await open(path, "wx", 384).catch((error) => {
|
|
152
185
|
if (error.code === "EEXIST")
|
|
@@ -154,12 +187,15 @@ async function writeNewWalletFile(path, file) {
|
|
|
154
187
|
throw error;
|
|
155
188
|
});
|
|
156
189
|
try {
|
|
157
|
-
await handle.writeFile(serializeWalletStore(
|
|
190
|
+
await handle.writeFile(serializeWalletStore(store), "utf8");
|
|
158
191
|
await handle.sync();
|
|
159
192
|
} finally {
|
|
160
193
|
await handle.close();
|
|
161
194
|
}
|
|
162
195
|
}
|
|
196
|
+
async function writeNewWalletFile(path, file) {
|
|
197
|
+
await writeNewWalletStore(path, newWalletStore(file));
|
|
198
|
+
}
|
|
163
199
|
async function readWalletStore(path) {
|
|
164
200
|
const details = await lstat(path).catch((error) => {
|
|
165
201
|
if (error.code === "ENOENT")
|
|
@@ -178,7 +214,43 @@ async function readWalletStore(path) {
|
|
|
178
214
|
}
|
|
179
215
|
return parseWalletStore(parsed);
|
|
180
216
|
}
|
|
217
|
+
async function migrateDefaultWalletPath(input = {}) {
|
|
218
|
+
const path = defaultWalletPath(input);
|
|
219
|
+
try {
|
|
220
|
+
await lstat(path);
|
|
221
|
+
return { path, migrated: false };
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error.code !== "ENOENT")
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
const legacyPath = legacyDefaultWalletPath(input);
|
|
227
|
+
let legacyStore;
|
|
228
|
+
try {
|
|
229
|
+
legacyStore = await readWalletStore(legacyPath);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error instanceof Error && error.message === `Wallet file not found: ${legacyPath}`)
|
|
232
|
+
return { path, migrated: false };
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
await writeNewWalletStore(path, legacyStore);
|
|
236
|
+
try {
|
|
237
|
+
await unlink(legacyPath);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw new Error(`Migrated wallet to ${path}, but could not remove legacy wallet file ${legacyPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
240
|
+
}
|
|
241
|
+
return { path, migrated: true };
|
|
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
|
+
}
|
|
181
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");
|
|
182
254
|
try {
|
|
183
255
|
const key = await deriveEncryptionKey(passphrase, decodeBase64Url(file.kdf.salt));
|
|
184
256
|
const plaintext = new Uint8Array(await crypto.subtle.decrypt({ name: AES_GCM, iv: arrayBuffer(decodeBase64Url(file.iv)) }, key, arrayBuffer(decodeBase64Url(file.ciphertext))));
|
|
@@ -186,10 +258,7 @@ async function unlockWalletFile(file, passphrase) {
|
|
|
186
258
|
plaintext.fill(0);
|
|
187
259
|
if (typeof value.recoveryKey !== "string")
|
|
188
260
|
throw new Error("missing recovery key");
|
|
189
|
-
|
|
190
|
-
if (wallet.address !== file.address || wallet.publicKey !== file.publicKey)
|
|
191
|
-
throw new Error("wallet identity mismatch");
|
|
192
|
-
return { wallet, recoveryKey: value.recoveryKey };
|
|
261
|
+
return await walletFromRecoveryKey(file, value.recoveryKey);
|
|
193
262
|
} catch {
|
|
194
263
|
throw new Error("Could not unlock wallet: check your passphrase or wallet file");
|
|
195
264
|
}
|
|
@@ -199,6 +268,22 @@ async function readEncryptedWalletFile(path, passphrase) {
|
|
|
199
268
|
const file = store.wallets[store.activeWalletIndex];
|
|
200
269
|
return (await unlockWalletFile(file, passphrase)).wallet;
|
|
201
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
|
+
}
|
|
202
287
|
async function replaceWalletStore(path, store) {
|
|
203
288
|
await readWalletStore(path);
|
|
204
289
|
const temporaryPath = join(dirname(path), `.${crypto.randomUUID()}.wallet.tmp`);
|
|
@@ -238,8 +323,8 @@ async function addWalletFile(path, file) {
|
|
|
238
323
|
}
|
|
239
324
|
async function importEncryptedWalletFile(input) {
|
|
240
325
|
const wallet = await loadLocalWallet({ recoveryKey: input.recoveryKey, ledgerId: input.ledgerId });
|
|
241
|
-
const
|
|
242
|
-
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) };
|
|
243
328
|
}
|
|
244
329
|
async function listWalletFileMetadata(path) {
|
|
245
330
|
const store = await readWalletStore(path);
|
|
@@ -261,14 +346,14 @@ async function switchWalletNetwork(path, passphrase, ledgerId) {
|
|
|
261
346
|
return existing;
|
|
262
347
|
const unlocked = await unlockWalletFile(existing, passphrase);
|
|
263
348
|
const wallet = await loadLocalWallet({ recoveryKey: unlocked.recoveryKey, ledgerId });
|
|
264
|
-
const
|
|
349
|
+
const updated = existing.protection === "none" ? createUnprotectedWallet({ wallet, recoveryKey: unlocked.recoveryKey }) : await encryptWallet({ wallet, recoveryKey: unlocked.recoveryKey, passphrase });
|
|
265
350
|
if (store.wallets.some((entry, index) => index !== store.activeWalletIndex && entry.ledgerId === ledgerId && entry.publicKey === wallet.publicKey)) {
|
|
266
351
|
throw new Error("This recovery key already has a wallet in the selected network; select that wallet instead");
|
|
267
352
|
}
|
|
268
353
|
const wallets = [...store.wallets];
|
|
269
|
-
wallets[store.activeWalletIndex] =
|
|
354
|
+
wallets[store.activeWalletIndex] = updated.file;
|
|
270
355
|
await replaceWalletStore(path, { ...store, wallets });
|
|
271
|
-
return
|
|
356
|
+
return updated.file;
|
|
272
357
|
}
|
|
273
358
|
async function walletFileMetadata(path) {
|
|
274
359
|
const store = await readWalletStore(path);
|
|
@@ -277,11 +362,12 @@ async function walletFileMetadata(path) {
|
|
|
277
362
|
|
|
278
363
|
// src/cli.ts
|
|
279
364
|
var HELP = `
|
|
280
|
-
LDGR CLI 0.
|
|
365
|
+
LDGR CLI 0.2.1 — self-custody wallet
|
|
281
366
|
|
|
282
367
|
Usage:
|
|
283
368
|
ldgr wallet create [--testnet] [--wallet <path>]
|
|
284
|
-
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>]
|
|
285
371
|
ldgr wallet list [--wallet <path>]
|
|
286
372
|
ldgr wallet select <index> [--wallet <path>]
|
|
287
373
|
ldgr address [--wallet <path>]
|
|
@@ -291,16 +377,17 @@ Usage:
|
|
|
291
377
|
ldgr network show [--wallet <path>]
|
|
292
378
|
ldgr network set <mainnet|testnet> [--wallet <path>]
|
|
293
379
|
|
|
294
|
-
The default wallet store is ~/.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
prints a warning on every wallet command.
|
|
380
|
+
The default wallet store is ~/.ldgr/wallet.json. Existing wallets at
|
|
381
|
+
~/.config/ldgr/wallet.json move there safely on first use. A wallet can be
|
|
382
|
+
passphrase-protected or unprotected (local-file permissions only). The CLI
|
|
383
|
+
never accepts recovery-key values or passphrases as arguments or environment
|
|
384
|
+
variables. Use \`ldgr wallet export <path>\` for an extension-compatible
|
|
385
|
+
recovery-key file. Testnet mode prints a warning on every wallet command.
|
|
300
386
|
`;
|
|
301
387
|
function parseArguments(argv) {
|
|
302
388
|
const command = [];
|
|
303
389
|
let walletPath = defaultWalletPath();
|
|
390
|
+
let walletPathExplicit = false;
|
|
304
391
|
let walletOnTestnet = false;
|
|
305
392
|
for (let index = 0;index < argv.length; index += 1) {
|
|
306
393
|
const value = argv[index];
|
|
@@ -309,6 +396,7 @@ function parseArguments(argv) {
|
|
|
309
396
|
if (!path || path.startsWith("-"))
|
|
310
397
|
throw new TypeError("--wallet requires a file path");
|
|
311
398
|
walletPath = path;
|
|
399
|
+
walletPathExplicit = true;
|
|
312
400
|
index += 1;
|
|
313
401
|
} else if (value === "--testnet") {
|
|
314
402
|
walletOnTestnet = true;
|
|
@@ -321,7 +409,7 @@ function parseArguments(argv) {
|
|
|
321
409
|
if (walletOnTestnet && !(command[0] === "wallet" && (command[1] === "create" || command[1] === "import"))) {
|
|
322
410
|
throw new TypeError("--testnet is only used with wallet create or wallet import; use `ldgr network set testnet` to switch an existing wallet");
|
|
323
411
|
}
|
|
324
|
-
return { command, walletPath, walletOnTestnet };
|
|
412
|
+
return { command, walletPath, walletPathExplicit, walletOnTestnet };
|
|
325
413
|
}
|
|
326
414
|
function networkLabel(file) {
|
|
327
415
|
return file.ledgerId === 0 ? "testnet" : "mainnet";
|
|
@@ -342,6 +430,23 @@ async function askPassphrase(message) {
|
|
|
342
430
|
throw new Error("Cancelled");
|
|
343
431
|
return answer;
|
|
344
432
|
}
|
|
433
|
+
async function chooseWalletPassphrase() {
|
|
434
|
+
const protectedWallet = await p.confirm({ message: "Protect this wallet with a passphrase?", initialValue: true });
|
|
435
|
+
if (p.isCancel(protectedWallet))
|
|
436
|
+
throw new Error("Cancelled");
|
|
437
|
+
if (!protectedWallet) {
|
|
438
|
+
p.log.warn("This wallet will be unprotected. Anyone who can read its owner-only local file can spend its funds.");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const passphrase = await askPassphrase("Choose a wallet passphrase");
|
|
442
|
+
const confirmation = await askPassphrase("Repeat the wallet passphrase");
|
|
443
|
+
if (passphrase !== confirmation)
|
|
444
|
+
throw new Error("Passphrases did not match");
|
|
445
|
+
return passphrase;
|
|
446
|
+
}
|
|
447
|
+
async function signingPassphrase(file, message) {
|
|
448
|
+
return file.protection === "none" ? undefined : askPassphrase(message);
|
|
449
|
+
}
|
|
345
450
|
async function confirm2(message) {
|
|
346
451
|
const answer = await p.confirm({ message, initialValue: false });
|
|
347
452
|
if (p.isCancel(answer) || !answer)
|
|
@@ -374,39 +479,34 @@ async function runCreate(path, testnet) {
|
|
|
374
479
|
const ledgerId = testnet ? 0 : 1;
|
|
375
480
|
if (ledgerId === 0)
|
|
376
481
|
showNetworkWarning({ ledgerId });
|
|
377
|
-
const passphrase = await
|
|
378
|
-
const confirmation = await askPassphrase("Repeat the wallet passphrase");
|
|
379
|
-
if (passphrase !== confirmation)
|
|
380
|
-
throw new Error("Passphrases did not match");
|
|
482
|
+
const passphrase = await chooseWalletPassphrase();
|
|
381
483
|
const spinner2 = p.spinner();
|
|
382
|
-
spinner2.start("Generating
|
|
484
|
+
spinner2.start("Generating your wallet");
|
|
383
485
|
const created = await createEncryptedWalletFile({ ledgerId, passphrase });
|
|
384
486
|
const index = await addWalletFile(path, created.file);
|
|
385
|
-
spinner2.stop("
|
|
487
|
+
spinner2.stop(passphrase === undefined ? "Unprotected wallet added" : "Protected wallet added");
|
|
386
488
|
p.note(`Address
|
|
387
489
|
${created.file.address}
|
|
388
490
|
|
|
389
491
|
Recovery key — save this offline now; it will not be shown again
|
|
390
492
|
${created.recoveryKey}`, "Your self-custody backup");
|
|
391
|
-
p.log.warn("This recovery key alone can restore and control this wallet.
|
|
493
|
+
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.");
|
|
392
494
|
p.outro(`Wallet #${index + 1} selected in ${path}`);
|
|
393
495
|
}
|
|
394
|
-
async function runImport(path, testnet) {
|
|
496
|
+
async function runImport(path, testnet, recoveryKeyPath) {
|
|
395
497
|
const ledgerId = testnet ? 0 : 1;
|
|
396
498
|
if (ledgerId === 0)
|
|
397
499
|
showNetworkWarning({ ledgerId });
|
|
398
|
-
const
|
|
500
|
+
const enteredRecoveryKey = recoveryKeyPath === undefined ? await p.password({
|
|
399
501
|
message: "Enter the recovery key to import",
|
|
400
502
|
validate(value) {
|
|
401
503
|
return value.trim() ? undefined : "Enter a recovery key";
|
|
402
504
|
}
|
|
403
|
-
});
|
|
404
|
-
if (p.isCancel(
|
|
505
|
+
}) : (await readFile2(recoveryKeyPath, "utf8")).trim();
|
|
506
|
+
if (p.isCancel(enteredRecoveryKey) || !enteredRecoveryKey)
|
|
405
507
|
throw new Error("Cancelled");
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
if (passphrase !== confirmation)
|
|
409
|
-
throw new Error("Passphrases did not match");
|
|
508
|
+
const recoveryKey = enteredRecoveryKey;
|
|
509
|
+
const passphrase = await chooseWalletPassphrase();
|
|
410
510
|
const spinner2 = p.spinner();
|
|
411
511
|
spinner2.start("Validating and encrypting imported wallet");
|
|
412
512
|
const imported = await importEncryptedWalletFile({ path, recoveryKey, ledgerId, passphrase });
|
|
@@ -420,12 +520,20 @@ ${networkLabel(imported.file)}`, `Wallet #${imported.index + 1} selected`);
|
|
|
420
520
|
showNetworkWarning(imported.file);
|
|
421
521
|
p.outro(`Wallet #${imported.index + 1} added to ${path}`);
|
|
422
522
|
}
|
|
523
|
+
async function runExport(path, backupPath) {
|
|
524
|
+
const file = await walletFileMetadata(path);
|
|
525
|
+
showNetworkWarning(file);
|
|
526
|
+
const passphrase = await signingPassphrase(file, "Enter your wallet passphrase to export");
|
|
527
|
+
await exportRecoveryKeyFile({ walletPath: path, backupPath, passphrase });
|
|
528
|
+
p.log.warn("Anyone with this recovery-key file can control the wallet. Keep it offline and never share it.");
|
|
529
|
+
p.outro(`Recovery-key file saved at ${backupPath}`);
|
|
530
|
+
}
|
|
423
531
|
async function runWalletList(path) {
|
|
424
532
|
const wallets = await listWalletFileMetadata(path);
|
|
425
533
|
const selected = wallets.find((wallet) => wallet.selected);
|
|
426
534
|
showNetworkWarning(selected);
|
|
427
535
|
const rows = wallets.map((wallet) => [
|
|
428
|
-
`${wallet.selected ? "● Selected" : "○"} #${wallet.index + 1} · ${networkLabel(wallet)}`,
|
|
536
|
+
`${wallet.selected ? "● Selected" : "○"} #${wallet.index + 1} · ${networkLabel(wallet)} · ${wallet.protection === "none" ? "Unprotected" : "Protected"}`,
|
|
429
537
|
wallet.address
|
|
430
538
|
].join(`
|
|
431
539
|
`));
|
|
@@ -477,7 +585,7 @@ async function runSend(path, rawAsset, recipient, amount) {
|
|
|
477
585
|
const client = await selectedClient(file);
|
|
478
586
|
const asset = resolvePublicAssetCode(rawAsset, file.ledgerId);
|
|
479
587
|
const definition = await definitionForAsset(client, asset, file.ledgerId);
|
|
480
|
-
const passphrase = await
|
|
588
|
+
const passphrase = await signingPassphrase(file, "Enter your wallet passphrase to sign");
|
|
481
589
|
const wallet = await readEncryptedWalletFile(path, passphrase);
|
|
482
590
|
const makeSubmission = async () => buildTransferSubmission({
|
|
483
591
|
wallet,
|
|
@@ -535,7 +643,7 @@ ${file.address}`, "Selected network");
|
|
|
535
643
|
if (ledgerId === 0)
|
|
536
644
|
showNetworkWarning({ ledgerId });
|
|
537
645
|
p.log.info("Switching networks derives a different address from the same self-custody key.");
|
|
538
|
-
const passphrase = await
|
|
646
|
+
const passphrase = await signingPassphrase(file, "Enter your wallet passphrase to switch networks");
|
|
539
647
|
await confirm2(`Switch this wallet to ${target}?`);
|
|
540
648
|
const spinner2 = p.spinner();
|
|
541
649
|
spinner2.start("Re-encrypting wallet for the selected network");
|
|
@@ -561,26 +669,31 @@ async function main() {
|
|
|
561
669
|
}
|
|
562
670
|
p.intro("LDGR CLI");
|
|
563
671
|
try {
|
|
672
|
+
const resolved = parsed.walletPathExplicit ? { path: parsed.walletPath, migrated: false } : await migrateDefaultWalletPath();
|
|
673
|
+
if (resolved.migrated)
|
|
674
|
+
p.log.info(`Moved legacy wallet store to ${resolved.path}`);
|
|
564
675
|
if (first === "wallet" && second === "create" && third === undefined)
|
|
565
|
-
await runCreate(
|
|
566
|
-
else if (first === "wallet" && second === "import" &&
|
|
567
|
-
await runImport(
|
|
676
|
+
await runCreate(resolved.path, parsed.walletOnTestnet);
|
|
677
|
+
else if (first === "wallet" && second === "import" && fourth === undefined)
|
|
678
|
+
await runImport(resolved.path, parsed.walletOnTestnet, third);
|
|
679
|
+
else if (first === "wallet" && second === "export" && third && fourth === undefined)
|
|
680
|
+
await runExport(resolved.path, third);
|
|
568
681
|
else if (first === "wallet" && second === "list" && third === undefined)
|
|
569
|
-
await runWalletList(
|
|
682
|
+
await runWalletList(resolved.path);
|
|
570
683
|
else if (first === "wallet" && second === "select" && third && fourth === undefined)
|
|
571
|
-
await runWalletSelect(
|
|
684
|
+
await runWalletSelect(resolved.path, third);
|
|
572
685
|
else if (first === "address" && second === undefined)
|
|
573
|
-
await runAddress(
|
|
686
|
+
await runAddress(resolved.path);
|
|
574
687
|
else if (first === "balance" && second === undefined)
|
|
575
|
-
await runBalance(
|
|
688
|
+
await runBalance(resolved.path);
|
|
576
689
|
else if (first === "send" && second && third && fourth === undefined)
|
|
577
|
-
await runSend(
|
|
690
|
+
await runSend(resolved.path, "dng", second, third);
|
|
578
691
|
else if (first === "send-token" && second && third && fourth)
|
|
579
|
-
await runSend(
|
|
692
|
+
await runSend(resolved.path, second, third, fourth);
|
|
580
693
|
else if (first === "network" && second === "show" && third === undefined)
|
|
581
|
-
await runNetwork(
|
|
694
|
+
await runNetwork(resolved.path);
|
|
582
695
|
else if (first === "network" && second === "set" && third && fourth === undefined)
|
|
583
|
-
await runNetwork(
|
|
696
|
+
await runNetwork(resolved.path, third);
|
|
584
697
|
else
|
|
585
698
|
throw new TypeError("Unknown command. Run `ldgr --help` for available commands.");
|
|
586
699
|
} catch (error) {
|
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,45 +21,72 @@ 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
|
};
|
|
41
|
+
export type DefaultWalletPathInput = {
|
|
42
|
+
readonly home?: string | undefined;
|
|
43
|
+
/** Test seam for the former XDG default; production reads XDG_CONFIG_HOME. */
|
|
44
|
+
readonly configHome?: string | undefined;
|
|
45
|
+
};
|
|
30
46
|
export type CreatedEncryptedWallet = {
|
|
31
|
-
readonly file:
|
|
47
|
+
readonly file: LocalWalletFile;
|
|
32
48
|
readonly serialized: string;
|
|
33
49
|
/** Display once during creation; never persist or log this value. */
|
|
34
50
|
readonly recoveryKey: string;
|
|
35
51
|
};
|
|
36
|
-
export declare function defaultWalletPath(input?:
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
export declare function defaultWalletPath(input?: DefaultWalletPathInput): string;
|
|
53
|
+
/** The v0.1 default location, used only to preserve existing local wallets. */
|
|
54
|
+
export declare function legacyDefaultWalletPath(input?: DefaultWalletPathInput): string;
|
|
39
55
|
export declare function createEncryptedWalletFile(input: {
|
|
40
56
|
readonly ledgerId: LedgerId;
|
|
41
|
-
readonly passphrase
|
|
57
|
+
readonly passphrase?: string | undefined;
|
|
42
58
|
}): Promise<CreatedEncryptedWallet>;
|
|
43
|
-
export declare function writeNewWalletFile(path: string, file:
|
|
44
|
-
|
|
59
|
+
export declare function writeNewWalletFile(path: string, file: LocalWalletFile): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Moves the validated former XDG default into ~/.ldgr/ on first wallet use.
|
|
62
|
+
* Explicit --wallet paths are deliberately never considered here.
|
|
63
|
+
*/
|
|
64
|
+
export declare function migrateDefaultWalletPath(input?: DefaultWalletPathInput): Promise<{
|
|
65
|
+
readonly path: string;
|
|
66
|
+
readonly migrated: boolean;
|
|
67
|
+
}>;
|
|
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>;
|
|
45
75
|
/** Adds a separately encrypted wallet and selects it. */
|
|
46
|
-
export declare function addWalletFile(path: string, file:
|
|
47
|
-
/**
|
|
76
|
+
export declare function addWalletFile(path: string, file: LocalWalletFile): Promise<number>;
|
|
77
|
+
/** Adds canonical recovery material with optional passphrase protection and selects it. */
|
|
48
78
|
export declare function importEncryptedWalletFile(input: {
|
|
49
79
|
readonly path: string;
|
|
50
80
|
readonly recoveryKey: string;
|
|
51
81
|
readonly ledgerId: LedgerId;
|
|
52
|
-
readonly passphrase
|
|
82
|
+
readonly passphrase?: string | undefined;
|
|
53
83
|
}): Promise<{
|
|
54
|
-
readonly file:
|
|
84
|
+
readonly file: LocalWalletFile;
|
|
55
85
|
readonly index: number;
|
|
56
86
|
}>;
|
|
57
87
|
export declare function listWalletFileMetadata(path: string): Promise<readonly WalletFileMetadata[]>;
|
|
58
|
-
export declare function selectWalletFile(path: string, index: number): Promise<
|
|
88
|
+
export declare function selectWalletFile(path: string, index: number): Promise<LocalWalletFile>;
|
|
59
89
|
/** Re-encrypts the same self-custody key for a different LDGR ledger. */
|
|
60
|
-
export declare function switchWalletNetwork(path: string, passphrase: string, ledgerId: LedgerId): Promise<
|
|
61
|
-
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 {};
|