Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,22 +108,22 @@ npx @getalby/cli get-info
npx @getalby/cli get-wallet-service-info

# Create an invoice
npx @getalby/cli make-invoice --amount 1000 --description "Payment"
npx @getalby/cli make-invoice --amount-sats 1000 --description "Payment"

# Get paid — returns the wallet's lightning address, or a BOLT-11 invoice if --amount is given.
# Get paid — returns the wallet's lightning address, or a BOLT-11 invoice if --amount-sats is given.
# - With no args: returns the wallet's lightning address (errors if the wallet has none)
npx @getalby/cli receive
# - With --amount: returns a BOLT-11 invoice for that amount; --description is optional
npx @getalby/cli receive --amount 100 --description "coffee"
# - With --amount-sats: returns a BOLT-11 invoice for that amount; --description is optional
npx @getalby/cli receive --amount-sats 100 --description "coffee"

# Pay any supported destination — auto-detects type from the destination string.
# Required args depend on the destination type:
# - BOLT-11 invoice (lnbc...): no extra args (use --amount only for zero-amount invoices)
# - BOLT-11 invoice (lnbc...): no extra args (use --amount-sats only for zero-amount invoices)
npx @getalby/cli pay "lnbc..."
# - Lightning address (user@domain): requires --amount (sats); optional --comment
npx @getalby/cli pay alice@getalby.com --amount 100 --comment "hi"
# - Node pubkey (66-char hex, compressed secp256k1): keysend, requires --amount (sats)
npx @getalby/cli pay 02abc... --amount 100
# - Lightning address (user@domain): requires --amount-sats; optional --comment
npx @getalby/cli pay alice@getalby.com --amount-sats 100 --comment "hi"
# - Node pubkey (66-char hex, compressed secp256k1): keysend, requires --amount-sats
npx @getalby/cli pay 02abc... --amount-sats 100
# - EVM address (0x...): pay crypto/stablecoin, requires --amount, --currency, and --network
npx @getalby/cli pay 0xabc... --amount 10 --currency USDC --network arbitrum

Expand All @@ -149,7 +149,7 @@ npx @getalby/cli fetch "https://example.com/api"
npx @getalby/cli fetch "https://example.com/api" --method POST --body '{"query":"hello"}' --headers '{"Accept":"application/json"}'

# Fetch with a custom max amount (default: 5000 sats, 0 = no limit)
npx @getalby/cli fetch "https://example.com/api" --max-amount 1000
npx @getalby/cli fetch "https://example.com/api" --max-amount-sats 1000

# Wait for a payment notification
npx @getalby/cli wait-for-payment --payment-hash "abc123..."
Expand All @@ -161,7 +161,7 @@ HOLD invoices allow you to accept payments conditionally - the payment is held u

```bash
# Create a HOLD invoice (you provide the payment hash)
npx @getalby/cli make-hold-invoice --amount 1000 --payment-hash "abc123..."
npx @getalby/cli make-hold-invoice --amount-sats 1000 --payment-hash "abc123..."

# Settle a HOLD invoice (claim the payment)
npx @getalby/cli settle-hold-invoice --preimage "def456..."
Expand All @@ -179,7 +179,7 @@ These commands don't require a wallet connection:
npx @getalby/cli fiat-to-sats --currency USD --amount 10

# Convert sats to USD
npx @getalby/cli sats-to-fiat --amount 1000 --currency USD
npx @getalby/cli sats-to-fiat --amount-sats 1000 --currency USD

# Parse a BOLT-11 invoice
npx @getalby/cli parse-invoice --invoice "lnbc..."
Expand All @@ -188,7 +188,7 @@ npx @getalby/cli parse-invoice --invoice "lnbc..."
npx @getalby/cli verify-preimage --invoice "lnbc..." --preimage "abc123..."

# Request invoice from lightning address
npx @getalby/cli request-invoice-from-lightning-address --address "hello@getalby.com" --amount 1000
npx @getalby/cli request-invoice-from-lightning-address --address "hello@getalby.com" --amount-sats 1000
```

