@getalby/cli 0.2.3 → 0.3.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/README.md +55 -5
- package/build/commands/auth.js +66 -0
- package/build/commands/cancel-hold-invoice.js +1 -1
- package/build/commands/connect.js +46 -0
- package/build/commands/fetch-l402.js +1 -1
- package/build/commands/get-balance.js +1 -1
- package/build/commands/get-budget.js +1 -1
- package/build/commands/get-info.js +1 -1
- package/build/commands/get-wallet-service-info.js +1 -1
- package/build/commands/list-transactions.js +1 -1
- package/build/commands/lookup-invoice.js +1 -1
- package/build/commands/make-hold-invoice.js +1 -1
- package/build/commands/make-invoice.js +1 -1
- package/build/commands/pay-invoice.js +1 -1
- package/build/commands/pay-keysend.js +1 -1
- package/build/commands/settle-hold-invoice.js +1 -1
- package/build/commands/sign-message.js +1 -1
- package/build/commands/wait-for-payment.js +1 -1
- package/build/index.js +34 -10
- package/build/test/connection-secret.test.js +5 -2
- package/build/utils.js +111 -7
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -6,8 +6,58 @@ Built for agents - use with the [Alby Bitcoin Payments CLI Skill](https://github
|
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
|
+
### First-time setup
|
|
10
|
+
|
|
11
|
+
The CLI is an interface to a wallet and therefore needs a connection secret.
|
|
12
|
+
|
|
13
|
+
**Option 1: `auth` — for wallets that support it (e.g. Alby Hub)**
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Step 1: generate a connection URL and open it in your wallet to approve
|
|
17
|
+
# --app-name is the name of the agent/app that will use the wallet via the CLI (e.g. "Claude Code", "OpenClaw")
|
|
18
|
+
npx @getalby/cli auth https://my.albyhub.com --app-name "Claude Code"
|
|
19
|
+
|
|
20
|
+
# Step 2: after approving in the wallet, complete the connection
|
|
21
|
+
npx @getalby/cli auth --complete
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Option 2: `connect` — paste a NWC connection secret directly**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @getalby/cli connect "nostr+walletconnect://..."
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Multiple wallets
|
|
31
|
+
|
|
32
|
+
Use `--wallet-name` when setting up to save named connections:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @getalby/cli connect "nostr+walletconnect://..." --wallet-name work
|
|
36
|
+
npx @getalby/cli auth https://my.albyhub.com --app-name "Claude Code" --wallet-name personal
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then pass `--wallet-name` to any command to use that wallet:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx @getalby/cli --wallet-name work get-balance
|
|
43
|
+
npx @getalby/cli --wallet-name personal pay-invoice --invoice lnbc...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Connection secret resolution (in order of priority)
|
|
47
|
+
|
|
48
|
+
1. `--connection-secret` flag (value or path to file)
|
|
49
|
+
2. `--wallet-name` flag (`~/.alby-cli/connection-secret-<name>.key`)
|
|
50
|
+
3. `NWC_URL` environment variable
|
|
51
|
+
4. `~/.alby-cli/connection-secret.key` (default file location)
|
|
52
|
+
|
|
9
53
|
```bash
|
|
10
|
-
#
|
|
54
|
+
# Use the default saved wallet connection (preferred)
|
|
55
|
+
npx @getalby/cli <command> [options]
|
|
56
|
+
|
|
57
|
+
# Use a named wallet
|
|
58
|
+
npx @getalby/cli --wallet-name alice <command> [options]
|
|
59
|
+
|
|
60
|
+
# Pass a file path to a connection secret
|
|
11
61
|
npx @getalby/cli -c /path/to/secret.txt <command> [options]
|
|
12
62
|
|
|
13
63
|
# Or pass connection string directly
|
|
@@ -16,7 +66,7 @@ npx @getalby/cli -c "nostr+walletconnect://..." <command> [options]
|
|
|
16
66
|
|
|
17
67
|
The `-c` option auto-detects whether you're passing a connection string or a file path. You can get a connection string from your NWC-compatible wallet (e.g., [Alby](https://getalby.com)).
|
|
18
68
|
|
|
19
|
-
You can also
|
|
69
|
+
You can also set the `NWC_URL` environment variable instead of using the `-c` option:
|
|
20
70
|
|
|
21
71
|
```txt
|
|
22
72
|
NWC_URL="nostr+walletconnect://..."
|
|
@@ -42,7 +92,7 @@ curl -X POST "https://faucet.nwc.dev/wallets/<username>/topup?amount=5000"
|
|
|
42
92
|
|
|
43
93
|
### Wallet Commands
|
|
44
94
|
|
|
45
|
-
These commands require `--
|
|
95
|
+
These commands require a wallet connection (`-c`, `--wallet-name`, or `NWC_URL`):
|
|
46
96
|
|
|
47
97
|
```bash
|
|
48
98
|
# Get wallet balance
|
|
@@ -122,7 +172,7 @@ npx @getalby/cli request-invoice-from-lightning-address --address "hello@getalby
|
|
|
122
172
|
|
|
123
173
|
### Wallet Commands
|
|
124
174
|
|
|
125
|
-
These require `-c
|
|
175
|
+
These require a wallet connection (`-c`, `--wallet-name`, or `NWC_URL`):
|
|
126
176
|
|
|
127
177
|
| Command | Description | Required Options |
|
|
128
178
|
| ------------------------- | ------------------------------ | ------------------------------- |
|
|
@@ -141,7 +191,7 @@ These require `-c` or `--connection-secret`:
|
|
|
141
191
|
|
|
142
192
|
### HOLD Invoice Commands
|
|
143
193
|
|
|
144
|
-
These require `-c
|
|
194
|
+
These require a wallet connection (`-c`, `--wallet-name`, or `NWC_URL`):
|
|
145
195
|
|
|
146
196
|
| Command | Description | Required Options |
|
|
147
197
|
| --------------------- | --------------------- | ---------------------------- |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { NWCClient } from "@getalby/sdk";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getConnectionSecretPath, getPendingConnectionRelayPath, getPendingConnectionSecretPath, handleError, } from "../utils.js";
|
|
6
|
+
import { generateSecretKey, getPublicKey } from "nostr-tools";
|
|
7
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
|
8
|
+
export function registerAuthCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command("auth <wallet-url>")
|
|
11
|
+
.description("Securely connect a wallet with human confirmation via the browser\n\n" +
|
|
12
|
+
" Step 1: npx @getalby/cli auth https://my.albyhub.com --app-name MyApp\n" +
|
|
13
|
+
" Step 2: after human confirmation, run any command to finalize the connection")
|
|
14
|
+
.option("--app-name <name>", 'Name of the agent or app that will use this wallet (e.g. "Claude Code")')
|
|
15
|
+
.option("--relay-url <url>", "Relay URL for the pending connection", "wss://relay.getalby.com/v1")
|
|
16
|
+
.option("--force", "Overwrite existing connection secret")
|
|
17
|
+
.option("--remove-pending", "Remove a pending connection and start fresh")
|
|
18
|
+
.action(async (walletUrl, options) => {
|
|
19
|
+
await handleError(async () => {
|
|
20
|
+
const walletName = program.opts().walletName;
|
|
21
|
+
const connectionSecretPath = getConnectionSecretPath(walletName);
|
|
22
|
+
const pendingSecretPath = getPendingConnectionSecretPath(walletName);
|
|
23
|
+
const pendingRelayPath = getPendingConnectionRelayPath(walletName);
|
|
24
|
+
// Remove pending connection
|
|
25
|
+
if (options.removePending) {
|
|
26
|
+
if (!existsSync(pendingSecretPath)) {
|
|
27
|
+
console.log(`No pending connection found at ${pendingSecretPath}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
rmSync(pendingSecretPath);
|
|
31
|
+
console.log(`Removed pending connection at ${pendingSecretPath}`);
|
|
32
|
+
}
|
|
33
|
+
if (existsSync(pendingRelayPath)) {
|
|
34
|
+
rmSync(pendingRelayPath);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Generate auth URL
|
|
39
|
+
if (!options.appName) {
|
|
40
|
+
console.error(`Error: No app name provided.\n` +
|
|
41
|
+
`Add --app-name <name> to identify the app in the wallet.`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (existsSync(connectionSecretPath) && !options.force) {
|
|
45
|
+
console.error(`Error: Already connected. Connection secret exists at ${connectionSecretPath}\n` +
|
|
46
|
+
`To overwrite, use --force.`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const secret = bytesToHex(generateSecretKey());
|
|
50
|
+
const pubkey = getPublicKey(hexToBytes(secret));
|
|
51
|
+
const authUrl = NWCClient.getAuthorizationUrl(`${walletUrl}/apps/new`, { name: options.appName }, pubkey).toString();
|
|
52
|
+
const dir = join(homedir(), ".alby-cli");
|
|
53
|
+
if (!existsSync(dir)) {
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
writeFileSync(pendingSecretPath, secret, { mode: 0o600 });
|
|
57
|
+
writeFileSync(pendingRelayPath, options.relayUrl, { mode: 0o600 });
|
|
58
|
+
console.log("Click the following URL to approve the connection in your wallet:\n" +
|
|
59
|
+
authUrl);
|
|
60
|
+
const retryCmd = walletName
|
|
61
|
+
? `npx @getalby/cli get-balance --wallet-name ${walletName}`
|
|
62
|
+
: `npx @getalby/cli get-balance`;
|
|
63
|
+
console.log(`\nOnce approved, run any command, e.g.:\n ${retryCmd}`);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -7,7 +7,7 @@ export function registerCancelHoldInvoiceCommand(program) {
|
|
|
7
7
|
.requiredOption("--payment-hash <hex>", "Payment hash (32 bytes hex)")
|
|
8
8
|
.action(async (options) => {
|
|
9
9
|
await handleError(async () => {
|
|
10
|
-
const client = getClient(program);
|
|
10
|
+
const client = await getClient(program);
|
|
11
11
|
const result = await cancelHoldInvoice(client, {
|
|
12
12
|
payment_hash: options.paymentHash,
|
|
13
13
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { NWCClient } from "@getalby/sdk";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { getConnectionSecretPath, handleError, saveConnectionSecret, testAndLogConnection, } from "../utils.js";
|
|
4
|
+
export function registerConnectCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command('connect "[connection-secret]"')
|
|
7
|
+
.description("Connect to a Nostr Wallet Connect wallet")
|
|
8
|
+
.option("--force", "Overwrite existing connection secret")
|
|
9
|
+
.action(async (connectionSecret, options) => {
|
|
10
|
+
await handleError(async () => {
|
|
11
|
+
const connectionSecretPath = getConnectionSecretPath(program.opts().walletName);
|
|
12
|
+
if (existsSync(connectionSecretPath) && !options.force) {
|
|
13
|
+
console.error(`Error: Already connected. Connection secret exists at ${connectionSecretPath}\n` +
|
|
14
|
+
`To overwrite, use --force.`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
if (!connectionSecret) {
|
|
18
|
+
console.error(`Usage: npx @getalby/cli connect "<connection-secret>"\n` +
|
|
19
|
+
`Provide a NWC connection secret (nostr+walletconnect://...)`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
if (!connectionSecret.startsWith("nostr+walletconnect://")) {
|
|
23
|
+
console.error(`Error: Invalid connection secret. Expected format: nostr+walletconnect://...`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const client = new NWCClient({
|
|
27
|
+
nostrWalletConnectUrl: connectionSecret,
|
|
28
|
+
});
|
|
29
|
+
if (!client.secret || !/^[0-9a-f]{64}$/i.test(client.secret)) {
|
|
30
|
+
console.error(`Error: Invalid connection secret. Missing or invalid secret key.`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
if (!client.walletPubkey ||
|
|
34
|
+
!/^[0-9a-f]{64}$/i.test(client.walletPubkey)) {
|
|
35
|
+
console.error(`Error: Invalid connection secret. Missing or invalid wallet pubkey.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
if (!client.relayUrls?.length) {
|
|
39
|
+
console.error(`Error: Invalid connection secret. Missing relay URL.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
await testAndLogConnection(client);
|
|
43
|
+
saveConnectionSecret(connectionSecretPath, connectionSecret, program.opts().verbose);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -10,7 +10,7 @@ export function registerFetchL402Command(program) {
|
|
|
10
10
|
.option("-H, --headers <json>", "Additional headers (JSON string)")
|
|
11
11
|
.action(async (options) => {
|
|
12
12
|
await handleError(async () => {
|
|
13
|
-
const client = getClient(program);
|
|
13
|
+
const client = await getClient(program);
|
|
14
14
|
const result = await fetchL402(client, {
|
|
15
15
|
url: options.url,
|
|
16
16
|
method: options.method,
|
|
@@ -6,7 +6,7 @@ export function registerGetBalanceCommand(program) {
|
|
|
6
6
|
.description("Get wallet balance")
|
|
7
7
|
.action(async () => {
|
|
8
8
|
await handleError(async () => {
|
|
9
|
-
const client = getClient(program);
|
|
9
|
+
const client = await getClient(program);
|
|
10
10
|
const result = await getBalance(client);
|
|
11
11
|
output(result);
|
|
12
12
|
});
|
|
@@ -6,7 +6,7 @@ export function registerGetBudgetCommand(program) {
|
|
|
6
6
|
.description("Get wallet budget information")
|
|
7
7
|
.action(async () => {
|
|
8
8
|
await handleError(async () => {
|
|
9
|
-
const client = getClient(program);
|
|
9
|
+
const client = await getClient(program);
|
|
10
10
|
const result = await getBudget(client);
|
|
11
11
|
output(result);
|
|
12
12
|
});
|
|
@@ -6,7 +6,7 @@ export function registerGetInfoCommand(program) {
|
|
|
6
6
|
.description("Get wallet info")
|
|
7
7
|
.action(async () => {
|
|
8
8
|
await handleError(async () => {
|
|
9
|
-
const client = getClient(program);
|
|
9
|
+
const client = await getClient(program);
|
|
10
10
|
const result = await getInfo(client);
|
|
11
11
|
output(result);
|
|
12
12
|
});
|
|
@@ -6,7 +6,7 @@ export function registerGetWalletServiceInfoCommand(program) {
|
|
|
6
6
|
.description("Get wallet service capabilities")
|
|
7
7
|
.action(async () => {
|
|
8
8
|
await handleError(async () => {
|
|
9
|
-
const client = getClient(program);
|
|
9
|
+
const client = await getClient(program);
|
|
10
10
|
const result = await getWalletServiceInfo(client);
|
|
11
11
|
output(result);
|
|
12
12
|
});
|
|
@@ -12,7 +12,7 @@ export function registerListTransactionsCommand(program) {
|
|
|
12
12
|
.option("-t, --type <type>", "Filter by type (incoming|outgoing)")
|
|
13
13
|
.action(async (options) => {
|
|
14
14
|
await handleError(async () => {
|
|
15
|
-
const client = getClient(program);
|
|
15
|
+
const client = await getClient(program);
|
|
16
16
|
const result = await listTransactions(client, {
|
|
17
17
|
from: options.from,
|
|
18
18
|
until: options.until,
|
|
@@ -12,7 +12,7 @@ export function registerLookupInvoiceCommand(program) {
|
|
|
12
12
|
console.error("Error: --payment-hash or --invoice is required");
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
|
-
const client = getClient(program);
|
|
15
|
+
const client = await getClient(program);
|
|
16
16
|
const result = await lookupInvoice(client, {
|
|
17
17
|
payment_hash: options.paymentHash,
|
|
18
18
|
invoice: options.invoice,
|
|
@@ -10,7 +10,7 @@ export function registerMakeHoldInvoiceCommand(program) {
|
|
|
10
10
|
.option("-e, --expiry <seconds>", "Expiry time in seconds", parseInt)
|
|
11
11
|
.action(async (options) => {
|
|
12
12
|
await handleError(async () => {
|
|
13
|
-
const client = getClient(program);
|
|
13
|
+
const client = await getClient(program);
|
|
14
14
|
const result = await makeHoldInvoice(client, {
|
|
15
15
|
amount_in_sats: options.amount,
|
|
16
16
|
payment_hash: options.paymentHash,
|
|
@@ -9,7 +9,7 @@ export function registerMakeInvoiceCommand(program) {
|
|
|
9
9
|
.option("-e, --expiry <seconds>", "Expiry time in seconds", parseInt)
|
|
10
10
|
.action(async (options) => {
|
|
11
11
|
await handleError(async () => {
|
|
12
|
-
const client = getClient(program);
|
|
12
|
+
const client = await getClient(program);
|
|
13
13
|
const result = await makeInvoice(client, {
|
|
14
14
|
amount_in_sats: options.amount,
|
|
15
15
|
description: options.description,
|
|
@@ -8,7 +8,7 @@ export function registerPayInvoiceCommand(program) {
|
|
|
8
8
|
.option("-a, --amount <sats>", "Amount (for zero-amount invoices)", parseInt)
|
|
9
9
|
.action(async (options) => {
|
|
10
10
|
await handleError(async () => {
|
|
11
|
-
const client = getClient(program);
|
|
11
|
+
const client = await getClient(program);
|
|
12
12
|
const result = await payInvoice(client, {
|
|
13
13
|
invoice: options.invoice,
|
|
14
14
|
amount_in_sats: options.amount,
|
|
@@ -10,7 +10,7 @@ export function registerPayKeysendCommand(program) {
|
|
|
10
10
|
.option("--tlv-records <json>", "TLV records as JSON array [{type, value}]")
|
|
11
11
|
.action(async (options) => {
|
|
12
12
|
await handleError(async () => {
|
|
13
|
-
const client = getClient(program);
|
|
13
|
+
const client = await getClient(program);
|
|
14
14
|
let tlvRecords;
|
|
15
15
|
if (options.tlvRecords) {
|
|
16
16
|
tlvRecords = JSON.parse(options.tlvRecords);
|
|
@@ -7,7 +7,7 @@ export function registerSettleHoldInvoiceCommand(program) {
|
|
|
7
7
|
.requiredOption("--preimage <hex>", "Preimage (32 bytes hex)")
|
|
8
8
|
.action(async (options) => {
|
|
9
9
|
await handleError(async () => {
|
|
10
|
-
const client = getClient(program);
|
|
10
|
+
const client = await getClient(program);
|
|
11
11
|
const result = await settleHoldInvoice(client, {
|
|
12
12
|
preimage: options.preimage,
|
|
13
13
|
});
|
|
@@ -7,7 +7,7 @@ export function registerSignMessageCommand(program) {
|
|
|
7
7
|
.requiredOption("-m, --message <text>", "Message to sign")
|
|
8
8
|
.action(async (options) => {
|
|
9
9
|
await handleError(async () => {
|
|
10
|
-
const client = getClient(program);
|
|
10
|
+
const client = await getClient(program);
|
|
11
11
|
const result = await signMessage(client, {
|
|
12
12
|
message: options.message,
|
|
13
13
|
});
|
|
@@ -9,7 +9,7 @@ export function registerWaitForPaymentCommand(program) {
|
|
|
9
9
|
.option("--timeout <seconds>", "Timeout in seconds", parseInt)
|
|
10
10
|
.action(async (options) => {
|
|
11
11
|
await handleError(async () => {
|
|
12
|
-
const client = getClient(program);
|
|
12
|
+
const client = await getClient(program);
|
|
13
13
|
const result = await waitForPayment(client, {
|
|
14
14
|
payment_hash: options.paymentHash,
|
|
15
15
|
type: options.type,
|
package/build/index.js
CHANGED
|
@@ -20,37 +20,61 @@ import { registerParseInvoiceCommand } from "./commands/parse-invoice.js";
|
|
|
20
20
|
import { registerVerifyPreimageCommand } from "./commands/verify-preimage.js";
|
|
21
21
|
import { registerRequestInvoiceFromLightningAddressCommand } from "./commands/request-invoice-from-lightning-address.js";
|
|
22
22
|
import { registerFetchL402Command } from "./commands/fetch-l402.js";
|
|
23
|
+
import { registerConnectCommand } from "./commands/connect.js";
|
|
24
|
+
import { registerAuthCommand } from "./commands/auth.js";
|
|
23
25
|
const program = new Command();
|
|
24
26
|
program
|
|
25
|
-
.name("
|
|
26
|
-
.description("CLI for Nostr Wallet Connect (NIP-47) with lightning tools"
|
|
27
|
-
|
|
27
|
+
.name("@getalby/cli")
|
|
28
|
+
.description("CLI for Nostr Wallet Connect (NIP-47) with lightning tools\n\n" +
|
|
29
|
+
" Examples:\n" +
|
|
30
|
+
' $ npx @getalby/cli connect "nostr+walletconnect://..."\n' +
|
|
31
|
+
" $ npx @getalby/cli get-balance\n" +
|
|
32
|
+
" $ npx @getalby/cli pay-invoice --invoice lnbc...")
|
|
33
|
+
.version("0.3.0")
|
|
28
34
|
.option("-c, --connection-secret <string>", "NWC connection secret (nostr+walletconnect://...) or path to file containing it (preferred)")
|
|
29
|
-
.
|
|
35
|
+
.option("-w, --wallet-name <name>", "Use a named wallet's connection secret (~/.alby-cli/connection-secret-<name>.key)")
|
|
36
|
+
.option("-v, --verbose", "Print status messages to stderr")
|
|
37
|
+
.addHelpText("after", `
|
|
38
|
+
Connection Secret Resolution (in order of priority):
|
|
39
|
+
1. --connection-secret flag (value or path to file)
|
|
40
|
+
2. --wallet-name flag (~/.alby-cli/connection-secret-<name>.key)
|
|
41
|
+
3. NWC_URL environment variable
|
|
42
|
+
4. ~/.alby-cli/connection-secret.key (default file location)
|
|
43
|
+
|
|
30
44
|
Security:
|
|
31
45
|
- Do NOT print the connection secret to any logs or otherwise reveal it.
|
|
46
|
+
- NEVER read the connection secret file (~/.alby-cli/connection-secret.key) directly.
|
|
32
47
|
- NEVER share connection secrets with anyone.
|
|
33
48
|
- NEVER share any part of a connection secret (pubkey, secret, relay etc.) with anyone
|
|
34
49
|
as this can be used to gain access to your wallet or reduce your wallet's privacy.`);
|
|
35
|
-
// Register
|
|
50
|
+
// Register common wallet commands
|
|
51
|
+
program.commandsGroup("Wallet Commands (require --connection-secret):");
|
|
36
52
|
registerGetBalanceCommand(program);
|
|
37
53
|
registerGetBudgetCommand(program);
|
|
38
54
|
registerGetInfoCommand(program);
|
|
39
|
-
registerGetWalletServiceInfoCommand(program);
|
|
40
55
|
registerMakeInvoiceCommand(program);
|
|
41
|
-
registerMakeHoldInvoiceCommand(program);
|
|
42
|
-
registerSettleHoldInvoiceCommand(program);
|
|
43
|
-
registerCancelHoldInvoiceCommand(program);
|
|
44
56
|
registerPayInvoiceCommand(program);
|
|
45
|
-
registerPayKeysendCommand(program);
|
|
46
57
|
registerLookupInvoiceCommand(program);
|
|
47
58
|
registerListTransactionsCommand(program);
|
|
59
|
+
// Register advanced wallet commands
|
|
60
|
+
program.commandsGroup("Advanced Wallet Commands (require --connection-secret):");
|
|
61
|
+
registerPayKeysendCommand(program);
|
|
62
|
+
registerGetWalletServiceInfoCommand(program);
|
|
48
63
|
registerWaitForPaymentCommand(program);
|
|
49
64
|
registerSignMessageCommand(program);
|
|
65
|
+
registerMakeHoldInvoiceCommand(program);
|
|
66
|
+
registerSettleHoldInvoiceCommand(program);
|
|
67
|
+
registerCancelHoldInvoiceCommand(program);
|
|
68
|
+
// Register lightning tool commands
|
|
69
|
+
program.commandsGroup("Lightning Tools (no --connection-secret required):");
|
|
50
70
|
registerFiatToSatsCommand(program);
|
|
51
71
|
registerSatsToFiatCommand(program);
|
|
52
72
|
registerParseInvoiceCommand(program);
|
|
53
73
|
registerVerifyPreimageCommand(program);
|
|
54
74
|
registerRequestInvoiceFromLightningAddressCommand(program);
|
|
55
75
|
registerFetchL402Command(program);
|
|
76
|
+
// Register setup commands
|
|
77
|
+
program.commandsGroup("Setup:");
|
|
78
|
+
registerAuthCommand(program);
|
|
79
|
+
registerConnectCommand(program);
|
|
56
80
|
program.parse();
|
|
@@ -45,9 +45,12 @@ describe("Connection Secret Handling", () => {
|
|
|
45
45
|
expect(result.output.error).toContain("Invalid connection secret");
|
|
46
46
|
});
|
|
47
47
|
test("errors when no connection secret provided", () => {
|
|
48
|
-
const result = runCli("get-balance"
|
|
48
|
+
const result = runCli("get-balance", {
|
|
49
|
+
HOME: "/nonexistent-test-home",
|
|
50
|
+
NWC_URL: "",
|
|
51
|
+
});
|
|
49
52
|
expect(result.success).toBe(false);
|
|
50
|
-
expect(result.output.error).toContain("
|
|
53
|
+
expect(result.output.error).toContain("No connection secret provided");
|
|
51
54
|
});
|
|
52
55
|
test("errors when connection string is malformed", () => {
|
|
53
56
|
const result = runCli(`-c "nostr+walletconnect://asdf" get-balance`);
|
package/build/utils.js
CHANGED
|
@@ -1,15 +1,119 @@
|
|
|
1
|
-
import { NWCClient } from "@getalby/sdk";
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { NWAClient, NWCClient } from "@getalby/sdk";
|
|
2
|
+
import { getInfo } from "./tools/nwc/get_info.js";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
function sanitizeWalletName(name) {
|
|
7
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
8
|
+
}
|
|
9
|
+
export function getConnectionSecretPath(name) {
|
|
10
|
+
const filename = name
|
|
11
|
+
? `connection-secret-${sanitizeWalletName(name)}.key`
|
|
12
|
+
: "connection-secret.key";
|
|
13
|
+
return join(homedir(), ".alby-cli", filename);
|
|
14
|
+
}
|
|
15
|
+
export function getPendingConnectionSecretPath(name) {
|
|
16
|
+
const filename = name
|
|
17
|
+
? `pending-connection-secret-${sanitizeWalletName(name)}.key`
|
|
18
|
+
: "pending-connection-secret.key";
|
|
19
|
+
return join(homedir(), ".alby-cli", filename);
|
|
20
|
+
}
|
|
21
|
+
export function getPendingConnectionRelayPath(name) {
|
|
22
|
+
const filename = name
|
|
23
|
+
? `pending-connection-relay-${sanitizeWalletName(name)}.txt`
|
|
24
|
+
: "pending-connection-relay.txt";
|
|
25
|
+
return join(homedir(), ".alby-cli", filename);
|
|
26
|
+
}
|
|
27
|
+
export function saveConnectionSecret(path, secret, verbose) {
|
|
28
|
+
const alreadyExists = existsSync(path);
|
|
29
|
+
const dir = join(homedir(), ".alby-cli");
|
|
30
|
+
if (!existsSync(dir)) {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
writeFileSync(path, secret, { mode: 0o600 });
|
|
34
|
+
if (alreadyExists) {
|
|
35
|
+
chmodSync(path, 0o600);
|
|
36
|
+
}
|
|
37
|
+
if (verbose) {
|
|
38
|
+
console.error(`Connection saved to ${path}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function testAndLogConnection(client) {
|
|
42
|
+
console.log("Testing connection...");
|
|
43
|
+
const info = await getInfo(client);
|
|
44
|
+
console.log(`Connected to ${info.alias || "wallet"} (${info.network || "unknown network"})`);
|
|
45
|
+
}
|
|
46
|
+
export async function completePendingConnection(pendingSecretPath, connectionSecretPath, relayUrl, verbose, pendingRelayPath) {
|
|
47
|
+
const secret = readFileSync(pendingSecretPath, "utf-8").trim();
|
|
48
|
+
const DEFAULT_RELAY = "wss://relay.getalby.com/v1";
|
|
49
|
+
if (!relayUrl && pendingRelayPath && existsSync(pendingRelayPath)) {
|
|
50
|
+
relayUrl = readFileSync(pendingRelayPath, "utf-8").trim();
|
|
51
|
+
}
|
|
52
|
+
const resolvedRelay = relayUrl ?? DEFAULT_RELAY;
|
|
53
|
+
const nwaClient = new NWAClient({
|
|
54
|
+
appSecretKey: secret,
|
|
55
|
+
relayUrls: [resolvedRelay],
|
|
56
|
+
requestMethods: [],
|
|
57
|
+
});
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let settled = false;
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
if (settled)
|
|
62
|
+
return;
|
|
63
|
+
settled = true;
|
|
64
|
+
unsub?.();
|
|
65
|
+
reject(new Error("Timed out waiting for wallet approval.\n\nTo retry, run the command again.\nTo cancel: npx @getalby/cli auth --remove-pending"));
|
|
66
|
+
}, 5000);
|
|
67
|
+
let unsub;
|
|
68
|
+
nwaClient
|
|
69
|
+
.subscribe({
|
|
70
|
+
onSuccess: async (nwcClient) => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
settled = true;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
unsub?.();
|
|
76
|
+
saveConnectionSecret(connectionSecretPath, nwcClient.getNostrWalletConnectUrl(), verbose);
|
|
77
|
+
rmSync(pendingSecretPath);
|
|
78
|
+
if (pendingRelayPath && existsSync(pendingRelayPath)) {
|
|
79
|
+
rmSync(pendingRelayPath);
|
|
80
|
+
}
|
|
81
|
+
resolve(nwcClient);
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
.then(({ unsub: u }) => {
|
|
85
|
+
unsub = u;
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export async function getClient(program) {
|
|
4
90
|
const opts = program.opts();
|
|
5
91
|
let connectionSecret = opts.connectionSecret;
|
|
6
|
-
|
|
7
|
-
|
|
92
|
+
const walletName = opts.walletName;
|
|
93
|
+
const connectionPath = getConnectionSecretPath(walletName);
|
|
94
|
+
const pendingPath = getPendingConnectionSecretPath(walletName);
|
|
95
|
+
if (!connectionSecret && !walletName) {
|
|
8
96
|
connectionSecret = process.env.NWC_URL;
|
|
9
97
|
}
|
|
10
98
|
if (!connectionSecret) {
|
|
11
|
-
|
|
12
|
-
|
|
99
|
+
try {
|
|
100
|
+
connectionSecret = readFileSync(connectionPath, "utf-8").trim();
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const err = error;
|
|
104
|
+
if (err.code !== "ENOENT")
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!connectionSecret && existsSync(pendingPath)) {
|
|
109
|
+
if (opts.verbose) {
|
|
110
|
+
console.error("Pending connection found. Waiting for wallet approval...");
|
|
111
|
+
}
|
|
112
|
+
const pendingRelayPath = getPendingConnectionRelayPath(walletName);
|
|
113
|
+
return await completePendingConnection(pendingPath, connectionPath, undefined, opts.verbose, pendingRelayPath);
|
|
114
|
+
}
|
|
115
|
+
if (!connectionSecret) {
|
|
116
|
+
throw new Error("No connection secret provided. Pass -c <secret or file path>, set NWC_URL, use --wallet-name <name>, or create ~/.alby-cli/connection-secret.key");
|
|
13
117
|
}
|
|
14
118
|
// Auto-detect: if it doesn't start with the protocol, treat as file path
|
|
15
119
|
if (!connectionSecret.startsWith("nostr+walletconnect://")) {
|
package/package.json
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
"name": "@getalby/cli",
|
|
3
3
|
"description": "CLI for Nostr Wallet Connect (NIP-47) with a few additional useful lightning tools",
|
|
4
4
|
"repository": "https://github.com/getAlby/cli.git",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "build/index.js",
|
|
8
8
|
"bin": {
|
|
9
|
-
"cli": "build/index.js"
|
|
10
|
-
"alby-cli": "build/index.js"
|
|
9
|
+
"cli": "build/index.js"
|
|
11
10
|
},
|
|
12
11
|
"files": [
|
|
13
12
|
"build/**/*"
|
|
@@ -33,10 +32,15 @@
|
|
|
33
32
|
],
|
|
34
33
|
"author": "Alby contributors",
|
|
35
34
|
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
36
38
|
"dependencies": {
|
|
37
39
|
"@getalby/lightning-tools": "^7.0.2",
|
|
38
40
|
"@getalby/sdk": "^7.0.0",
|
|
39
|
-
"
|
|
41
|
+
"@noble/hashes": "^2.0.1",
|
|
42
|
+
"commander": "^14.0.3",
|
|
43
|
+
"nostr-tools": "^2.23.3"
|
|
40
44
|
},
|
|
41
45
|
"devDependencies": {
|
|
42
46
|
"@types/node": "^25.2.0",
|