@ldgr/cli 0.5.2 → 0.5.3
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 +10 -1
- package/dist/cli.js +78 -3
- package/dist/wallet-file.d.ts +4 -0
- package/dist/web-wallet-export.d.ts +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ldgr/cli
|
|
2
2
|
|
|
3
|
-
LDGR's self-custody terminal wallet. Version 0.5.
|
|
3
|
+
LDGR's self-custody terminal wallet. Version 0.5.3 creates or imports local wallets, lets you select between them, shows addresses and balances, sends native Ledra or a token, and persistently switches the selected wallet between mainnet and testnet. It also exports encrypted files for the web wallet and reviews prepared MCP requests for transfers, swaps, token creation and escrow creation in a separate interactive terminal.
|
|
4
4
|
|
|
5
5
|
The MCP launcher uses `ldgr mcp --stdin` with a UTF-8 JSON envelope for
|
|
6
6
|
`wallet-info` and `save-request`. Envelopes are limited to 128 KiB. Public wallet
|
|
@@ -16,6 +16,7 @@ npm install -g @ldgr/cli
|
|
|
16
16
|
ldgr wallet create
|
|
17
17
|
ldgr wallet import ~/Downloads/ldgr-recovery-key.txt
|
|
18
18
|
ldgr wallet export ~/Desktop/ldgr-recovery-key.txt
|
|
19
|
+
ldgr wallet export-web ~/Desktop/ldgr-web-wallet.json
|
|
19
20
|
ldgr wallet list
|
|
20
21
|
ldgr wallet select 2
|
|
21
22
|
ldgr address
|
|
@@ -26,6 +27,14 @@ ldgr network show
|
|
|
26
27
|
ldgr network set testnet
|
|
27
28
|
```
|
|
28
29
|
|
|
30
|
+
## Encrypted web-wallet transfer
|
|
31
|
+
|
|
32
|
+
Run `ldgr wallet export-web <file> [--wallet <path>]` in your own interactive terminal. It shows the selected public address, network and destination `https://wlt.ldgr.ltd`, then asks for confirmation with **No** selected. A protected source wallet prompts privately for its existing passphrase. Every export requires a separate transfer-file passphrase of at least 12 characters, entered twice through masked prompts, including when the source wallet is unprotected. Recovery keys and passphrases are never command arguments, environment variables, or terminal output for this operation.
|
|
33
|
+
|
|
34
|
+
The command writes a new owner-only file without overwriting an existing path. Its version-1 `ldgr-web-wallet-transfer` envelope contains `ledgerId`, `address`, `publicKey`, and a passphrase-encrypted `ldgr-browser-wallet` vault. Open [Ledra Wallet](https://wlt.ldgr.ltd/#import), choose **Import encrypted wallet file**, select this file, and enter its transfer passphrase there. Delete the transfer file after import and keep the passphrase private. The encrypted file is sensitive backup material. On Windows its permissions rely on native ACLs, as the source wallet does.
|
|
35
|
+
|
|
36
|
+
The MCP command envelope may launch `wallet export-web` in a separate terminal, but the machine bridge and `@ldgr/cli/mcp` module cannot export or unlock a wallet. Piped input or output is rejected before the command reads or migrates the source vault.
|
|
37
|
+
|
|
29
38
|
## MCP approval requests
|
|
30
39
|
|
|
31
40
|
The public `@ldgr/cli/mcp` module exposes `getMcpWalletInfo`, `validateMcpRequest`, and `saveMcpRequest`. Metadata is an explicit public projection: address, public key, network, ledger ID and protection mode. It never returns a recovery key, passphrase, encrypted vault or signer. Request preparation does not unlock, sign or submit.
|
package/dist/cli.js
CHANGED
|
@@ -812,7 +812,7 @@ function commandArgs(input) {
|
|
|
812
812
|
const creation = first === "wallet" && (second === "create" || second === "import");
|
|
813
813
|
if (testnet && !creation)
|
|
814
814
|
throw new TypeError("Invalid network flag");
|
|
815
|
-
const valid = first === "wallet" && second === "create" && positional.length === 2 || first === "wallet" && second === "import" && (positional.length === 2 || positional.length === 3) || first === "wallet" && second === "list" && positional.length === 2 || first === "wallet" && second === "select" && positional.length === 3 && /^[0-9]+$/.test(third) || first === "approve" && positional.length === 2 || first === "address" && positional.length === 1 || first === "network" && second === "show" && positional.length === 2 || first === "network" && second === "set" && positional.length === 3 && (third === "mainnet" || third === "testnet");
|
|
815
|
+
const valid = first === "wallet" && second === "create" && positional.length === 2 || first === "wallet" && second === "import" && (positional.length === 2 || positional.length === 3) || first === "wallet" && second === "list" && positional.length === 2 || first === "wallet" && second === "export-web" && positional.length === 3 || first === "wallet" && second === "select" && positional.length === 3 && /^[0-9]+$/.test(third) || first === "approve" && positional.length === 2 || first === "address" && positional.length === 1 || first === "network" && second === "show" && positional.length === 2 || first === "network" && second === "set" && positional.length === 3 && (third === "mainnet" || third === "testnet");
|
|
816
816
|
if (!valid)
|
|
817
817
|
throw new TypeError("Unsupported command");
|
|
818
818
|
return [...input];
|
|
@@ -873,14 +873,58 @@ async function runMcpBridge(args) {
|
|
|
873
873
|
return;
|
|
874
874
|
}
|
|
875
875
|
|
|
876
|
+
// src/web-wallet-export.ts
|
|
877
|
+
import { importEncryptedWalletVault } from "@ldgr/wallet-core";
|
|
878
|
+
import { mkdir as mkdir4, open as open4 } from "node:fs/promises";
|
|
879
|
+
import { dirname as dirname4 } from "node:path";
|
|
880
|
+
async function exportWebWalletFile(input) {
|
|
881
|
+
if (input.transferPassphrase.length < 12)
|
|
882
|
+
throw new RangeError("Transfer passphrase must be at least 12 characters");
|
|
883
|
+
const file = await walletFileMetadata(input.walletPath);
|
|
884
|
+
if (input.expectedIdentity && (file.ledgerId !== input.expectedIdentity.ledgerId || file.address !== input.expectedIdentity.address || file.publicKey !== input.expectedIdentity.publicKey)) {
|
|
885
|
+
throw new Error("Selected wallet changed after review; start the export again");
|
|
886
|
+
}
|
|
887
|
+
const unlocked = await unlockWalletFile(file, input.sourcePassphrase);
|
|
888
|
+
const imported = await importEncryptedWalletVault({
|
|
889
|
+
recoveryKey: unlocked.recoveryKey,
|
|
890
|
+
ledgerId: file.ledgerId,
|
|
891
|
+
passphrase: input.transferPassphrase
|
|
892
|
+
});
|
|
893
|
+
if (imported.address !== file.address || imported.publicKey !== file.publicKey || imported.vault.publicKey !== file.publicKey || imported.vault.protection !== "passphrase") {
|
|
894
|
+
throw new Error("Exported wallet identity or protection does not match the source wallet");
|
|
895
|
+
}
|
|
896
|
+
const envelope = {
|
|
897
|
+
format: "ldgr-web-wallet-transfer",
|
|
898
|
+
version: 1,
|
|
899
|
+
ledgerId: file.ledgerId,
|
|
900
|
+
address: file.address,
|
|
901
|
+
publicKey: file.publicKey,
|
|
902
|
+
vault: imported.vault
|
|
903
|
+
};
|
|
904
|
+
await mkdir4(dirname4(input.backupPath), { recursive: true, mode: 448 });
|
|
905
|
+
const handle = await open4(input.backupPath, "wx", 384).catch((error) => {
|
|
906
|
+
if (error.code === "EEXIST")
|
|
907
|
+
throw new Error(`Web-wallet transfer file already exists: ${input.backupPath}`);
|
|
908
|
+
throw error;
|
|
909
|
+
});
|
|
910
|
+
try {
|
|
911
|
+
await handle.writeFile(JSON.stringify(envelope) + `
|
|
912
|
+
`, "utf8");
|
|
913
|
+
await handle.sync();
|
|
914
|
+
} finally {
|
|
915
|
+
await handle.close();
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
876
919
|
// src/cli.ts
|
|
877
920
|
var HELP = `
|
|
878
|
-
LDGR CLI 0.5.
|
|
921
|
+
LDGR CLI 0.5.3 — self-custody wallet
|
|
879
922
|
|
|
880
923
|
Usage:
|
|
881
924
|
ldgr wallet create [--testnet] [--wallet <path>]
|
|
882
925
|
ldgr wallet import [recovery-key-file] [--testnet] [--wallet <path>]
|
|
883
926
|
ldgr wallet export <recovery-key-file> [--wallet <path>]
|
|
927
|
+
ldgr wallet export-web <transfer-file> [--wallet <path>]
|
|
884
928
|
ldgr wallet list [--wallet <path>]
|
|
885
929
|
ldgr wallet select <index> [--wallet <path>]
|
|
886
930
|
ldgr address [--wallet <path>]
|
|
@@ -895,7 +939,9 @@ The default wallet store is ~/.ldgr/wallet.json. A wallet can be
|
|
|
895
939
|
passphrase-protected or unprotected (local-file permissions only). The CLI
|
|
896
940
|
never accepts recovery-key values or passphrases as arguments or environment
|
|
897
941
|
variables. Use \`ldgr wallet export <path>\` for an extension-compatible
|
|
898
|
-
recovery-key file.
|
|
942
|
+
recovery-key file. Use \`ldgr wallet export-web <path>\` in an interactive
|
|
943
|
+
terminal for a passphrase-encrypted transfer file for https://wlt.ldgr.ltd.
|
|
944
|
+
Testnet mode prints a warning on every wallet command.
|
|
899
945
|
`;
|
|
900
946
|
function parseArguments(argv) {
|
|
901
947
|
const command = [];
|
|
@@ -1041,6 +1087,30 @@ async function runExport(path, backupPath) {
|
|
|
1041
1087
|
p2.log.warn("Anyone with this recovery-key file can control the wallet. Keep it offline and never share it.");
|
|
1042
1088
|
p2.outro(`Recovery-key file saved at ${backupPath}`);
|
|
1043
1089
|
}
|
|
1090
|
+
async function runExportWeb(path, backupPath) {
|
|
1091
|
+
const file = await walletFileMetadata(path);
|
|
1092
|
+
showNetworkWarning(file);
|
|
1093
|
+
p2.note(`Address
|
|
1094
|
+
${file.address}
|
|
1095
|
+
|
|
1096
|
+
Network
|
|
1097
|
+
${networkLabel(file)}
|
|
1098
|
+
|
|
1099
|
+
Destination
|
|
1100
|
+
https://wlt.ldgr.ltd
|
|
1101
|
+
|
|
1102
|
+
Transfer file
|
|
1103
|
+
${backupPath.replace(/[\x00-\x1f\x7f-\x9f\p{Cf}\u2028\u2029]/gu, "?")}`, "Review encrypted wallet export");
|
|
1104
|
+
await confirm3("Export this wallet for https://wlt.ldgr.ltd?");
|
|
1105
|
+
const sourcePassphrase = await signingPassphrase(file, "Enter your source wallet passphrase");
|
|
1106
|
+
const transferPassphrase = await askPassphrase("Choose a separate transfer-file passphrase");
|
|
1107
|
+
const repeated = await askPassphrase("Repeat the transfer-file passphrase");
|
|
1108
|
+
if (transferPassphrase !== repeated)
|
|
1109
|
+
throw new Error("Passphrases did not match");
|
|
1110
|
+
await exportWebWalletFile({ walletPath: path, backupPath, sourcePassphrase, transferPassphrase, expectedIdentity: file });
|
|
1111
|
+
p2.log.info("Open https://wlt.ldgr.ltd/#import, choose Import encrypted wallet file, and select this file. Enter its passphrase only in the wallet. Delete the transfer file after import.");
|
|
1112
|
+
p2.outro("Encrypted web-wallet transfer file saved");
|
|
1113
|
+
}
|
|
1044
1114
|
async function runWalletList(path) {
|
|
1045
1115
|
const wallets = await listWalletFileMetadata(path);
|
|
1046
1116
|
const selected = wallets.find((wallet) => wallet.selected);
|
|
@@ -1189,6 +1259,9 @@ async function main() {
|
|
|
1189
1259
|
}
|
|
1190
1260
|
p2.intro("LDGR CLI");
|
|
1191
1261
|
try {
|
|
1262
|
+
if (first === "wallet" && second === "export-web" && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
1263
|
+
throw new Error("Web-wallet export requires an interactive terminal with terminal input and output");
|
|
1264
|
+
}
|
|
1192
1265
|
const resolved = parsed.walletPathExplicit ? { path: parsed.walletPath, migrated: false } : await migrateDefaultWalletPath();
|
|
1193
1266
|
if (resolved.migrated)
|
|
1194
1267
|
p2.log.info(`Moved legacy wallet store to ${resolved.path}`);
|
|
@@ -1198,6 +1271,8 @@ async function main() {
|
|
|
1198
1271
|
await runImport(resolved.path, parsed.walletOnTestnet, third);
|
|
1199
1272
|
else if (first === "wallet" && second === "export" && third && fourth === undefined)
|
|
1200
1273
|
await runExport(resolved.path, third);
|
|
1274
|
+
else if (first === "wallet" && second === "export-web" && third && fourth === undefined)
|
|
1275
|
+
await runExportWeb(resolved.path, third);
|
|
1201
1276
|
else if (first === "wallet" && second === "list" && third === undefined)
|
|
1202
1277
|
await runWalletList(resolved.path);
|
|
1203
1278
|
else if (first === "wallet" && second === "select" && third && fourth === undefined)
|
package/dist/wallet-file.d.ts
CHANGED
|
@@ -67,6 +67,10 @@ export declare function migrateDefaultWalletPath(input?: DefaultWalletPathInput)
|
|
|
67
67
|
readonly path: string;
|
|
68
68
|
readonly migrated: boolean;
|
|
69
69
|
}>;
|
|
70
|
+
export declare function unlockWalletFile(file: LocalWalletFile, passphrase?: string): Promise<{
|
|
71
|
+
readonly wallet: LdgrSigner;
|
|
72
|
+
readonly recoveryKey: string;
|
|
73
|
+
}>;
|
|
70
74
|
export declare function readEncryptedWalletFile(path: string, passphrase?: string): Promise<LdgrSigner>;
|
|
71
75
|
/** Writes the exact raw recovery-key text accepted by the extension importer. */
|
|
72
76
|
export declare function exportRecoveryKeyFile(input: {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type EncryptedWalletVault } from "@ldgr/wallet-core";
|
|
2
|
+
import type { LedgerId } from "@ldgr/protocol";
|
|
3
|
+
type WalletIdentity = {
|
|
4
|
+
readonly ledgerId: LedgerId;
|
|
5
|
+
readonly address: string;
|
|
6
|
+
readonly publicKey: string;
|
|
7
|
+
};
|
|
8
|
+
export type WebWalletTransfer = WalletIdentity & {
|
|
9
|
+
readonly format: "ldgr-web-wallet-transfer";
|
|
10
|
+
readonly version: 1;
|
|
11
|
+
readonly vault: EncryptedWalletVault;
|
|
12
|
+
};
|
|
13
|
+
/** Local-only export; never expose this operation through the public MCP module. */
|
|
14
|
+
export declare function exportWebWalletFile(input: {
|
|
15
|
+
readonly walletPath: string;
|
|
16
|
+
readonly backupPath: string;
|
|
17
|
+
readonly sourcePassphrase?: string | undefined;
|
|
18
|
+
readonly transferPassphrase: string;
|
|
19
|
+
readonly expectedIdentity?: WalletIdentity;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
export {};
|