## Command Reference
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@getalby/cli",
"description": "CLI for Nostr Wallet Connect (NIP-47) with a few additional useful lightning tools",
"repository": "https://github.com/getAlby/cli.git",
"version": "0.7.0",
"version": "0.8.0",
"type": "module",
"main": "build/index.js",
"bin": {
Expand Down
11 changes: 7 additions & 4 deletions src/commands/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from "commander";
import { fetch402 } from "../tools/lightning/fetch.js";
import { getClient, handleError, output } from "../utils.js";
import { getClient, handleError, output, parseSatsOption } from "../utils.js";

export function registerFetch402Command(program: Command) {
program
Expand All @@ -13,9 +13,12 @@ export function registerFetch402Command(program: Command) {
.option("-b, --body <json>", "Request body (JSON string)")
.option("-H, --headers <json>", "Additional headers (JSON string)")
.option(
"--max-amount <sats>",
"--max-amount-sats <sats>",
"Maximum amount in sats to pay per request. Aborts if the endpoint requests more. (default: 5000, 0 = no limit)",
parseInt,
// allowZero: 0 is the documented "no limit" sentinel here. Strict parsing
// still rejects malformed values (e.g. "0.5", "abc") so a typo can't
// silently coerce to 0 and disable the spend cap.
parseSatsOption(true),
)
.action(async (url, options) => {
await handleError(async () => {
Expand All @@ -25,7 +28,7 @@ export function registerFetch402Command(program: Command) {
method: options.method,
body: options.body,
headers: options.headers ? JSON.parse(options.headers) : undefined,
maxAmountSats: options.maxAmount,
maxAmountSats: options.maxAmountSats,
});
output(result);
});
Expand Down
5 changes: 4 additions & 1 deletion src/commands/fiat-to-sats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ export function registerFiatToSatsCommand(program: Command) {
.command("fiat-to-sats")
.description("Convert fiat to sats")
.requiredOption("--currency <code>", "Currency code (e.g., USD, EUR)")
.requiredOption("-a, --amount <n>", "Fiat amount", parseFloat)
.requiredOption("--amount <n>", "Fiat amount", Number)
.action(async (options) => {
await handleError(async () => {
if (!Number.isFinite(options.amount) || options.amount <= 0) {
throw new Error(`Invalid --amount: ${options.amount}`);
}
const result = await fiatToSats({
currency: options.currency,
amount: options.amount,
Expand Down
6 changes: 3 additions & 3 deletions src/commands/make-hold-invoice.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Command } from "commander";
import { makeHoldInvoice } from "../tools/nwc/make_hold_invoice.js";
import { getClient, handleError, output } from "../utils.js";
import { getClient, handleError, output, parseSatsOption } from "../utils.js";

export function registerMakeHoldInvoiceCommand(program: Command) {
program
.command("make-hold-invoice")
.description("Create a HOLD invoice that requires manual settlement")
.requiredOption("-a, --amount <sats>", "Amount in sats", parseInt)
.requiredOption("--amount-sats <sats>", "Amount in sats", parseSatsOption())
.requiredOption("--payment-hash <hex>", "Payment hash (32 bytes hex)")
.option("-d, --description <text>", "Invoice description")
.option("-e, --expiry <seconds>", "Expiry time in seconds", parseInt)
.action(async (options) => {
await handleError(async () => {
const client = await getClient(program);
const result = await makeHoldInvoice(client, {
amount_in_sats: options.amount,
amount_in_sats: options.amountSats,
payment_hash: options.paymentHash,
description: options.description,
expiry: options.expiry,
Expand Down
6 changes: 3 additions & 3 deletions src/commands/make-invoice.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { Command } from "commander";
import { makeInvoice } from "../tools/nwc/make_invoice.js";
import { getClient, handleError, output } from "../utils.js";
import { getClient, handleError, output, parseSatsOption } from "../utils.js";

export function registerMakeInvoiceCommand(program: Command) {
program
.command("make-invoice")
.description("Create a lightning invoice")
.requiredOption("-a, --amount <sats>", "Amount in sats", parseInt)
.requiredOption("--amount-sats <sats>", "Amount in sats", parseSatsOption())
.option("-d, --description <text>", "Invoice description")
.option("-e, --expiry <seconds>", "Expiry time in seconds", parseInt)
.action(async (options) => {
await handleError(async () => {
const client = await getClient(program);
const result = await makeInvoice(client, {
amount_in_sats: options.amount,
amount_in_sats: options.amountSats,
description: options.description,
expiry: options.expiry,
});
Expand Down
2 changes: 1 addition & 1 deletion src/commands/pay-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function registerPayCryptoCommand(program: Command) {
)
.argument("<address>", "Recipient address on the target network")
.requiredOption(
"-a, --amount <number>",
"--amount <number>",
"Amount to send in target-currency units (e.g. 10 = 10 USDC)",
Number,
)
Expand Down
10 changes: 7 additions & 3 deletions src/commands/pay-invoice.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { Command } from "commander";
import { payInvoice } from "../tools/nwc/pay_invoice.js";
import { getClient, handleError, output } from "../utils.js";
import { getClient, handleError, output, parseSatsOption } from "../utils.js";

export function registerPayInvoiceCommand(program: Command) {
program
.command("pay-invoice")
.description("Pay a lightning invoice")
.argument("<bolt11>", "Invoice to pay")
.option("-a, --amount <sats>", "Amount (for zero-amount invoices)", parseInt)
.option(
"--amount-sats <sats>",
"Amount in sats (for zero-amount invoices)",
parseSatsOption(),
)
.action(async (invoice, options) => {
await handleError(async () => {
const client = await getClient(program);
const result = await payInvoice(client, {
invoice,
amount_in_sats: options.amount,
amount_in_sats: options.amountSats,
});
output(result);
});
Expand Down
6 changes: 3 additions & 3 deletions src/commands/pay-keysend.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Command } from "commander";
import { payKeysend, TlvRecord } from "../tools/nwc/pay_keysend.js";
import { getClient, handleError, output } from "../utils.js";
import { getClient, handleError, output, parseSatsOption } from "../utils.js";

export function registerPayKeysendCommand(program: Command) {
program
.command("pay-keysend")
.description("Send a keysend payment to a node")
.requiredOption("-p, --pubkey <hex>", "Destination node public key")
.requiredOption("-a, --amount <sats>", "Amount in sats", parseInt)
.requiredOption("--amount-sats <sats>", "Amount in sats", parseSatsOption())
.option("--preimage <hex>", "Preimage (optional, will be generated if not provided)")
.option("--tlv-records <json>", "TLV records as JSON array [{type, value}]")
.action(async (options) => {
Expand All @@ -19,7 +19,7 @@ export function registerPayKeysendCommand(program: Command) {
}
const result = await payKeysend(client, {
pubkey: options.pubkey,
amount_in_sats: options.amount,
amount_in_sats: options.amountSats,
preimage: options.preimage,
tlv_records: tlvRecords,
});
Expand Down
79 changes: 41 additions & 38 deletions src/commands/pay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
payCrypto,
findSupportedPair,
} from "../lendaswap/swap.js";
import { getClient, handleError, output } from "../utils.js";
import { getClient, handleError, output, parseSatsOption } from "../utils.js";

type DestinationType = "crypto" | "invoice" | "lightning-address" | "keysend";

Expand All @@ -29,13 +29,14 @@ function detectDestinationType(destination: string): DestinationType | null {
}

const ALLOWED_OPTS: Record<DestinationType, ReadonlyArray<string>> = {
invoice: ["amount"],
"lightning-address": ["amount", "comment"],
keysend: ["amount", "preimage", "tlvRecords"],
invoice: ["amountSats"],
"lightning-address": ["amountSats", "comment"],
keysend: ["amountSats", "preimage", "tlvRecords"],
crypto: ["amount", "currency", "network"],
};

const OPT_FLAG: Record<string, string> = {
amountSats: "--amount-sats",
amount: "--amount",
comment: "--comment",
preimage: "--preimage",
Expand All @@ -52,11 +53,24 @@ function rejectUnusedOpts(
const allowed = new Set(ALLOWED_OPTS[type]);
const used = Object.keys(options).filter((k) => providedKeys.has(k));
const stray = used.filter((k) => !allowed.has(k));
if (stray.length > 0) {
if (stray.length === 0) {
return;
}
// Mixing up the two amount flags is the most likely mistake — point the
// user straight at the correct one instead of a bare "not applicable".
if (stray.includes("amount") && type !== "crypto") {
throw new Error(
`Option${stray.length > 1 ? "s" : ""} ${stray.map((k) => OPT_FLAG[k] ?? `--${k}`).join(", ")} not applicable to ${type} payment`,
`Option --amount is not valid for ${type} payments — use --amount-sats (sats) instead`,
);
}
if (stray.includes("amountSats") && type === "crypto") {
throw new Error(
"Option --amount-sats is not valid for crypto payments — use --amount with --currency instead",
);
}
throw new Error(
`Option${stray.length > 1 ? "s" : ""} ${stray.map((k) => OPT_FLAG[k] ?? `--${k}`).join(", ")} not applicable to ${type} payment`,
);
}

export function registerPayCommand(program: Command) {
Expand All @@ -65,18 +79,23 @@ export function registerPayCommand(program: Command) {
.description(
"Pay any supported destination — auto-detects type from the destination string.\n\n" +
"Supported destinations:\n" +
" - BOLT-11 invoice (lnbc... / lntb... / lnbcrt... / lntbs...): no extra args (use --amount only for zero-amount invoices)\n" +
" - Lightning address (user@domain): requires --amount (sats); optional --comment\n" +
" - Node pubkey (66-char hex, compressed secp256k1): keysend, requires --amount (sats)\n" +
" - BOLT-11 invoice (lnbc... / lntb... / lnbcrt... / lntbs...): no extra args (use --amount-sats only for zero-amount invoices)\n" +
" - Lightning address (user@domain): requires --amount-sats; optional --comment\n" +
" - Node pubkey (66-char hex, compressed secp256k1): keysend, requires --amount-sats\n" +
" - EVM address (0x...): pay crypto/stablecoin, requires --amount, --currency, and --network",
)
.argument(
"<destination>",
"Invoice, lightning address, node pubkey, or EVM address",
)
.option(
"-a, --amount <number>",
"Amount — sats for lightning destinations, target-currency units for crypto (e.g. 10 = 10 USDC)",
"--amount-sats <sats>",
"Amount in sats — for lightning destinations (invoice, lightning address, keysend)",
parseSatsOption(),
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.option(
"--amount <number>",
"Amount in target-currency units for crypto, e.g. 10 = 10 USDC (use with --currency)",
Number,
)
.option("--comment <text>", "Comment for lightning address payments")
Expand All @@ -100,8 +119,8 @@ export function registerPayCommand(program: Command) {
"after",
"\nExamples:\n" +
" $ npx @getalby/cli pay lnbc1...\n" +
" $ npx @getalby/cli pay alice@getalby.com --amount 100 --comment hi\n" +
" $ npx @getalby/cli pay 02aabb... --amount 100\n" +
" $ npx @getalby/cli pay alice@getalby.com --amount-sats 100 --comment hi\n" +
" $ npx @getalby/cli pay 02aabb... --amount-sats 100\n" +
" $ npx @getalby/cli pay 0xabc... --amount 10 --currency USDC --network arbitrum\n",
)
.action(async (destination: string, options, cmd: Command) => {
Expand Down Expand Up @@ -132,37 +151,26 @@ export function registerPayCommand(program: Command) {

switch (type) {
case "invoice": {
if (
options.amount !== undefined &&
!Number.isInteger(options.amount)
) {
throw new Error(
`Invalid --amount: must be an integer number of sats`,
);
}
// --amount-sats is optional here (only for zero-amount invoices)
// and, when present, already validated by parseSatsOption.
const client = await getClient(program);
const result = await payInvoice(client, {
invoice: destination,
amount_in_sats: options.amount,
amount_in_sats: options.amountSats,
metadata: {},
});
output(result);
return;
}
case "lightning-address": {
if (options.amount === undefined) {
throw new Error(
"Lightning address payments require --amount <sats>",
);
}
if (!Number.isInteger(options.amount) || options.amount <= 0) {
if (options.amountSats === undefined) {
throw new Error(
`Invalid --amount: must be a positive integer number of sats`,
"Lightning address payments require --amount-sats <sats>",
);
}
const invoice = await requestInvoiceFromLightningAddress({
lightning_address: destination,
amount_in_sats: options.amount,
amount_in_sats: options.amountSats,
comment: options.comment,
});
const client = await getClient(program);
Expand All @@ -181,13 +189,8 @@ export function registerPayCommand(program: Command) {
return;
}
case "keysend": {
if (options.amount === undefined) {
throw new Error("Keysend payments require --amount <sats>");
}
if (!Number.isInteger(options.amount) || options.amount <= 0) {
throw new Error(
`Invalid --amount: must be a positive integer number of sats`,
);
if (options.amountSats === undefined) {
throw new Error("Keysend payments require --amount-sats <sats>");
}
let tlvRecords: TlvRecord[] | undefined;
if (options.tlvRecords) {
Expand All @@ -196,7 +199,7 @@ export function registerPayCommand(program: Command) {
const client = await getClient(program);
const result = await payKeysend(client, {
pubkey: destination,
amount_in_sats: options.amount,
amount_in_sats: options.amountSats,
preimage: options.preimage,
tlv_records: tlvRecords,
});
Expand Down
Loading
Loading