@getalby/cli 0.6.1 → 0.7.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 +26 -6
- package/build/commands/auth.js +11 -7
- package/build/commands/connect.js +2 -1
- package/build/commands/list-wallets.js +12 -0
- package/build/commands/pay-crypto.js +47 -0
- package/build/commands/pay-invoice.js +3 -3
- package/build/commands/pay.js +188 -0
- package/build/commands/receive.js +40 -0
- package/build/index.js +15 -2
- package/build/lendaswap/swap.js +177 -0
- package/build/test/connection-secret.test.js +2 -2
- package/build/test/lightning-tools.test.js +1 -1
- package/build/test/list-wallets.test.js +75 -0
- package/build/test/nwc-hold-invoices.test.js +0 -2
- package/build/test/nwc-payments.test.js +2 -2
- package/build/test/pay-command.test.js +84 -0
- package/build/test/pay-crypto.test.js +168 -0
- package/build/test/receive-command.test.js +42 -0
- package/build/utils.js +74 -12
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -40,7 +40,13 @@ Then pass `--wallet-name` to any command to use that wallet:
|
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
42
|
npx @getalby/cli --wallet-name work get-balance
|
|
43
|
-
npx @getalby/cli --wallet-name personal pay
|
|
43
|
+
npx @getalby/cli --wallet-name personal pay lnbc...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
List the wallets you've configured (names and connection status only, never the secrets):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx @getalby/cli list-wallets
|
|
44
50
|
```
|
|
45
51
|
|
|
46
52
|
### Connection secret resolution (in order of priority)
|
|
@@ -104,11 +110,25 @@ npx @getalby/cli get-wallet-service-info
|
|
|
104
110
|
# Create an invoice
|
|
105
111
|
npx @getalby/cli make-invoice --amount 1000 --description "Payment"
|
|
106
112
|
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
npx @getalby/cli
|
|
113
|
+
# Get paid — returns the wallet's lightning address, or a BOLT-11 invoice if --amount is given.
|
|
114
|
+
# - With no args: returns the wallet's lightning address (errors if the wallet has none)
|
|
115
|
+
npx @getalby/cli receive
|
|
116
|
+
# - With --amount: returns a BOLT-11 invoice for that amount; --description is optional
|
|
117
|
+
npx @getalby/cli receive --amount 100 --description "coffee"
|
|
118
|
+
|
|
119
|
+
# Pay any supported destination — auto-detects type from the destination string.
|
|
120
|
+
# Required args depend on the destination type:
|
|
121
|
+
# - BOLT-11 invoice (lnbc...): no extra args (use --amount only for zero-amount invoices)
|
|
122
|
+
npx @getalby/cli pay "lnbc..."
|
|
123
|
+
# - Lightning address (user@domain): requires --amount (sats); optional --comment
|
|
124
|
+
npx @getalby/cli pay alice@getalby.com --amount 100 --comment "hi"
|
|
125
|
+
# - Node pubkey (66-char hex, compressed secp256k1): keysend, requires --amount (sats)
|
|
126
|
+
npx @getalby/cli pay 02abc... --amount 100
|
|
127
|
+
# - EVM address (0x...): pay crypto/stablecoin, requires --amount, --currency, and --network
|
|
128
|
+
npx @getalby/cli pay 0xabc... --amount 10 --currency USDC --network arbitrum
|
|
129
|
+
|
|
130
|
+
# The dedicated `pay-invoice`, `pay-keysend`, and `pay-crypto` commands are
|
|
131
|
+
# still available if you want to constrain the destination type explicitly.
|
|
112
132
|
|
|
113
133
|
# Look up an invoice by payment hash
|
|
114
134
|
npx @getalby/cli lookup-invoice --payment-hash "abc123..."
|
package/build/commands/auth.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { NWCClient } from "@getalby/sdk";
|
|
2
2
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { getConnectionSecretPath, getPendingConnectionRelayPath, getPendingConnectionSecretPath, handleError, } from "../utils.js";
|
|
3
|
+
import { getAlbyCliDir, DEFAULT_RELAY_URLS, getConnectionSecretPath, getPendingConnectionRelayPath, getPendingConnectionSecretPath, handleError, } from "../utils.js";
|
|
6
4
|
import { generateSecretKey, getPublicKey } from "nostr-tools";
|
|
7
5
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
|
8
6
|
export function registerAuthCommand(program) {
|
|
@@ -12,7 +10,7 @@ export function registerAuthCommand(program) {
|
|
|
12
10
|
" Step 1: npx @getalby/cli auth https://my.albyhub.com --app-name MyApp\n" +
|
|
13
11
|
" Step 2: after human confirmation, run any command to finalize the connection")
|
|
14
12
|
.option("--app-name <name>", 'Name of the agent or app that will use this wallet (e.g. "Claude Code")')
|
|
15
|
-
.option("--relay-url <url>",
|
|
13
|
+
.option("--relay-url <url>", `Relay URL for the pending connection (repeat to use multiple relays, default: ${DEFAULT_RELAY_URLS.join(", ")})`, (value, previous) => previous.concat([value]), [])
|
|
16
14
|
.option("--force", "Overwrite existing connection secret")
|
|
17
15
|
.option("--remove-pending", "Remove a pending connection and start fresh")
|
|
18
16
|
.action(async (walletUrl, options) => {
|
|
@@ -43,18 +41,24 @@ export function registerAuthCommand(program) {
|
|
|
43
41
|
}
|
|
44
42
|
if (existsSync(connectionSecretPath) && !options.force) {
|
|
45
43
|
console.error(`Error: Already connected. Connection secret exists at ${connectionSecretPath}\n` +
|
|
46
|
-
`To overwrite, use --force
|
|
44
|
+
`To overwrite, use --force.\n` +
|
|
45
|
+
`To connect an additional wallet, use --wallet-name <name>.`);
|
|
47
46
|
process.exit(1);
|
|
48
47
|
}
|
|
48
|
+
const relayUrls = options.relayUrl.length > 0 ? options.relayUrl : DEFAULT_RELAY_URLS;
|
|
49
49
|
const secret = bytesToHex(generateSecretKey());
|
|
50
50
|
const pubkey = getPublicKey(hexToBytes(secret));
|
|
51
51
|
const authUrl = NWCClient.getAuthorizationUrl(`${walletUrl}/apps/new`, { name: options.appName }, pubkey).toString();
|
|
52
|
-
const dir =
|
|
52
|
+
const dir = getAlbyCliDir();
|
|
53
53
|
if (!existsSync(dir)) {
|
|
54
54
|
mkdirSync(dir, { recursive: true });
|
|
55
55
|
}
|
|
56
56
|
writeFileSync(pendingSecretPath, secret, { mode: 0o600 });
|
|
57
|
-
|
|
57
|
+
// Store one relay per line so multiple relays survive the
|
|
58
|
+
// round-trip to the completion step.
|
|
59
|
+
writeFileSync(pendingRelayPath, relayUrls.join("\n"), {
|
|
60
|
+
mode: 0o600,
|
|
61
|
+
});
|
|
58
62
|
console.log("Click the following URL to approve the connection in your wallet:\n" +
|
|
59
63
|
authUrl);
|
|
60
64
|
const retryCmd = walletName
|
|
@@ -11,7 +11,8 @@ export function registerConnectCommand(program) {
|
|
|
11
11
|
const connectionSecretPath = getConnectionSecretPath(program.opts().walletName);
|
|
12
12
|
if (existsSync(connectionSecretPath) && !options.force) {
|
|
13
13
|
console.error(`Error: Already connected. Connection secret exists at ${connectionSecretPath}\n` +
|
|
14
|
-
`To overwrite, use --force
|
|
14
|
+
`To overwrite, use --force.\n` +
|
|
15
|
+
`To connect an additional wallet, use --wallet-name <name>.`);
|
|
15
16
|
process.exit(1);
|
|
16
17
|
}
|
|
17
18
|
if (!connectionSecret) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getAlbyCliDir, handleError, listWallets, output } from "../utils.js";
|
|
2
|
+
export function registerListWalletsCommand(program) {
|
|
3
|
+
program
|
|
4
|
+
.command("list-wallets")
|
|
5
|
+
.description("List configured wallets (names and connection status only, no secrets)")
|
|
6
|
+
.action(async () => {
|
|
7
|
+
await handleError(async () => {
|
|
8
|
+
const wallets = listWallets();
|
|
9
|
+
output({ directory: getAlbyCliDir(), wallets });
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { payInvoice } from "../tools/nwc/pay_invoice.js";
|
|
2
|
+
import { getClient, handleError, output } from "../utils.js";
|
|
3
|
+
import { isPlausibleEvmAddress, payCrypto, findSupportedPair, } from "../lendaswap/swap.js";
|
|
4
|
+
export function registerPayCryptoCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("pay-crypto")
|
|
7
|
+
.description("Pay any supported crypto or stablecoin address from your bitcoin lightning wallet.\n\n" +
|
|
8
|
+
"If the requested currency/network pair isn't supported you'll get an error listing the pairs that are.")
|
|
9
|
+
.argument("<address>", "Recipient address on the target network")
|
|
10
|
+
.requiredOption("-a, --amount <number>", "Amount to send in target-currency units (e.g. 10 = 10 USDC)", Number)
|
|
11
|
+
.requiredOption("--currency <name>", "Target currency (e.g. USDC)")
|
|
12
|
+
.requiredOption("--network <name>", "Target network (chain name or id, e.g. arbitrum / 42161)")
|
|
13
|
+
.addHelpText("after", "\nExample:\n" +
|
|
14
|
+
" $ npx @getalby/cli pay-crypto 0xabc... --amount 10 --currency USDC --network arbitrum\n")
|
|
15
|
+
.action(async (address, options) => {
|
|
16
|
+
await handleError(async () => {
|
|
17
|
+
if (!Number.isFinite(options.amount) || options.amount <= 0) {
|
|
18
|
+
throw new Error(`Invalid --amount: ${options.amount}`);
|
|
19
|
+
}
|
|
20
|
+
if (!isPlausibleEvmAddress(address)) {
|
|
21
|
+
throw new Error(`Recipient address does not look valid (expected 0x + 40 hex chars): ${address}`);
|
|
22
|
+
}
|
|
23
|
+
// Validate the pair against the live Lendaswap catalog before
|
|
24
|
+
// asking the user for their wallet — fast feedback on typos.
|
|
25
|
+
const pair = await findSupportedPair(options.currency, options.network);
|
|
26
|
+
const nwc = await getClient(program);
|
|
27
|
+
const { swapId } = await payCrypto({
|
|
28
|
+
pair,
|
|
29
|
+
amount: options.amount,
|
|
30
|
+
targetAddress: address,
|
|
31
|
+
payInvoice: async (bolt11Invoice) => {
|
|
32
|
+
await payInvoice(nwc, { invoice: bolt11Invoice });
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
output({
|
|
36
|
+
swap_id: swapId,
|
|
37
|
+
status: "completed",
|
|
38
|
+
target: {
|
|
39
|
+
address,
|
|
40
|
+
currency: pair.symbol,
|
|
41
|
+
network: pair.network,
|
|
42
|
+
amount: options.amount,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -4,13 +4,13 @@ export function registerPayInvoiceCommand(program) {
|
|
|
4
4
|
program
|
|
5
5
|
.command("pay-invoice")
|
|
6
6
|
.description("Pay a lightning invoice")
|
|
7
|
-
.
|
|
7
|
+
.argument("<bolt11>", "Invoice to pay")
|
|
8
8
|
.option("-a, --amount <sats>", "Amount (for zero-amount invoices)", parseInt)
|
|
9
|
-
.action(async (options) => {
|
|
9
|
+
.action(async (invoice, options) => {
|
|
10
10
|
await handleError(async () => {
|
|
11
11
|
const client = await getClient(program);
|
|
12
12
|
const result = await payInvoice(client, {
|
|
13
|
-
invoice
|
|
13
|
+
invoice,
|
|
14
14
|
amount_in_sats: options.amount,
|
|
15
15
|
});
|
|
16
16
|
output(result);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { LN_ADDRESS_REGEX } from "@getalby/lightning-tools";
|
|
2
|
+
import { payInvoice } from "../tools/nwc/pay_invoice.js";
|
|
3
|
+
import { payKeysend } from "../tools/nwc/pay_keysend.js";
|
|
4
|
+
import { requestInvoiceFromLightningAddress } from "../tools/lightning/request_invoice_from_lightning_address.js";
|
|
5
|
+
import { isPlausibleEvmAddress, payCrypto, findSupportedPair, } from "../lendaswap/swap.js";
|
|
6
|
+
import { getClient, handleError, output } from "../utils.js";
|
|
7
|
+
function detectDestinationType(destination) {
|
|
8
|
+
if (/^0x[0-9a-fA-F]{40}$/.test(destination))
|
|
9
|
+
return "crypto";
|
|
10
|
+
// BOLT-11 prefixes: lnbc = mainnet, lntb = testnet/signet, lnbcrt = regtest, lntbs = signet (e.g. mutinynet).
|
|
11
|
+
if (/^ln(bcrt|tbs|bc|tb)/i.test(destination))
|
|
12
|
+
return "invoice";
|
|
13
|
+
if (LN_ADDRESS_REGEX.test(destination))
|
|
14
|
+
return "lightning-address";
|
|
15
|
+
if (/^0[23][0-9a-fA-F]{64}$/.test(destination))
|
|
16
|
+
return "keysend";
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const ALLOWED_OPTS = {
|
|
20
|
+
invoice: ["amount"],
|
|
21
|
+
"lightning-address": ["amount", "comment"],
|
|
22
|
+
keysend: ["amount", "preimage", "tlvRecords"],
|
|
23
|
+
crypto: ["amount", "currency", "network"],
|
|
24
|
+
};
|
|
25
|
+
const OPT_FLAG = {
|
|
26
|
+
amount: "--amount",
|
|
27
|
+
comment: "--comment",
|
|
28
|
+
preimage: "--preimage",
|
|
29
|
+
tlvRecords: "--tlv-records",
|
|
30
|
+
currency: "--currency",
|
|
31
|
+
network: "--network",
|
|
32
|
+
};
|
|
33
|
+
function rejectUnusedOpts(type, options, providedKeys) {
|
|
34
|
+
const allowed = new Set(ALLOWED_OPTS[type]);
|
|
35
|
+
const used = Object.keys(options).filter((k) => providedKeys.has(k));
|
|
36
|
+
const stray = used.filter((k) => !allowed.has(k));
|
|
37
|
+
if (stray.length > 0) {
|
|
38
|
+
throw new Error(`Option${stray.length > 1 ? "s" : ""} ${stray.map((k) => OPT_FLAG[k] ?? `--${k}`).join(", ")} not applicable to ${type} payment`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function registerPayCommand(program) {
|
|
42
|
+
program
|
|
43
|
+
.command("pay")
|
|
44
|
+
.description("Pay any supported destination — auto-detects type from the destination string.\n\n" +
|
|
45
|
+
"Supported destinations:\n" +
|
|
46
|
+
" - BOLT-11 invoice (lnbc... / lntb... / lnbcrt... / lntbs...): no extra args (use --amount only for zero-amount invoices)\n" +
|
|
47
|
+
" - Lightning address (user@domain): requires --amount (sats); optional --comment\n" +
|
|
48
|
+
" - Node pubkey (66-char hex, compressed secp256k1): keysend, requires --amount (sats)\n" +
|
|
49
|
+
" - EVM address (0x...): pay crypto/stablecoin, requires --amount, --currency, and --network")
|
|
50
|
+
.argument("<destination>", "Invoice, lightning address, node pubkey, or EVM address")
|
|
51
|
+
.option("-a, --amount <number>", "Amount — sats for lightning destinations, target-currency units for crypto (e.g. 10 = 10 USDC)", Number)
|
|
52
|
+
.option("--comment <text>", "Comment for lightning address payments")
|
|
53
|
+
.option("--preimage <hex>", "Preimage for keysend (optional, generated if omitted)")
|
|
54
|
+
.option("--tlv-records <json>", "TLV records for keysend, as JSON array [{type, value}]")
|
|
55
|
+
.option("--currency <name>", "Target currency for crypto payments (required for EVM destinations)")
|
|
56
|
+
.option("--network <name>", "Target network for crypto payments — chain name or id (required for EVM destinations)")
|
|
57
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
58
|
+
" $ npx @getalby/cli pay lnbc1...\n" +
|
|
59
|
+
" $ npx @getalby/cli pay alice@getalby.com --amount 100 --comment hi\n" +
|
|
60
|
+
" $ npx @getalby/cli pay 02aabb... --amount 100\n" +
|
|
61
|
+
" $ npx @getalby/cli pay 0xabc... --amount 10 --currency USDC --network arbitrum\n")
|
|
62
|
+
.action(async (destination, options, cmd) => {
|
|
63
|
+
await handleError(async () => {
|
|
64
|
+
const type = detectDestinationType(destination);
|
|
65
|
+
if (!type) {
|
|
66
|
+
throw new Error(`Could not detect destination type for: ${destination}\n` +
|
|
67
|
+
"Expected one of:\n" +
|
|
68
|
+
" - BOLT-11 invoice (starts with lnbc, lntb, lnbcrt, or lntbs)\n" +
|
|
69
|
+
" - Lightning address (user@domain)\n" +
|
|
70
|
+
" - Node pubkey for keysend (66-char hex, compressed secp256k1: starts with 02/03)\n" +
|
|
71
|
+
" - EVM address (0x + 40 hex characters)");
|
|
72
|
+
}
|
|
73
|
+
// Track which options the user *explicitly* set (vs. defaults from
|
|
74
|
+
// commander) so we only reject stray flags the user actually typed.
|
|
75
|
+
const providedKeys = new Set();
|
|
76
|
+
for (const opt of cmd.options) {
|
|
77
|
+
const key = opt.attributeName();
|
|
78
|
+
const src = cmd.getOptionValueSource(key);
|
|
79
|
+
if (src === "cli" || src === "env") {
|
|
80
|
+
providedKeys.add(key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
rejectUnusedOpts(type, options, providedKeys);
|
|
84
|
+
switch (type) {
|
|
85
|
+
case "invoice": {
|
|
86
|
+
if (options.amount !== undefined &&
|
|
87
|
+
!Number.isInteger(options.amount)) {
|
|
88
|
+
throw new Error(`Invalid --amount: must be an integer number of sats`);
|
|
89
|
+
}
|
|
90
|
+
const client = await getClient(program);
|
|
91
|
+
const result = await payInvoice(client, {
|
|
92
|
+
invoice: destination,
|
|
93
|
+
amount_in_sats: options.amount,
|
|
94
|
+
metadata: {},
|
|
95
|
+
});
|
|
96
|
+
output(result);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
case "lightning-address": {
|
|
100
|
+
if (options.amount === undefined) {
|
|
101
|
+
throw new Error("Lightning address payments require --amount <sats>");
|
|
102
|
+
}
|
|
103
|
+
if (!Number.isInteger(options.amount) || options.amount <= 0) {
|
|
104
|
+
throw new Error(`Invalid --amount: must be a positive integer number of sats`);
|
|
105
|
+
}
|
|
106
|
+
const invoice = await requestInvoiceFromLightningAddress({
|
|
107
|
+
lightning_address: destination,
|
|
108
|
+
amount_in_sats: options.amount,
|
|
109
|
+
comment: options.comment,
|
|
110
|
+
});
|
|
111
|
+
const client = await getClient(program);
|
|
112
|
+
// Stash identifier + comment on the payment record so the wallet
|
|
113
|
+
// can show who was paid even when the LNURL server drops them
|
|
114
|
+
// from the invoice memo.
|
|
115
|
+
const metadata = {
|
|
116
|
+
...(options.comment && { comment: options.comment }),
|
|
117
|
+
recipient_data: { identifier: destination },
|
|
118
|
+
};
|
|
119
|
+
const result = await payInvoice(client, {
|
|
120
|
+
invoice: invoice.paymentRequest,
|
|
121
|
+
metadata,
|
|
122
|
+
});
|
|
123
|
+
output(result);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
case "keysend": {
|
|
127
|
+
if (options.amount === undefined) {
|
|
128
|
+
throw new Error("Keysend payments require --amount <sats>");
|
|
129
|
+
}
|
|
130
|
+
if (!Number.isInteger(options.amount) || options.amount <= 0) {
|
|
131
|
+
throw new Error(`Invalid --amount: must be a positive integer number of sats`);
|
|
132
|
+
}
|
|
133
|
+
let tlvRecords;
|
|
134
|
+
if (options.tlvRecords) {
|
|
135
|
+
tlvRecords = JSON.parse(options.tlvRecords);
|
|
136
|
+
}
|
|
137
|
+
const client = await getClient(program);
|
|
138
|
+
const result = await payKeysend(client, {
|
|
139
|
+
pubkey: destination,
|
|
140
|
+
amount_in_sats: options.amount,
|
|
141
|
+
preimage: options.preimage,
|
|
142
|
+
tlv_records: tlvRecords,
|
|
143
|
+
});
|
|
144
|
+
output(result);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
case "crypto": {
|
|
148
|
+
if (options.amount === undefined) {
|
|
149
|
+
throw new Error("Crypto payments require --amount <number>");
|
|
150
|
+
}
|
|
151
|
+
if (!Number.isFinite(options.amount) || options.amount <= 0) {
|
|
152
|
+
throw new Error(`Invalid --amount: ${options.amount}`);
|
|
153
|
+
}
|
|
154
|
+
if (!options.currency) {
|
|
155
|
+
throw new Error("Crypto payments require --currency <name>");
|
|
156
|
+
}
|
|
157
|
+
if (!options.network) {
|
|
158
|
+
throw new Error("Crypto payments require --network <chain-name-or-id>");
|
|
159
|
+
}
|
|
160
|
+
if (!isPlausibleEvmAddress(destination)) {
|
|
161
|
+
throw new Error(`Recipient address does not look valid (expected 0x + 40 hex chars): ${destination}`);
|
|
162
|
+
}
|
|
163
|
+
const pair = await findSupportedPair(options.currency, options.network);
|
|
164
|
+
const nwc = await getClient(program);
|
|
165
|
+
const { swapId } = await payCrypto({
|
|
166
|
+
pair,
|
|
167
|
+
amount: options.amount,
|
|
168
|
+
targetAddress: destination,
|
|
169
|
+
payInvoice: async (bolt11Invoice) => {
|
|
170
|
+
await payInvoice(nwc, { invoice: bolt11Invoice });
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
output({
|
|
174
|
+
swap_id: swapId,
|
|
175
|
+
status: "completed",
|
|
176
|
+
target: {
|
|
177
|
+
address: destination,
|
|
178
|
+
currency: pair.symbol,
|
|
179
|
+
network: pair.network,
|
|
180
|
+
amount: options.amount,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { makeInvoice } from "../tools/nwc/make_invoice.js";
|
|
2
|
+
import { getClient, handleError, output } from "../utils.js";
|
|
3
|
+
export function registerReceiveCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command("receive")
|
|
6
|
+
.description("Get paid — returns either the wallet's lightning address or a BOLT-11 invoice.\n\n" +
|
|
7
|
+
" - receive → returns the wallet's lightning address (if available)\n" +
|
|
8
|
+
" - receive --amount <sats> → returns a BOLT-11 invoice for the given amount")
|
|
9
|
+
.option("-a, --amount <sats>", "Invoice amount in sats", parseInt)
|
|
10
|
+
.option("-d, --description <text>", "Invoice description (requires --amount)")
|
|
11
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
12
|
+
" $ npx @getalby/cli receive\n" +
|
|
13
|
+
' $ npx @getalby/cli receive --amount 2100 --description "coffee"\n')
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
await handleError(async () => {
|
|
16
|
+
if (options.amount === undefined) {
|
|
17
|
+
if (options.description !== undefined) {
|
|
18
|
+
throw new Error("--description requires --amount");
|
|
19
|
+
}
|
|
20
|
+
const client = await getClient(program);
|
|
21
|
+
if (!client.lud16) {
|
|
22
|
+
throw new Error("This wallet does not expose a lightning address. " +
|
|
23
|
+
"Either pass --amount <sats> to generate a BOLT-11 invoice, " +
|
|
24
|
+
"or connect a wallet that has a lightning address.");
|
|
25
|
+
}
|
|
26
|
+
output({ lightning_address: client.lud16 });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!Number.isInteger(options.amount) || options.amount <= 0) {
|
|
30
|
+
throw new Error("Invalid --amount: must be a positive integer number of sats");
|
|
31
|
+
}
|
|
32
|
+
const client = await getClient(program);
|
|
33
|
+
const result = await makeInvoice(client, {
|
|
34
|
+
amount_in_sats: options.amount,
|
|
35
|
+
description: options.description,
|
|
36
|
+
});
|
|
37
|
+
output(result);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
package/build/index.js
CHANGED
|
@@ -8,6 +8,8 @@ import { registerMakeInvoiceCommand } from "./commands/make-invoice.js";
|
|
|
8
8
|
import { registerMakeHoldInvoiceCommand } from "./commands/make-hold-invoice.js";
|
|
9
9
|
import { registerSettleHoldInvoiceCommand } from "./commands/settle-hold-invoice.js";
|
|
10
10
|
import { registerCancelHoldInvoiceCommand } from "./commands/cancel-hold-invoice.js";
|
|
11
|
+
import { registerPayCommand } from "./commands/pay.js";
|
|
12
|
+
import { registerReceiveCommand } from "./commands/receive.js";
|
|
11
13
|
import { registerPayInvoiceCommand } from "./commands/pay-invoice.js";
|
|
12
14
|
import { registerPayKeysendCommand } from "./commands/pay-keysend.js";
|
|
13
15
|
import { registerLookupInvoiceCommand } from "./commands/lookup-invoice.js";
|
|
@@ -20,8 +22,10 @@ import { registerParseInvoiceCommand } from "./commands/parse-invoice.js";
|
|
|
20
22
|
import { registerVerifyPreimageCommand } from "./commands/verify-preimage.js";
|
|
21
23
|
import { registerRequestInvoiceFromLightningAddressCommand } from "./commands/request-invoice-from-lightning-address.js";
|
|
22
24
|
import { registerFetch402Command } from "./commands/fetch.js";
|
|
25
|
+
import { registerPayCryptoCommand } from "./commands/pay-crypto.js";
|
|
23
26
|
import { registerConnectCommand } from "./commands/connect.js";
|
|
24
27
|
import { registerAuthCommand } from "./commands/auth.js";
|
|
28
|
+
import { registerListWalletsCommand } from "./commands/list-wallets.js";
|
|
25
29
|
import { registerDiscoverCommand } from "./commands/discover.js";
|
|
26
30
|
const program = new Command();
|
|
27
31
|
program
|
|
@@ -32,8 +36,11 @@ program
|
|
|
32
36
|
" $ npx @getalby/cli auth https://my.albyhub.com --app-name OpenClaw\n" +
|
|
33
37
|
' $ npx @getalby/cli connect "nostr+walletconnect://..."\n' +
|
|
34
38
|
" $ npx @getalby/cli get-balance\n" +
|
|
35
|
-
" $ npx @getalby/cli pay
|
|
36
|
-
|
|
39
|
+
" $ npx @getalby/cli pay lnbc...\n" +
|
|
40
|
+
" $ npx @getalby/cli pay alice@getalby.com --amount 100\n" +
|
|
41
|
+
' $ npx @getalby/cli receive --amount 2100 --description "Coffee"')
|
|
42
|
+
.version("0.7.0")
|
|
43
|
+
.configureHelp({ showGlobalOptions: true })
|
|
37
44
|
.option("-w, --wallet-name <name>", "Use a named wallet's connection secret (~/.alby-cli/connection-secret-<name>.key)")
|
|
38
45
|
.option("-c, --connection-secret <string>", "NWC connection secret (nostr+walletconnect://...) or path to file containing it (preferred)")
|
|
39
46
|
.option("-v, --verbose", "Print status messages to stderr")
|
|
@@ -52,6 +59,8 @@ Security:
|
|
|
52
59
|
as this can be used to gain access to your wallet or reduce your wallet's privacy.`);
|
|
53
60
|
// Register common wallet commands
|
|
54
61
|
program.commandsGroup("Wallet Commands (requires wallet connection):");
|
|
62
|
+
registerPayCommand(program);
|
|
63
|
+
registerReceiveCommand(program);
|
|
55
64
|
registerGetBalanceCommand(program);
|
|
56
65
|
registerGetBudgetCommand(program);
|
|
57
66
|
registerGetInfoCommand(program);
|
|
@@ -78,6 +87,9 @@ registerRequestInvoiceFromLightningAddressCommand(program);
|
|
|
78
87
|
// Register fetch command for payment-protected resources
|
|
79
88
|
program.commandsGroup("HTTP 402 Payments (requires wallet connection):");
|
|
80
89
|
registerFetch402Command(program);
|
|
90
|
+
// Register cross-currency payments (Lightning → EVM via atomic swap)
|
|
91
|
+
program.commandsGroup("Cross-Currency Payments (requires wallet connection):");
|
|
92
|
+
registerPayCryptoCommand(program);
|
|
81
93
|
// Register service discovery
|
|
82
94
|
program.commandsGroup("Service Discovery:");
|
|
83
95
|
registerDiscoverCommand(program);
|
|
@@ -85,4 +97,5 @@ registerDiscoverCommand(program);
|
|
|
85
97
|
program.commandsGroup("Setup:");
|
|
86
98
|
registerAuthCommand(program);
|
|
87
99
|
registerConnectCommand(program);
|
|
100
|
+
registerListWalletsCommand(program);
|
|
88
101
|
program.parse();
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Asset, Client, InMemorySwapStorage, InMemoryWalletStorage, toChain, toChainName, } from "@lendasat/lendaswap-sdk-pure";
|
|
2
|
+
// Allow tests (or local dev against staging) to override the API endpoint.
|
|
3
|
+
const API_BASE_URL = process.env.LENDASWAP_API_URL || "https://api.satora.io";
|
|
4
|
+
// Terminal statuses where the swap is irrecoverably done. Mirrors the same
|
|
5
|
+
// constants used by the bitcoin-card-topup reference frontend.
|
|
6
|
+
const SUCCESS_STATUSES = ["clientredeemed", "serverredeemed"];
|
|
7
|
+
const FAILURE_STATUSES = [
|
|
8
|
+
"expired",
|
|
9
|
+
"clientrefunded",
|
|
10
|
+
"clientrefundedserverrefunded",
|
|
11
|
+
"clientrefundedserverfunded",
|
|
12
|
+
"clientinvalidfunded",
|
|
13
|
+
"clientfundedtoolate",
|
|
14
|
+
"serverwontfund",
|
|
15
|
+
];
|
|
16
|
+
let clientPromise = null;
|
|
17
|
+
function getClient() {
|
|
18
|
+
if (!clientPromise) {
|
|
19
|
+
// In-memory storage: a single CLI invocation waits synchronously for a
|
|
20
|
+
// terminal swap status, so there's nothing to recover across runs. If the
|
|
21
|
+
// process is killed mid-swap, the HTLC refund timer is the safety net.
|
|
22
|
+
clientPromise = Client.builder()
|
|
23
|
+
.withBaseUrl(API_BASE_URL)
|
|
24
|
+
.withSignerStorage(new InMemoryWalletStorage())
|
|
25
|
+
.withSwapStorage(new InMemorySwapStorage())
|
|
26
|
+
.build();
|
|
27
|
+
}
|
|
28
|
+
return clientPromise;
|
|
29
|
+
}
|
|
30
|
+
let supportedPairsPromise = null;
|
|
31
|
+
/**
|
|
32
|
+
* Fetch all (currency, network) pairs that can be the target of a
|
|
33
|
+
* Lightning → EVM swap. The list comes straight from the Lendaswap API:
|
|
34
|
+
* `getTokens()` for the token universe, intersected with `getSwapPairs()`
|
|
35
|
+
* filtered to source = Lightning.
|
|
36
|
+
*/
|
|
37
|
+
export function getSupportedPairs() {
|
|
38
|
+
if (!supportedPairsPromise) {
|
|
39
|
+
supportedPairsPromise = (async () => {
|
|
40
|
+
const client = await getClient();
|
|
41
|
+
const [tokens, swapPairs] = await Promise.all([
|
|
42
|
+
client.getTokens(),
|
|
43
|
+
client.getSwapPairs(),
|
|
44
|
+
]);
|
|
45
|
+
const lightningTargetChains = new Set(swapPairs.pairs
|
|
46
|
+
.filter((p) => p.source === "Lightning")
|
|
47
|
+
.map((p) => p.target));
|
|
48
|
+
return tokens.evm_tokens
|
|
49
|
+
.filter((t) => lightningTargetChains.has(t.chain))
|
|
50
|
+
.map((t) => ({
|
|
51
|
+
symbol: t.symbol,
|
|
52
|
+
network: toChainName(t.chain),
|
|
53
|
+
decimals: t.decimals,
|
|
54
|
+
chain: t.chain,
|
|
55
|
+
tokenId: t.token_id,
|
|
56
|
+
}));
|
|
57
|
+
})();
|
|
58
|
+
}
|
|
59
|
+
return supportedPairsPromise;
|
|
60
|
+
}
|
|
61
|
+
function formatPairsList(pairs) {
|
|
62
|
+
return pairs.map((p) => ` - ${p.symbol} on ${p.network}`).join("\n");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a (currency, network) pair against the live API list, or throw
|
|
66
|
+
* with a human-readable error listing every supported pair. Network can be
|
|
67
|
+
* a chain name ("arbitrum") or chain id ("42161"); matching is case-insensitive.
|
|
68
|
+
*/
|
|
69
|
+
export async function findSupportedPair(currency, network) {
|
|
70
|
+
const pairs = await getSupportedPairs();
|
|
71
|
+
const symbol = currency.toUpperCase();
|
|
72
|
+
// toChain normalizes "arbitrum"/"42161"/"Arbitrum" to the canonical chain id.
|
|
73
|
+
const chain = toChain(network);
|
|
74
|
+
const pair = pairs.find((p) => p.symbol.toUpperCase() === symbol && p.chain === chain);
|
|
75
|
+
if (!pair) {
|
|
76
|
+
throw new Error(`Unsupported currency/network combination: ${currency} on ${network}.\n` +
|
|
77
|
+
`Supported:\n${formatPairsList(pairs)}`);
|
|
78
|
+
}
|
|
79
|
+
return pair;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* EVM address shape check: every chain reachable from Lightning is EVM, so
|
|
83
|
+
* the universal `0x` + 40-hex format applies. Lendaswap does the
|
|
84
|
+
* authoritative validation when it builds the swap; this is just a sanity
|
|
85
|
+
* pre-check so an obvious typo fails fast before we lock funds.
|
|
86
|
+
*/
|
|
87
|
+
export function isPlausibleEvmAddress(address) {
|
|
88
|
+
return /^0x[0-9a-fA-F]{40}$/.test(address);
|
|
89
|
+
}
|
|
90
|
+
function toSmallestUnit(amount, decimals) {
|
|
91
|
+
return Math.round(amount * 10 ** decimals);
|
|
92
|
+
}
|
|
93
|
+
async function createPaymentSwap(params) {
|
|
94
|
+
const client = await getClient();
|
|
95
|
+
const targetAmount = toSmallestUnit(params.amount, params.pair.decimals);
|
|
96
|
+
const targetAsset = {
|
|
97
|
+
chain: params.pair.chain,
|
|
98
|
+
tokenId: params.pair.tokenId,
|
|
99
|
+
};
|
|
100
|
+
const result = await client.createSwap({
|
|
101
|
+
source: Asset.BTC_LIGHTNING,
|
|
102
|
+
target: targetAsset,
|
|
103
|
+
targetAmount,
|
|
104
|
+
targetAddress: params.targetAddress,
|
|
105
|
+
gasless: true,
|
|
106
|
+
referralCode: "lnds_2c07e38f10a28d47",
|
|
107
|
+
});
|
|
108
|
+
// Source is BTC_LIGHTNING and target is an EVM token, so the SDK routes
|
|
109
|
+
// through its Lightning→EVM path.
|
|
110
|
+
return result.response;
|
|
111
|
+
}
|
|
112
|
+
async function subscribeToSwap(swapId, onUpdate) {
|
|
113
|
+
const client = await getClient();
|
|
114
|
+
return client.subscribeToSwaps([swapId], onUpdate);
|
|
115
|
+
}
|
|
116
|
+
async function claimSwap(swapId) {
|
|
117
|
+
const client = await getClient();
|
|
118
|
+
return client.claim(swapId);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Run a Lightning → on-chain crypto payment swap and block until it reaches a
|
|
122
|
+
* terminal status. Throws on any failure status. All swap-provider specifics
|
|
123
|
+
* (Lendaswap SDK calls, status handling, claim-on-serverfunded) live here so
|
|
124
|
+
* that swapping out the provider is a self-contained change.
|
|
125
|
+
*/
|
|
126
|
+
export async function payCrypto(params) {
|
|
127
|
+
const swap = await createPaymentSwap({
|
|
128
|
+
pair: params.pair,
|
|
129
|
+
amount: params.amount,
|
|
130
|
+
targetAddress: params.targetAddress,
|
|
131
|
+
});
|
|
132
|
+
// Subscribe BEFORE paying so we don't miss the `serverfunded` event
|
|
133
|
+
// that triggers our claim. Unsubscribe in `finally` no matter what.
|
|
134
|
+
let unsubscribe;
|
|
135
|
+
try {
|
|
136
|
+
await new Promise((resolve, reject) => {
|
|
137
|
+
let settled = false;
|
|
138
|
+
const settle = (fn) => {
|
|
139
|
+
if (settled)
|
|
140
|
+
return;
|
|
141
|
+
settled = true;
|
|
142
|
+
fn();
|
|
143
|
+
};
|
|
144
|
+
let claimStarted = false;
|
|
145
|
+
subscribeToSwap(swap.id, (_id, status) => {
|
|
146
|
+
if (status === "serverfunded" && !claimStarted) {
|
|
147
|
+
claimStarted = true;
|
|
148
|
+
claimSwap(swap.id).catch((err) => settle(() => reject(err instanceof Error ? err : new Error(String(err)))));
|
|
149
|
+
}
|
|
150
|
+
if (SUCCESS_STATUSES.includes(status)) {
|
|
151
|
+
settle(resolve);
|
|
152
|
+
}
|
|
153
|
+
else if (FAILURE_STATUSES.includes(status)) {
|
|
154
|
+
settle(() => reject(new Error(`Swap ${status}`)));
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
.then((unsub) => {
|
|
158
|
+
unsubscribe = unsub;
|
|
159
|
+
// If the swap already terminated before subscribe resolved,
|
|
160
|
+
// tear down immediately.
|
|
161
|
+
if (settled)
|
|
162
|
+
unsub();
|
|
163
|
+
})
|
|
164
|
+
.catch((err) => settle(() => reject(err instanceof Error ? err : new Error(String(err)))));
|
|
165
|
+
// Pay the Lightning invoice. Failure here propagates as the
|
|
166
|
+
// overall swap failure; success doesn't resolve us — only a
|
|
167
|
+
// terminal swap status does.
|
|
168
|
+
params
|
|
169
|
+
.payInvoice(swap.bolt11_invoice)
|
|
170
|
+
.catch((err) => settle(() => reject(err instanceof Error ? err : new Error(String(err)))));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
unsubscribe?.();
|
|
175
|
+
}
|
|
176
|
+
return { swapId: swap.id };
|
|
177
|
+
}
|
|
@@ -50,12 +50,12 @@ describe("Connection Secret Handling", () => {
|
|
|
50
50
|
NWC_URL: "",
|
|
51
51
|
});
|
|
52
52
|
expect(result.success).toBe(false);
|
|
53
|
-
expect(result.output.error).toContain("No connection
|
|
53
|
+
expect(result.output.error).toContain("No wallet connection found");
|
|
54
54
|
});
|
|
55
55
|
test("errors when connection string is malformed", () => {
|
|
56
56
|
const result = runCli(`-c "nostr+walletconnect://asdf" get-balance`);
|
|
57
57
|
expect(result.success).toBe(false);
|
|
58
|
-
expect(result.output.error).toContain("Invalid
|
|
58
|
+
expect(result.output.error).toContain("Invalid NWC URL");
|
|
59
59
|
});
|
|
60
60
|
test("reads connection secret from NWC_URL environment variable", () => {
|
|
61
61
|
const result = runCli("get-balance", { NWC_URL: wallet.nwcUrl });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { runCli } from "./helpers.js";
|
|
3
3
|
const exampleInvoice = "lnbc1u1p5hlrr8dqqnp4qwmtpr4p72ms7gnq3pkfk2876y2msvl33s3840dlp6xsv2w59dpscpp55utq6s8u5407namwt4jvhgsaf9fyszppjfwyxp7qsw6cyc8vxukqsp583usez9yhmkcavvvjz8cq56v3nglh2q37xkf4ufrgwxfrfjkm54s9qyysgqcqzp2xqyz5vqgtyysw64zt9sj6kfpqnekzwc37y2uyg0xdapgxqqth4uahff0x89sjfsvukjlllasg5dn05u2uha6qcvxz2y3ye5k7958qtes4pv4ggqtnjyky";
|
|
4
|
-
const exampleLightningAddress = "
|
|
4
|
+
const exampleLightningAddress = "nwc1779952113427@getalby.com";
|
|
5
5
|
describe("Lightning Tools (no wallet required)", () => {
|
|
6
6
|
test("fiat-to-sats converts USD to sats", () => {
|
|
7
7
|
const result = runCli("fiat-to-sats -a 1 --currency USD");
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { runCli } from "./helpers.js";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
describe("list-wallets command", () => {
|
|
7
|
+
let testHome;
|
|
8
|
+
let albyDir;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
testHome = mkdtempSync(join(tmpdir(), "alby-cli-test-"));
|
|
11
|
+
albyDir = join(testHome, ".alby-cli");
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
rmSync(testHome, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
function writeWallet(filename) {
|
|
17
|
+
mkdirSync(albyDir, { recursive: true });
|
|
18
|
+
writeFileSync(join(albyDir, filename), "nostr+walletconnect://test");
|
|
19
|
+
}
|
|
20
|
+
test("returns empty list when no .alby-cli directory exists", () => {
|
|
21
|
+
const result = runCli("list-wallets", { HOME: testHome });
|
|
22
|
+
expect(result.success).toBe(true);
|
|
23
|
+
expect(result.output.wallets).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
test("lists the default (unnamed) wallet", () => {
|
|
26
|
+
writeWallet("connection-secret.key");
|
|
27
|
+
const result = runCli("list-wallets", { HOME: testHome });
|
|
28
|
+
expect(result.success).toBe(true);
|
|
29
|
+
expect(result.output.wallets).toEqual([
|
|
30
|
+
{ name: null, isDefault: true, status: "connected" },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
test("lists named wallets sorted with default first", () => {
|
|
34
|
+
writeWallet("connection-secret.key");
|
|
35
|
+
writeWallet("connection-secret-work.key");
|
|
36
|
+
writeWallet("connection-secret-personal.key");
|
|
37
|
+
const result = runCli("list-wallets", { HOME: testHome });
|
|
38
|
+
expect(result.success).toBe(true);
|
|
39
|
+
expect(result.output.wallets).toEqual([
|
|
40
|
+
{ name: null, isDefault: true, status: "connected" },
|
|
41
|
+
{ name: "personal", isDefault: false, status: "connected" },
|
|
42
|
+
{ name: "work", isDefault: false, status: "connected" },
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
test("reports pending connections", () => {
|
|
46
|
+
writeWallet("pending-connection-secret-test.key");
|
|
47
|
+
const result = runCli("list-wallets", { HOME: testHome });
|
|
48
|
+
expect(result.success).toBe(true);
|
|
49
|
+
expect(result.output.wallets).toEqual([
|
|
50
|
+
{ name: "test", isDefault: false, status: "pending" },
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
test("connected status takes precedence over pending for the same wallet", () => {
|
|
54
|
+
writeWallet("connection-secret-dual.key");
|
|
55
|
+
writeWallet("pending-connection-secret-dual.key");
|
|
56
|
+
const result = runCli("list-wallets", { HOME: testHome });
|
|
57
|
+
expect(result.success).toBe(true);
|
|
58
|
+
expect(result.output.wallets).toEqual([
|
|
59
|
+
{ name: "dual", isDefault: false, status: "connected" },
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
test("does not reveal secret contents", () => {
|
|
63
|
+
writeWallet("connection-secret.key");
|
|
64
|
+
const result = runCli("list-wallets", { HOME: testHome });
|
|
65
|
+
expect(JSON.stringify(result.output)).not.toContain("nostr+walletconnect://");
|
|
66
|
+
});
|
|
67
|
+
test("ignores unrelated files", () => {
|
|
68
|
+
mkdirSync(albyDir, { recursive: true });
|
|
69
|
+
writeFileSync(join(albyDir, "pending-connection-relay-test.txt"), "wss://relay");
|
|
70
|
+
writeFileSync(join(albyDir, "notes.txt"), "hello");
|
|
71
|
+
const result = runCli("list-wallets", { HOME: testHome });
|
|
72
|
+
expect(result.success).toBe(true);
|
|
73
|
+
expect(result.output.wallets).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -40,7 +40,6 @@ describe("NWC HOLD Invoice Commands", () => {
|
|
|
40
40
|
"-c",
|
|
41
41
|
sender.nwcUrl,
|
|
42
42
|
"pay-invoice",
|
|
43
|
-
"-i",
|
|
44
43
|
holdResult.output.invoice,
|
|
45
44
|
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
46
45
|
// Wait for the hold invoice to be accepted
|
|
@@ -64,7 +63,6 @@ describe("NWC HOLD Invoice Commands", () => {
|
|
|
64
63
|
"-c",
|
|
65
64
|
sender.nwcUrl,
|
|
66
65
|
"pay-invoice",
|
|
67
|
-
"-i",
|
|
68
66
|
holdResult.output.invoice,
|
|
69
67
|
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
70
68
|
// Wait for the hold invoice to be in held state
|
|
@@ -14,7 +14,7 @@ describe("NWC Payment Commands", () => {
|
|
|
14
14
|
expect(invoiceResult.success).toBe(true);
|
|
15
15
|
expect(invoiceResult.output.invoice).toBeDefined();
|
|
16
16
|
// Pay with sender wallet
|
|
17
|
-
const paymentResult = runCli(`-c "${sender.nwcUrl}" pay-invoice
|
|
17
|
+
const paymentResult = runCli(`-c "${sender.nwcUrl}" pay-invoice "${invoiceResult.output.invoice}"`);
|
|
18
18
|
expect(paymentResult.success).toBe(true);
|
|
19
19
|
expect(paymentResult.output.preimage).toBeDefined();
|
|
20
20
|
});
|
|
@@ -23,7 +23,7 @@ describe("NWC Payment Commands", () => {
|
|
|
23
23
|
const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice -a 50`);
|
|
24
24
|
expect(invoiceResult.success).toBe(true);
|
|
25
25
|
// Pay the invoice first (unpaid invoices may not be found)
|
|
26
|
-
const payResult = runCli(`-c "${sender.nwcUrl}" pay-invoice
|
|
26
|
+
const payResult = runCli(`-c "${sender.nwcUrl}" pay-invoice "${invoiceResult.output.invoice}"`);
|
|
27
27
|
expect(payResult.success).toBe(true);
|
|
28
28
|
// Lookup the paid invoice using the invoice string
|
|
29
29
|
const lookupResult = runCli(`-c "${receiver.nwcUrl}" lookup-invoice -i "${invoiceResult.output.invoice}"`);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "vitest";
|
|
2
|
+
import { createTestWallet, runCli } from "./helpers.js";
|
|
3
|
+
describe("pay command — destination detection", () => {
|
|
4
|
+
test("unknown destination format lists all 4 accepted shapes", () => {
|
|
5
|
+
const result = runCli(`pay notavaliddestination`);
|
|
6
|
+
expect(result.success).toBe(false);
|
|
7
|
+
expect(result.output.error).toContain("Could not detect destination type");
|
|
8
|
+
expect(result.output.error).toContain("BOLT-11 invoice");
|
|
9
|
+
expect(result.output.error).toContain("Lightning address");
|
|
10
|
+
expect(result.output.error).toContain("Node pubkey");
|
|
11
|
+
expect(result.output.error).toContain("EVM address");
|
|
12
|
+
});
|
|
13
|
+
test("lightning address without --amount is rejected before wallet load", () => {
|
|
14
|
+
const result = runCli(`pay alice@getalby.com`);
|
|
15
|
+
expect(result.success).toBe(false);
|
|
16
|
+
expect(result.output.error).toContain("--amount");
|
|
17
|
+
});
|
|
18
|
+
test("keysend pubkey without --amount is rejected before wallet load", () => {
|
|
19
|
+
const pubkey = "02" + "a".repeat(64);
|
|
20
|
+
const result = runCli(`pay ${pubkey}`);
|
|
21
|
+
expect(result.success).toBe(false);
|
|
22
|
+
expect(result.output.error).toContain("--amount");
|
|
23
|
+
});
|
|
24
|
+
test("EVM address without --amount is rejected before wallet load", () => {
|
|
25
|
+
const result = runCli(`pay 0x000000000000000000000000000000000000dead`);
|
|
26
|
+
expect(result.success).toBe(false);
|
|
27
|
+
expect(result.output.error).toContain("--amount");
|
|
28
|
+
});
|
|
29
|
+
test("EVM address without --currency is rejected", () => {
|
|
30
|
+
const result = runCli(`pay 0x000000000000000000000000000000000000dead --amount 10 --network arbitrum`);
|
|
31
|
+
expect(result.success).toBe(false);
|
|
32
|
+
expect(result.output.error).toContain("--currency");
|
|
33
|
+
});
|
|
34
|
+
test("EVM address without --network is rejected", () => {
|
|
35
|
+
const result = runCli(`pay 0x000000000000000000000000000000000000dead --amount 10 --currency USDC`);
|
|
36
|
+
expect(result.success).toBe(false);
|
|
37
|
+
expect(result.output.error).toContain("--network");
|
|
38
|
+
});
|
|
39
|
+
test("--currency on a BOLT-11 invoice is rejected as not applicable", () => {
|
|
40
|
+
const result = runCli(`pay lnbc1junk --currency USDT`);
|
|
41
|
+
expect(result.success).toBe(false);
|
|
42
|
+
expect(result.output.error).toContain("not applicable to invoice payment");
|
|
43
|
+
});
|
|
44
|
+
test("testnet/signet invoice prefixes (lntb...) are recognized as invoices", () => {
|
|
45
|
+
// Same path as the lnbc test above — exercises that lntb is treated as
|
|
46
|
+
// an invoice (not falling through to the unknown-destination error).
|
|
47
|
+
const result = runCli(`pay lntb1junk --currency USDT`);
|
|
48
|
+
expect(result.success).toBe(false);
|
|
49
|
+
expect(result.output.error).toContain("not applicable to invoice payment");
|
|
50
|
+
});
|
|
51
|
+
test("--comment on a keysend pubkey is rejected as not applicable", () => {
|
|
52
|
+
const pubkey = "02" + "a".repeat(64);
|
|
53
|
+
const result = runCli(`pay ${pubkey} --amount 100 --comment hi`);
|
|
54
|
+
expect(result.success).toBe(false);
|
|
55
|
+
expect(result.output.error).toContain("not applicable to keysend payment");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("pay command — live integration", () => {
|
|
59
|
+
let sender;
|
|
60
|
+
let receiver;
|
|
61
|
+
beforeAll(async () => {
|
|
62
|
+
sender = await createTestWallet();
|
|
63
|
+
receiver = await createTestWallet();
|
|
64
|
+
}, 60000);
|
|
65
|
+
test("pay <bolt11> pays an invoice end-to-end", () => {
|
|
66
|
+
const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice -a 100`);
|
|
67
|
+
expect(invoiceResult.success).toBe(true);
|
|
68
|
+
const paymentResult = runCli(`-c "${sender.nwcUrl}" pay "${invoiceResult.output.invoice}"`);
|
|
69
|
+
expect(paymentResult.success).toBe(true);
|
|
70
|
+
expect(paymentResult.output.preimage).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
test("pay <lightning-address> --amount fetches an invoice and pays it", () => {
|
|
73
|
+
const paymentResult = runCli(`-c "${sender.nwcUrl}" pay ${receiver.lightningAddress} --amount 100`);
|
|
74
|
+
expect(paymentResult.success).toBe(true);
|
|
75
|
+
expect(paymentResult.output.preimage).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
test("pay <pubkey> --amount sends a keysend payment", () => {
|
|
78
|
+
const infoResult = runCli(`-c "${receiver.nwcUrl}" get-info`);
|
|
79
|
+
expect(infoResult.success).toBe(true);
|
|
80
|
+
const paymentResult = runCli(`-c "${sender.nwcUrl}" pay ${infoResult.output.pubkey} --amount 100`);
|
|
81
|
+
expect(paymentResult.success).toBe(true);
|
|
82
|
+
expect(paymentResult.output.preimage).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { createServer } from "http";
|
|
4
|
+
// Stand up a local HTTP mock for the Lendaswap API endpoints the CLI hits
|
|
5
|
+
// during validation. The CLI spawns a fresh subprocess per test, so the
|
|
6
|
+
// `LENDASWAP_API_URL` env var (consumed in src/lendaswap/swap.ts) points it
|
|
7
|
+
// at this mock instead of api.satora.io. We only need `/tokens` and
|
|
8
|
+
// `/swap-pairs` — pair validation runs before wallet load, so tests never
|
|
9
|
+
// reach the swap-creation endpoints.
|
|
10
|
+
//
|
|
11
|
+
// IMPORTANT: we use async `spawn`, not the shared `execSync`-based runCli
|
|
12
|
+
// helper. `execSync` blocks the event loop, which would prevent this
|
|
13
|
+
// in-process mock from accepting the subprocess's TCP connection — fetch
|
|
14
|
+
// would just time out.
|
|
15
|
+
let server;
|
|
16
|
+
let mockUrl = "";
|
|
17
|
+
const MOCK_TOKENS = {
|
|
18
|
+
btc_tokens: [],
|
|
19
|
+
evm_tokens: [
|
|
20
|
+
{
|
|
21
|
+
chain: "42161",
|
|
22
|
+
decimals: 6,
|
|
23
|
+
name: "USD Coin",
|
|
24
|
+
symbol: "USDC",
|
|
25
|
+
token_id: "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
chain: "42161",
|
|
29
|
+
decimals: 6,
|
|
30
|
+
name: "Tether USD",
|
|
31
|
+
symbol: "USDT",
|
|
32
|
+
token_id: "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
// Only the Lightning → 42161 (Arbitrum) pair is enabled in the mock. So
|
|
37
|
+
// USDC/USDT on Arbitrum resolve; the same symbols on any other chain are
|
|
38
|
+
// rejected as unsupported.
|
|
39
|
+
const MOCK_SWAP_PAIRS = {
|
|
40
|
+
pairs: [
|
|
41
|
+
{
|
|
42
|
+
fee_percentage: 0.0025,
|
|
43
|
+
max_sats: 100_000_000,
|
|
44
|
+
min_sats: 1000,
|
|
45
|
+
source: "Lightning",
|
|
46
|
+
target: "42161",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
server = createServer((req, res) => {
|
|
52
|
+
res.setHeader("Content-Type", "application/json");
|
|
53
|
+
if (req.url === "/tokens") {
|
|
54
|
+
res.end(JSON.stringify(MOCK_TOKENS));
|
|
55
|
+
}
|
|
56
|
+
else if (req.url === "/swap-pairs") {
|
|
57
|
+
res.end(JSON.stringify(MOCK_SWAP_PAIRS));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
res.statusCode = 404;
|
|
61
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
|
|
65
|
+
const addr = server.address();
|
|
66
|
+
mockUrl = `http://127.0.0.1:${addr.port}`;
|
|
67
|
+
});
|
|
68
|
+
afterAll(() => new Promise((resolve) => server.close(() => resolve())));
|
|
69
|
+
function runCliAsync(args) {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const child = spawn("node", ["build/index.js", ...args.split(" ")], {
|
|
72
|
+
env: {
|
|
73
|
+
...process.env,
|
|
74
|
+
// Wallet load always fails — exposes the validation gates as the
|
|
75
|
+
// only thing that can succeed or fail before that point.
|
|
76
|
+
HOME: "/tmp/nonexistent-alby-cli-test-home",
|
|
77
|
+
NWC_URL: "",
|
|
78
|
+
LENDASWAP_API_URL: mockUrl,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
let stdout = "";
|
|
82
|
+
let stderr = "";
|
|
83
|
+
child.stdout.on("data", (d) => (stdout += d.toString()));
|
|
84
|
+
child.stderr.on("data", (d) => (stderr += d.toString()));
|
|
85
|
+
child.on("close", (code) => {
|
|
86
|
+
const raw = code === 0 ? stdout : stderr || stdout || "{}";
|
|
87
|
+
try {
|
|
88
|
+
resolve({ success: code === 0, output: JSON.parse(raw) });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
resolve({ success: code === 0, output: { error: raw } });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
describe("pay-crypto validation", () => {
|
|
97
|
+
describe("unsupported currency/network combination", () => {
|
|
98
|
+
test("unknown currency is rejected and lists supported pairs", async () => {
|
|
99
|
+
const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount 10 --currency XYZ --network arbitrum");
|
|
100
|
+
expect(result.success).toBe(false);
|
|
101
|
+
expect(result.output.error).toContain("Unsupported currency/network combination");
|
|
102
|
+
expect(result.output.error).toContain("USDC on Arbitrum");
|
|
103
|
+
});
|
|
104
|
+
test("USDC on ethereum is rejected (chain not in swap-pairs)", async () => {
|
|
105
|
+
const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount 10 --currency USDC --network ethereum");
|
|
106
|
+
expect(result.success).toBe(false);
|
|
107
|
+
expect(result.output.error).toContain("Unsupported currency/network combination");
|
|
108
|
+
expect(result.output.error).toContain("USDC on Arbitrum");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe("malformed EVM address", () => {
|
|
112
|
+
test("completely non-hex string is rejected", async () => {
|
|
113
|
+
const result = await runCliAsync("pay-crypto notanaddress --amount 10 --currency USDC --network arbitrum");
|
|
114
|
+
expect(result.success).toBe(false);
|
|
115
|
+
expect(result.output.error).toContain("address does not look valid");
|
|
116
|
+
});
|
|
117
|
+
test("too-short hex with 0x prefix is rejected", async () => {
|
|
118
|
+
const result = await runCliAsync("pay-crypto 0xabc --amount 10 --currency USDC --network arbitrum");
|
|
119
|
+
expect(result.success).toBe(false);
|
|
120
|
+
expect(result.output.error).toContain("address does not look valid");
|
|
121
|
+
});
|
|
122
|
+
test("40-char hex without 0x prefix is rejected", async () => {
|
|
123
|
+
const result = await runCliAsync("pay-crypto 000000000000000000000000000000000000dead --amount 10 --currency USDC --network arbitrum");
|
|
124
|
+
expect(result.success).toBe(false);
|
|
125
|
+
expect(result.output.error).toContain("address does not look valid");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe("invalid amount", () => {
|
|
129
|
+
test("--amount 0 is rejected", async () => {
|
|
130
|
+
const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount 0 --currency USDC --network arbitrum");
|
|
131
|
+
expect(result.success).toBe(false);
|
|
132
|
+
expect(result.output.error).toContain("Invalid --amount");
|
|
133
|
+
});
|
|
134
|
+
test("--amount -1 is rejected", async () => {
|
|
135
|
+
const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount -1 --currency USDC --network arbitrum");
|
|
136
|
+
expect(result.success).toBe(false);
|
|
137
|
+
expect(result.output.error).toContain("Invalid --amount");
|
|
138
|
+
});
|
|
139
|
+
test("--amount abc (NaN) is rejected", async () => {
|
|
140
|
+
const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount abc --currency USDC --network arbitrum");
|
|
141
|
+
expect(result.success).toBe(false);
|
|
142
|
+
expect(result.output.error).toContain("Invalid --amount");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe("missing required options", () => {
|
|
146
|
+
test("missing --currency is rejected", async () => {
|
|
147
|
+
const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount 10 --network arbitrum");
|
|
148
|
+
expect(result.success).toBe(false);
|
|
149
|
+
expect(result.output.error).toContain("--currency");
|
|
150
|
+
});
|
|
151
|
+
test("missing --network is rejected", async () => {
|
|
152
|
+
const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount 10 --currency USDC");
|
|
153
|
+
expect(result.success).toBe(false);
|
|
154
|
+
expect(result.output.error).toContain("--network");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe("happy-path validation", () => {
|
|
158
|
+
test("valid USDC/arbitrum inputs get past validation and fail only at wallet load", async () => {
|
|
159
|
+
// The mocked supported list includes USDC on 42161 (Arbitrum), so
|
|
160
|
+
// findSupportedPair succeeds. With the wallet env disabled, the only
|
|
161
|
+
// error left is "No wallet connection found" from getClient() —
|
|
162
|
+
// proof that amount, address, and pair gates all accepted the input.
|
|
163
|
+
const result = await runCliAsync("pay-crypto 0x000000000000000000000000000000000000dead --amount 10 --currency USDC --network arbitrum");
|
|
164
|
+
expect(result.success).toBe(false);
|
|
165
|
+
expect(result.output.error).toContain("No wallet connection found");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "vitest";
|
|
2
|
+
import { createTestWallet, runCli } from "./helpers.js";
|
|
3
|
+
describe("receive command — validation", () => {
|
|
4
|
+
test("--description without --amount is rejected", () => {
|
|
5
|
+
const result = runCli(`receive --description "hi"`);
|
|
6
|
+
expect(result.success).toBe(false);
|
|
7
|
+
expect(result.output.error).toContain("--description requires --amount");
|
|
8
|
+
});
|
|
9
|
+
test("--amount 0 is rejected", () => {
|
|
10
|
+
const result = runCli(`receive --amount 0`);
|
|
11
|
+
expect(result.success).toBe(false);
|
|
12
|
+
expect(result.output.error).toContain("Invalid --amount");
|
|
13
|
+
});
|
|
14
|
+
test("--amount abc (NaN) is rejected", () => {
|
|
15
|
+
const result = runCli(`receive --amount abc`);
|
|
16
|
+
expect(result.success).toBe(false);
|
|
17
|
+
expect(result.output.error).toContain("Invalid --amount");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe("receive command — live integration", () => {
|
|
21
|
+
let wallet;
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
wallet = await createTestWallet();
|
|
24
|
+
}, 60000);
|
|
25
|
+
test("receive (no amount) returns the wallet's lightning address", () => {
|
|
26
|
+
const result = runCli(`-c "${wallet.nwcUrl}" receive`);
|
|
27
|
+
expect(result.success).toBe(true);
|
|
28
|
+
expect(result.output.lightning_address).toBe(wallet.lightningAddress);
|
|
29
|
+
});
|
|
30
|
+
test("receive --amount returns a BOLT-11 invoice", () => {
|
|
31
|
+
const result = runCli(`-c "${wallet.nwcUrl}" receive --amount 100`);
|
|
32
|
+
expect(result.success).toBe(true);
|
|
33
|
+
expect(result.output.invoice).toMatch(/^lnbc/i);
|
|
34
|
+
expect(result.output.amount_in_sats).toBe(100);
|
|
35
|
+
});
|
|
36
|
+
test("receive --amount --description produces an invoice", () => {
|
|
37
|
+
const result = runCli(`-c "${wallet.nwcUrl}" receive --amount 100 --description "test"`);
|
|
38
|
+
expect(result.success).toBe(true);
|
|
39
|
+
expect(result.output.invoice).toMatch(/^lnbc/i);
|
|
40
|
+
expect(result.output.amount_in_sats).toBe(100);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/build/utils.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { NWAClient, NWCClient } from "@getalby/sdk";
|
|
2
2
|
import { getInfo } from "./tools/nwc/get_info.js";
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
export const DEFAULT_RELAY_URLS = [
|
|
7
|
+
"wss://relay.getalby.com",
|
|
8
|
+
"wss://relay2.getalby.com",
|
|
9
|
+
];
|
|
10
|
+
export function getAlbyCliDir() {
|
|
11
|
+
return join(homedir(), ".alby-cli");
|
|
12
|
+
}
|
|
6
13
|
function sanitizeWalletName(name) {
|
|
7
14
|
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
8
15
|
}
|
|
@@ -10,23 +17,70 @@ export function getConnectionSecretPath(name) {
|
|
|
10
17
|
const filename = name
|
|
11
18
|
? `connection-secret-${sanitizeWalletName(name)}.key`
|
|
12
19
|
: "connection-secret.key";
|
|
13
|
-
return join(
|
|
20
|
+
return join(getAlbyCliDir(), filename);
|
|
14
21
|
}
|
|
15
22
|
export function getPendingConnectionSecretPath(name) {
|
|
16
23
|
const filename = name
|
|
17
24
|
? `pending-connection-secret-${sanitizeWalletName(name)}.key`
|
|
18
25
|
: "pending-connection-secret.key";
|
|
19
|
-
return join(
|
|
26
|
+
return join(getAlbyCliDir(), filename);
|
|
20
27
|
}
|
|
21
28
|
export function getPendingConnectionRelayPath(name) {
|
|
22
29
|
const filename = name
|
|
23
30
|
? `pending-connection-relay-${sanitizeWalletName(name)}.txt`
|
|
24
31
|
: "pending-connection-relay.txt";
|
|
25
|
-
return join(
|
|
32
|
+
return join(getAlbyCliDir(), filename);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* List configured wallets by scanning ~/.alby-cli for connection secret files.
|
|
36
|
+
* Never reads or returns secret contents - only wallet names and status.
|
|
37
|
+
*/
|
|
38
|
+
export function listWallets() {
|
|
39
|
+
const dir = getAlbyCliDir();
|
|
40
|
+
let files;
|
|
41
|
+
try {
|
|
42
|
+
files = readdirSync(dir);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const err = error;
|
|
46
|
+
if (err.code === "ENOENT")
|
|
47
|
+
return [];
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
// Map of wallet name (null for default) -> status. Connected takes precedence
|
|
51
|
+
// over pending so a re-authed wallet still shows as usable.
|
|
52
|
+
const wallets = new Map();
|
|
53
|
+
const patterns = [
|
|
54
|
+
{ regex: /^connection-secret(?:-(.+))?\.key$/, status: "connected" },
|
|
55
|
+
{ regex: /^pending-connection-secret(?:-(.+))?\.key$/, status: "pending" },
|
|
56
|
+
];
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
for (const { regex, status } of patterns) {
|
|
59
|
+
const match = file.match(regex);
|
|
60
|
+
if (!match)
|
|
61
|
+
continue;
|
|
62
|
+
const name = match[1] ?? null;
|
|
63
|
+
if (status === "connected" || !wallets.has(name)) {
|
|
64
|
+
wallets.set(name, wallets.get(name) === "connected" ? "connected" : status);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...wallets.entries()]
|
|
70
|
+
.map(([name, status]) => ({
|
|
71
|
+
name,
|
|
72
|
+
isDefault: name === null,
|
|
73
|
+
status,
|
|
74
|
+
}))
|
|
75
|
+
.sort((a, b) => {
|
|
76
|
+
if (a.isDefault !== b.isDefault)
|
|
77
|
+
return a.isDefault ? -1 : 1;
|
|
78
|
+
return (a.name ?? "").localeCompare(b.name ?? "");
|
|
79
|
+
});
|
|
26
80
|
}
|
|
27
81
|
export function saveConnectionSecret(path, secret, verbose) {
|
|
28
82
|
const alreadyExists = existsSync(path);
|
|
29
|
-
const dir =
|
|
83
|
+
const dir = getAlbyCliDir();
|
|
30
84
|
if (!existsSync(dir)) {
|
|
31
85
|
mkdirSync(dir, { recursive: true });
|
|
32
86
|
}
|
|
@@ -43,16 +97,20 @@ export async function testAndLogConnection(client) {
|
|
|
43
97
|
const info = await getInfo(client);
|
|
44
98
|
console.log(`Connected to ${info.alias || "wallet"} (${info.network || "unknown network"})`);
|
|
45
99
|
}
|
|
46
|
-
export async function completePendingConnection(pendingSecretPath, connectionSecretPath,
|
|
100
|
+
export async function completePendingConnection(pendingSecretPath, connectionSecretPath, relayUrls, verbose, pendingRelayPath) {
|
|
47
101
|
const secret = readFileSync(pendingSecretPath, "utf-8").trim();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
102
|
+
if ((!relayUrls || relayUrls.length === 0) &&
|
|
103
|
+
pendingRelayPath &&
|
|
104
|
+
existsSync(pendingRelayPath)) {
|
|
105
|
+
relayUrls = readFileSync(pendingRelayPath, "utf-8")
|
|
106
|
+
.split("\n")
|
|
107
|
+
.map((line) => line.trim())
|
|
108
|
+
.filter((line) => line.length > 0);
|
|
51
109
|
}
|
|
52
|
-
const
|
|
110
|
+
const resolvedRelays = relayUrls && relayUrls.length > 0 ? relayUrls : DEFAULT_RELAY_URLS;
|
|
53
111
|
const nwaClient = new NWAClient({
|
|
54
112
|
appSecretKey: secret,
|
|
55
|
-
relayUrls:
|
|
113
|
+
relayUrls: resolvedRelays,
|
|
56
114
|
requestMethods: [],
|
|
57
115
|
});
|
|
58
116
|
return new Promise((resolve, reject) => {
|
|
@@ -117,7 +175,11 @@ export async function getClient(program) {
|
|
|
117
175
|
}
|
|
118
176
|
}
|
|
119
177
|
if (!connectionSecret) {
|
|
120
|
-
throw new Error("No connection
|
|
178
|
+
throw new Error("No wallet connection found. Run 'auth' or 'connect' first to set up a wallet:\n" +
|
|
179
|
+
" npx @getalby/cli auth <wallet-url> # e.g. https://my.albyhub.com\n" +
|
|
180
|
+
' npx @getalby/cli connect "nostr+walletconnect://..."\n' +
|
|
181
|
+
"\n" +
|
|
182
|
+
"Already have a connection secret? Pass -c <secret or file path>, set NWC_URL, use --wallet-name <name>, or create ~/.alby-cli/connection-secret.key");
|
|
121
183
|
}
|
|
122
184
|
// Auto-detect: if it doesn't start with the protocol, treat as file path
|
|
123
185
|
if (!connectionSecret.startsWith("nostr+walletconnect://")) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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.7.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "build/index.js",
|
|
8
8
|
"bin": {
|
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
"node": ">=20"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@getalby/lightning-tools": "^8.
|
|
40
|
-
"@getalby/sdk": "^
|
|
39
|
+
"@getalby/lightning-tools": "^8.1.1",
|
|
40
|
+
"@getalby/sdk": "^8.0.1",
|
|
41
|
+
"@lendasat/lendaswap-sdk-pure": "^0.2.36",
|
|
41
42
|
"@noble/hashes": "^2.0.1",
|
|
42
43
|
"commander": "^14.0.3",
|
|
43
44
|
"nostr-tools": "^2.23.3"
|