@getalby/cli 0.0.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.
Files changed (57) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +94 -0
  3. package/build/auth.js +20 -0
  4. package/build/commands/cancel-hold-invoice.js +17 -0
  5. package/build/commands/fetch-l402.js +23 -0
  6. package/build/commands/fiat-to-sats.js +18 -0
  7. package/build/commands/get-balance.js +14 -0
  8. package/build/commands/get-budget.js +14 -0
  9. package/build/commands/get-info.js +14 -0
  10. package/build/commands/get-wallet-service-info.js +14 -0
  11. package/build/commands/list-transactions.js +27 -0
  12. package/build/commands/lookup-invoice.js +23 -0
  13. package/build/commands/make-hold-invoice.js +23 -0
  14. package/build/commands/make-invoice.js +21 -0
  15. package/build/commands/parse-invoice.js +14 -0
  16. package/build/commands/pay-invoice.js +19 -0
  17. package/build/commands/pay-keysend.js +27 -0
  18. package/build/commands/request-invoice.js +20 -0
  19. package/build/commands/sats-to-fiat.js +18 -0
  20. package/build/commands/settle-hold-invoice.js +17 -0
  21. package/build/commands/sign-message.js +17 -0
  22. package/build/commands/verify-preimage.js +18 -0
  23. package/build/commands/wait-for-payment.js +21 -0
  24. package/build/index.js +50 -0
  25. package/build/mcp_server.js +36 -0
  26. package/build/sse.js +50 -0
  27. package/build/streamable_http.js +49 -0
  28. package/build/test/fetch-l402.test.js +7 -0
  29. package/build/test/helpers.js +61 -0
  30. package/build/test/lightning-tools.test.js +36 -0
  31. package/build/test/nwc-hold-invoices.test.js +79 -0
  32. package/build/test/nwc-payments.test.js +42 -0
  33. package/build/test/nwc-readonly.test.js +41 -0
  34. package/build/tools/lightning/fetch_l402.js +32 -0
  35. package/build/tools/lightning/fiat_to_sats.js +10 -0
  36. package/build/tools/lightning/parse_invoice.js +8 -0
  37. package/build/tools/lightning/request_invoice.js +14 -0
  38. package/build/tools/lightning/sats_to_fiat.js +11 -0
  39. package/build/tools/lightning/schemas/invoice.js +20 -0
  40. package/build/tools/lightning/verify_preimage.js +9 -0
  41. package/build/tools/nwc/cancel_hold_invoice.js +8 -0
  42. package/build/tools/nwc/get_balance.js +6 -0
  43. package/build/tools/nwc/get_budget.js +14 -0
  44. package/build/tools/nwc/get_info.js +3 -0
  45. package/build/tools/nwc/get_wallet_service_info.js +3 -0
  46. package/build/tools/nwc/list_transactions.js +18 -0
  47. package/build/tools/nwc/lookup_invoice.js +11 -0
  48. package/build/tools/nwc/make_hold_invoice.js +12 -0
  49. package/build/tools/nwc/make_invoice.js +14 -0
  50. package/build/tools/nwc/pay_invoice.js +12 -0
  51. package/build/tools/nwc/pay_keysend.js +12 -0
  52. package/build/tools/nwc/schemas/transaction.js +26 -0
  53. package/build/tools/nwc/settle_hold_invoice.js +8 -0
  54. package/build/tools/nwc/sign_message.js +6 -0
  55. package/build/tools/nwc/wait_for_payment.js +43 -0
  56. package/build/utils.js +23 -0
  57. package/package.json +47 -0
@@ -0,0 +1,17 @@
1
+ import { signMessage } from "../tools/nwc/sign_message.js";
2
+ import { getClient, handleError, output } from "../utils.js";
3
+ export function registerSignMessageCommand(program) {
4
+ program
5
+ .command("sign-message")
6
+ .description("Sign a message with the wallet's key")
7
+ .requiredOption("-m, --message <text>", "Message to sign")
8
+ .action(async (options) => {
9
+ await handleError(async () => {
10
+ const client = getClient(program);
11
+ const result = await signMessage(client, {
12
+ message: options.message,
13
+ });
14
+ output(result);
15
+ });
16
+ });
17
+ }
@@ -0,0 +1,18 @@
1
+ import { verifyPreimage } from "../tools/lightning/verify_preimage.js";
2
+ import { handleError, output } from "../utils.js";
3
+ export function registerVerifyPreimageCommand(program) {
4
+ program
5
+ .command("verify-preimage")
6
+ .description("Verify a preimage against an invoice")
7
+ .requiredOption("-i, --invoice <bolt11>", "BOLT-11 invoice")
8
+ .requiredOption("--preimage <hex>", "Preimage to verify (32 bytes hex)")
9
+ .action(async (options) => {
10
+ await handleError(async () => {
11
+ const result = verifyPreimage({
12
+ invoice: options.invoice,
13
+ preimage: options.preimage,
14
+ });
15
+ output(result);
16
+ });
17
+ });
18
+ }
@@ -0,0 +1,21 @@
1
+ import { waitForPayment } from "../tools/nwc/wait_for_payment.js";
2
+ import { getClient, handleError, output } from "../utils.js";
3
+ export function registerWaitForPaymentCommand(program) {
4
+ program
5
+ .command("wait-for-payment")
6
+ .description("Wait for a payment notification")
7
+ .requiredOption("--payment-hash <hex>", "Payment hash to wait for")
8
+ .option("--type <type>", "Notification type filter: payment_received, payment_sent, hold_invoice_accepted")
9
+ .option("--timeout <seconds>", "Timeout in seconds", parseInt)
10
+ .action(async (options) => {
11
+ await handleError(async () => {
12
+ const client = getClient(program);
13
+ const result = await waitForPayment(client, {
14
+ payment_hash: options.paymentHash,
15
+ type: options.type,
16
+ timeout: options.timeout,
17
+ });
18
+ output(result);
19
+ });
20
+ });
21
+ }
package/build/index.js ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { registerGetBalanceCommand } from "./commands/get-balance.js";
4
+ import { registerGetBudgetCommand } from "./commands/get-budget.js";
5
+ import { registerGetInfoCommand } from "./commands/get-info.js";
6
+ import { registerGetWalletServiceInfoCommand } from "./commands/get-wallet-service-info.js";
7
+ import { registerMakeInvoiceCommand } from "./commands/make-invoice.js";
8
+ import { registerMakeHoldInvoiceCommand } from "./commands/make-hold-invoice.js";
9
+ import { registerSettleHoldInvoiceCommand } from "./commands/settle-hold-invoice.js";
10
+ import { registerCancelHoldInvoiceCommand } from "./commands/cancel-hold-invoice.js";
11
+ import { registerPayInvoiceCommand } from "./commands/pay-invoice.js";
12
+ import { registerPayKeysendCommand } from "./commands/pay-keysend.js";
13
+ import { registerLookupInvoiceCommand } from "./commands/lookup-invoice.js";
14
+ import { registerListTransactionsCommand } from "./commands/list-transactions.js";
15
+ import { registerWaitForPaymentCommand } from "./commands/wait-for-payment.js";
16
+ import { registerSignMessageCommand } from "./commands/sign-message.js";
17
+ import { registerFiatToSatsCommand } from "./commands/fiat-to-sats.js";
18
+ import { registerSatsToFiatCommand } from "./commands/sats-to-fiat.js";
19
+ import { registerParseInvoiceCommand } from "./commands/parse-invoice.js";
20
+ import { registerVerifyPreimageCommand } from "./commands/verify-preimage.js";
21
+ import { registerRequestInvoiceCommand } from "./commands/request-invoice.js";
22
+ import { registerFetchL402Command } from "./commands/fetch-l402.js";
23
+ const program = new Command();
24
+ program
25
+ .name("alby-cli")
26
+ .description("CLI for Nostr Wallet Connect (NIP-47) with lightning tools")
27
+ .version("0.0.0")
28
+ .option("-c, --connection-secret <string>", "NWC connection secret (nostr+walletconnect://...)");
29
+ // Register all commands
30
+ registerGetBalanceCommand(program);
31
+ registerGetBudgetCommand(program);
32
+ registerGetInfoCommand(program);
33
+ registerGetWalletServiceInfoCommand(program);
34
+ registerMakeInvoiceCommand(program);
35
+ registerMakeHoldInvoiceCommand(program);
36
+ registerSettleHoldInvoiceCommand(program);
37
+ registerCancelHoldInvoiceCommand(program);
38
+ registerPayInvoiceCommand(program);
39
+ registerPayKeysendCommand(program);
40
+ registerLookupInvoiceCommand(program);
41
+ registerListTransactionsCommand(program);
42
+ registerWaitForPaymentCommand(program);
43
+ registerSignMessageCommand(program);
44
+ registerFiatToSatsCommand(program);
45
+ registerSatsToFiatCommand(program);
46
+ registerParseInvoiceCommand(program);
47
+ registerVerifyPreimageCommand(program);
48
+ registerRequestInvoiceCommand(program);
49
+ registerFetchL402Command(program);
50
+ program.parse();
@@ -0,0 +1,36 @@
1
+ import { webln } from "@getalby/sdk";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { registerGetInfoTool } from "./tools/nwc/get_info.js";
4
+ import { registerGetWalletServiceInfoTool } from "./tools/nwc/get_wallet_service_info.js";
5
+ import { registerLookupInvoiceTool } from "./tools/nwc/lookup_invoice.js";
6
+ import { registerMakeInvoiceTool } from "./tools/nwc/make_invoice.js";
7
+ import { registerPayInvoiceTool } from "./tools/nwc/pay_invoice.js";
8
+ import { registerGetBalanceTool } from "./tools/nwc/get_balance.js";
9
+ import { registerListTransactionsTool } from "./tools/nwc/list_transactions.js";
10
+ import { registerFetchL402Tool } from "./tools/lightning/fetch_l402.js";
11
+ import { registerFiatToSatsTool } from "./tools/lightning/fiat_to_sats.js";
12
+ import { registerParseInvoiceTool } from "./tools/lightning/parse_invoice.js";
13
+ import { registerRequestInvoiceFromLightningAddressTool } from "./tools/lightning/request_invoice.js";
14
+ export function createMCPServer(client) {
15
+ const server = new McpServer({
16
+ name: "@getalby/mcp",
17
+ version: "1.1.0",
18
+ title: "Alby MCP Server",
19
+ });
20
+ // NWC
21
+ registerGetWalletServiceInfoTool(server, client);
22
+ registerGetInfoTool(server, client);
23
+ registerMakeInvoiceTool(server, client);
24
+ registerPayInvoiceTool(server, client);
25
+ registerGetBalanceTool(server, client);
26
+ registerLookupInvoiceTool(server, client);
27
+ registerListTransactionsTool(server, client);
28
+ // Lightning tools
29
+ registerFetchL402Tool(server, new webln.NostrWebLNProvider({
30
+ client,
31
+ }));
32
+ registerFiatToSatsTool(server);
33
+ registerParseInvoiceTool(server);
34
+ registerRequestInvoiceFromLightningAddressTool(server);
35
+ return server;
36
+ }
package/build/sse.js ADDED
@@ -0,0 +1,50 @@
1
+ import { nwc } from "@getalby/sdk";
2
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
3
+ import { createMCPServer } from "./mcp_server.js";
4
+ import { getConnectionSecret } from "./auth.js";
5
+ export function addSSEEndpoints(app) {
6
+ // Store transports for each session type
7
+ // SSE is deprecated so we do not put much effort in here (e.g. cleaning up unused sessions)
8
+ const sessions = {};
9
+ app.get("/sse", async (req, res) => {
10
+ const nostrWalletConnectUrl = getConnectionSecret(req.header("Authorization"), req.query.nwc);
11
+ if (!nostrWalletConnectUrl) {
12
+ res
13
+ .status(400)
14
+ .send("Bearer auth with NWC connection secret or nwc query parameter not provided");
15
+ return;
16
+ }
17
+ const client = new nwc.NWCClient({
18
+ nostrWalletConnectUrl,
19
+ });
20
+ const transport = new SSEServerTransport("/messages", res);
21
+ const server = createMCPServer(client);
22
+ sessions[transport.sessionId] = {
23
+ server,
24
+ transport,
25
+ };
26
+ console.info("Created new SSE session", transport.sessionId);
27
+ if (req.query.sessionId) {
28
+ console.info("Request provided its own session ID: " + req.query.sessionId);
29
+ sessions[req.query.sessionId] = {
30
+ server,
31
+ transport,
32
+ };
33
+ }
34
+ server.connect(transport);
35
+ });
36
+ app.post("/messages", (req, res) => {
37
+ const sessionId = req.query.sessionId;
38
+ console.info("SSE messages request", sessionId);
39
+ const session = sessions[sessionId];
40
+ if (session) {
41
+ console.info("Found session for messages request", sessionId);
42
+ session.transport.handlePostMessage(req, res);
43
+ }
44
+ else {
45
+ res
46
+ .status(400)
47
+ .send("No transport found for sessionId: " + req.query.sessionId);
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,49 @@
1
+ import { createMCPServer } from "./mcp_server.js";
2
+ import { nwc } from "@getalby/sdk";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { getConnectionSecret } from "./auth.js";
5
+ import { json } from "express";
6
+ export function addStreamableHttpEndpoints(app) {
7
+ app.post("/mcp", json(), async (req, res) => {
8
+ // In stateless mode, create a new instance of transport and server for each request
9
+ // to ensure complete isolation. A single instance would cause request ID collisions
10
+ // when multiple clients connect concurrently.
11
+ try {
12
+ const nostrWalletConnectUrl = getConnectionSecret(req.header("Authorization"), req.query.nwc);
13
+ if (!nostrWalletConnectUrl) {
14
+ res
15
+ .status(400)
16
+ .send("Bearer auth with NWC connection secret or nwc query parameter not provided");
17
+ return;
18
+ }
19
+ const client = new nwc.NWCClient({
20
+ nostrWalletConnectUrl,
21
+ });
22
+ const server = createMCPServer(client);
23
+ const transport = new StreamableHTTPServerTransport({
24
+ sessionIdGenerator: undefined,
25
+ });
26
+ res.on("close", () => {
27
+ console.log("Request closed");
28
+ transport.close();
29
+ server.close();
30
+ client.close();
31
+ });
32
+ await server.connect(transport);
33
+ await transport.handleRequest(req, res, req.body);
34
+ }
35
+ catch (error) {
36
+ console.error("Error handling MCP request:", error);
37
+ if (!res.headersSent) {
38
+ res.status(500).json({
39
+ jsonrpc: "2.0",
40
+ error: {
41
+ code: -32603,
42
+ message: "Internal server error",
43
+ },
44
+ id: null,
45
+ });
46
+ }
47
+ }
48
+ });
49
+ }
@@ -0,0 +1,7 @@
1
+ import { describe, test, expect } from "vitest";
2
+ describe("L402 Commands", () => {
3
+ // NOTE: there is no easy way to test l402 right now
4
+ test.skip("fetch-l402 fetches L402 protected resource", () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ });
@@ -0,0 +1,61 @@
1
+ import { execSync } from "child_process";
2
+ import { createHash, randomBytes } from "crypto";
3
+ export function generateHoldInvoiceParams() {
4
+ const preimage = randomBytes(32).toString("hex");
5
+ const paymentHash = createHash("sha256")
6
+ .update(Buffer.from(preimage, "hex"))
7
+ .digest("hex");
8
+ return { preimage, paymentHash };
9
+ }
10
+ export async function createTestWallet(retries = 5) {
11
+ for (let i = 0; i < retries; i++) {
12
+ try {
13
+ const response = await fetch("https://faucet.nwc.dev?balance=10000", {
14
+ method: "POST",
15
+ });
16
+ if (!response.ok) {
17
+ if (i < retries - 1) {
18
+ await new Promise((r) => setTimeout(r, 2000 * (i + 1)));
19
+ continue;
20
+ }
21
+ throw new Error(`Faucet request failed: ${response.status}`);
22
+ }
23
+ const nwcUrl = (await response.text()).trim();
24
+ const lud16Match = nwcUrl.match(/lud16=([^&\s]+)/);
25
+ if (!lud16Match) {
26
+ throw new Error("No lud16 in NWC URL");
27
+ }
28
+ return {
29
+ nwcUrl,
30
+ lightningAddress: decodeURIComponent(lud16Match[1]),
31
+ };
32
+ }
33
+ catch (error) {
34
+ if (i < retries - 1) {
35
+ await new Promise((r) => setTimeout(r, 2000 * (i + 1)));
36
+ continue;
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+ throw new Error("Failed to create test wallet");
42
+ }
43
+ export function runCli(args) {
44
+ try {
45
+ const result = execSync(`node build/index.js ${args}`, {
46
+ encoding: "utf-8",
47
+ cwd: process.cwd(),
48
+ });
49
+ return { success: true, output: JSON.parse(result) };
50
+ }
51
+ catch (error) {
52
+ const err = error;
53
+ const errorOutput = err.stderr || err.stdout || "{}";
54
+ try {
55
+ return { success: false, output: JSON.parse(errorOutput) };
56
+ }
57
+ catch {
58
+ return { success: false, output: { error: errorOutput } };
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { runCli } from "./helpers.js";
3
+ const exampleInvoice = "lnbc1u1p5hlrr8dqqnp4qwmtpr4p72ms7gnq3pkfk2876y2msvl33s3840dlp6xsv2w59dpscpp55utq6s8u5407namwt4jvhgsaf9fyszppjfwyxp7qsw6cyc8vxukqsp583usez9yhmkcavvvjz8cq56v3nglh2q37xkf4ufrgwxfrfjkm54s9qyysgqcqzp2xqyz5vqgtyysw64zt9sj6kfpqnekzwc37y2uyg0xdapgxqqth4uahff0x89sjfsvukjlllasg5dn05u2uha6qcvxz2y3ye5k7958qtes4pv4ggqtnjyky";
4
+ const exampleLightningAddress = "nwc1769966844@getalby.com";
5
+ describe("Lightning Tools (no wallet required)", () => {
6
+ test("fiat-to-sats converts USD to sats", () => {
7
+ const result = runCli("fiat-to-sats -a 1 --currency USD");
8
+ expect(result.success).toBe(true);
9
+ expect(result.output.amount_in_sats).toBeTypeOf("number");
10
+ expect(result.output.amount_in_sats).toBeGreaterThan(0);
11
+ });
12
+ test("sats-to-fiat converts sats to USD", () => {
13
+ const result = runCli("sats-to-fiat -a 1000 --currency USD");
14
+ expect(result.success).toBe(true);
15
+ expect(result.output.amount).toBeTypeOf("number");
16
+ expect(result.output.amount).toBeGreaterThan(0);
17
+ });
18
+ test("parse-invoice parses a BOLT-11 invoice", () => {
19
+ const result = runCli(`parse-invoice -i "${exampleInvoice}"`);
20
+ expect(result.success).toBe(true);
21
+ expect(result.output.paymentHash).toBeDefined();
22
+ expect(result.output.amount_in_sats).toBe(100);
23
+ });
24
+ test("verify-preimage returns false for invalid preimage", () => {
25
+ // Use a fake preimage (32 bytes of zeros)
26
+ const fakePreimage = "0000000000000000000000000000000000000000000000000000000000000000";
27
+ const result = runCli(`verify-preimage -i "${exampleInvoice}" --preimage "${fakePreimage}"`);
28
+ expect(result.success).toBe(true);
29
+ expect(result.output.valid).toBe(false);
30
+ });
31
+ test("request-invoice requests invoice from lightning address", async () => {
32
+ const result = runCli(`request-invoice -a "${exampleLightningAddress}" -s 100`);
33
+ expect(result.success).toBe(true);
34
+ expect(result.output.paymentRequest.toLowerCase()).toMatch(/^lnbc/);
35
+ });
36
+ });
@@ -0,0 +1,79 @@
1
+ import { describe, test, expect, beforeAll } from "vitest";
2
+ import { spawn } from "child_process";
3
+ import { createTestWallet, runCli, generateHoldInvoiceParams, } from "./helpers.js";
4
+ describe("NWC HOLD Invoice Commands", () => {
5
+ let sender;
6
+ let receiver;
7
+ beforeAll(async () => {
8
+ // Create wallets sequentially to avoid faucet rate limiting
9
+ receiver = await createTestWallet();
10
+ sender = await createTestWallet();
11
+ }, 60000);
12
+ test("make-hold-invoice creates hold invoice", () => {
13
+ const { paymentHash } = generateHoldInvoiceParams();
14
+ const result = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice -a 100 --payment-hash "${paymentHash}"`);
15
+ expect(result.success).toBe(true);
16
+ expect(result.output.invoice).toBeDefined();
17
+ expect(result.output.payment_hash).toBe(paymentHash);
18
+ });
19
+ test("settle-hold-invoice settles with preimage", async () => {
20
+ const { preimage, paymentHash } = generateHoldInvoiceParams();
21
+ // Create a hold invoice
22
+ const holdResult = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice -a 100 --payment-hash "${paymentHash}"`);
23
+ expect(holdResult.success).toBe(true);
24
+ // Start wait-for-payment in background
25
+ const waitProcess = spawn("node", [
26
+ "build/index.js",
27
+ "-c",
28
+ receiver.nwcUrl,
29
+ "wait-for-payment",
30
+ "-p",
31
+ paymentHash,
32
+ "-t",
33
+ "30",
34
+ ], { stdio: ["ignore", "pipe", "pipe"] });
35
+ // Give it time to start listening
36
+ await new Promise((r) => setTimeout(r, 1000));
37
+ // Pay the invoice from sender (this will be held)
38
+ const payProcess = spawn("node", [
39
+ "build/index.js",
40
+ "-c",
41
+ sender.nwcUrl,
42
+ "pay-invoice",
43
+ "-i",
44
+ holdResult.output.invoice,
45
+ ], { stdio: ["ignore", "pipe", "pipe"] });
46
+ // Wait for the hold invoice to be accepted
47
+ await new Promise((r) => setTimeout(r, 2000));
48
+ // Settle the hold invoice
49
+ const settleResult = runCli(`-c "${receiver.nwcUrl}" settle-hold-invoice --preimage "${preimage}"`);
50
+ expect(settleResult.success).toBe(true);
51
+ expect(settleResult.output.preimage).toBe(preimage);
52
+ // Cleanup
53
+ waitProcess.kill();
54
+ payProcess.kill();
55
+ }, 60000);
56
+ test("cancel-hold-invoice cancels hold invoice", async () => {
57
+ const { paymentHash } = generateHoldInvoiceParams();
58
+ // Create a hold invoice
59
+ const holdResult = runCli(`-c "${receiver.nwcUrl}" make-hold-invoice -a 100 --payment-hash "${paymentHash}"`);
60
+ expect(holdResult.success).toBe(true);
61
+ // Pay the invoice from sender (this will put it in held state)
62
+ const payProcess = spawn("node", [
63
+ "build/index.js",
64
+ "-c",
65
+ sender.nwcUrl,
66
+ "pay-invoice",
67
+ "-i",
68
+ holdResult.output.invoice,
69
+ ], { stdio: ["ignore", "pipe", "pipe"] });
70
+ // Wait for the hold invoice to be in held state
71
+ await new Promise((r) => setTimeout(r, 2000));
72
+ // Cancel the hold invoice (while it's held)
73
+ const cancelResult = runCli(`-c "${receiver.nwcUrl}" cancel-hold-invoice --payment-hash "${paymentHash}"`);
74
+ expect(cancelResult.success).toBe(true);
75
+ expect(cancelResult.output.payment_hash).toBe(paymentHash);
76
+ // Cleanup
77
+ payProcess.kill();
78
+ }, 60000);
79
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, test, expect, beforeAll } from "vitest";
2
+ import { createTestWallet, runCli } from "./helpers.js";
3
+ describe("NWC Payment Commands", () => {
4
+ let sender;
5
+ let receiver;
6
+ beforeAll(async () => {
7
+ // Create wallets sequentially to avoid faucet rate limiting
8
+ sender = await createTestWallet();
9
+ receiver = await createTestWallet();
10
+ }, 60000);
11
+ test("make-invoice and pay-invoice", () => {
12
+ // Create invoice with receiver wallet
13
+ const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice -a 100`);
14
+ expect(invoiceResult.success).toBe(true);
15
+ expect(invoiceResult.output.invoice).toBeDefined();
16
+ // Pay with sender wallet
17
+ const paymentResult = runCli(`-c "${sender.nwcUrl}" pay-invoice -i "${invoiceResult.output.invoice}"`);
18
+ expect(paymentResult.success).toBe(true);
19
+ expect(paymentResult.output.preimage).toBeDefined();
20
+ });
21
+ test("lookup-invoice finds paid invoice", () => {
22
+ // Create an invoice
23
+ const invoiceResult = runCli(`-c "${receiver.nwcUrl}" make-invoice -a 50`);
24
+ expect(invoiceResult.success).toBe(true);
25
+ // Pay the invoice first (unpaid invoices may not be found)
26
+ const payResult = runCli(`-c "${sender.nwcUrl}" pay-invoice -i "${invoiceResult.output.invoice}"`);
27
+ expect(payResult.success).toBe(true);
28
+ // Lookup the paid invoice using the invoice string
29
+ const lookupResult = runCli(`-c "${receiver.nwcUrl}" lookup-invoice -i "${invoiceResult.output.invoice}"`);
30
+ expect(lookupResult.success).toBe(true);
31
+ expect(lookupResult.output.payment_hash).toBeDefined();
32
+ });
33
+ test("pay-keysend sends keysend payment", () => {
34
+ // Get receiver's pubkey
35
+ const infoResult = runCli(`-c "${receiver.nwcUrl}" get-info`);
36
+ expect(infoResult.success).toBe(true);
37
+ // Send keysend payment
38
+ const keysendResult = runCli(`-c "${sender.nwcUrl}" pay-keysend -p "${infoResult.output.pubkey}" -a 100`);
39
+ expect(keysendResult.success).toBe(true);
40
+ expect(keysendResult.output.preimage).toBeDefined();
41
+ });
42
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, test, expect, beforeAll } from "vitest";
2
+ import { createTestWallet, runCli } from "./helpers.js";
3
+ describe("NWC Read-only Commands", () => {
4
+ let wallet;
5
+ beforeAll(async () => {
6
+ wallet = await createTestWallet();
7
+ }, 60000);
8
+ test("get-balance returns wallet balance", () => {
9
+ const result = runCli(`-c "${wallet.nwcUrl}" get-balance`);
10
+ expect(result.success).toBe(true);
11
+ expect(result.output.amount_in_sats).toBeTypeOf("number");
12
+ });
13
+ test("get-budget returns budget info", () => {
14
+ const result = runCli(`-c "${wallet.nwcUrl}" get-budget`);
15
+ expect(result.success).toBe(true);
16
+ expect(result.output).toBeDefined();
17
+ });
18
+ test("get-info returns wallet info", () => {
19
+ const result = runCli(`-c "${wallet.nwcUrl}" get-info`);
20
+ expect(result.success).toBe(true);
21
+ expect(result.output.alias).toBeDefined();
22
+ expect(result.output.pubkey).toBeDefined();
23
+ });
24
+ test("get-wallet-service-info returns service capabilities", () => {
25
+ const result = runCli(`-c "${wallet.nwcUrl}" get-wallet-service-info`);
26
+ expect(result.success).toBe(true);
27
+ expect(Array.isArray(result.output.capabilities)).toBe(true);
28
+ });
29
+ test("list-transactions returns transaction list", () => {
30
+ const result = runCli(`-c "${wallet.nwcUrl}" list-transactions`);
31
+ expect(result.success).toBe(true);
32
+ expect(Array.isArray(result.output.transactions)).toBe(true);
33
+ });
34
+ // NOTE: Faucet wallets don't have sign_message scope
35
+ test.skip("sign-message signs a message", () => {
36
+ const testMessage = "Hello, World!";
37
+ const result = runCli(`-c "${wallet.nwcUrl}" sign-message -m "${testMessage}"`);
38
+ expect(result.success).toBe(true);
39
+ expect(result.output.signature).toBeTypeOf("string");
40
+ });
41
+ });
@@ -0,0 +1,32 @@
1
+ import { fetchWithL402 } from "@getalby/lightning-tools";
2
+ export async function fetchL402(client, params) {
3
+ const requestOptions = {
4
+ method: params.method,
5
+ };
6
+ if (params.method && params.method !== "GET" && params.method !== "HEAD") {
7
+ requestOptions.body = params.body;
8
+ requestOptions.headers = {
9
+ "Content-Type": "application/json",
10
+ ...params.headers,
11
+ };
12
+ }
13
+ else if (params.headers) {
14
+ requestOptions.headers = params.headers;
15
+ }
16
+ const webln = {
17
+ sendPayment: async (invoice) => {
18
+ const result = await client.payInvoice({ invoice });
19
+ return { preimage: result.preimage };
20
+ },
21
+ };
22
+ const result = await fetchWithL402(params.url, requestOptions, {
23
+ webln: webln,
24
+ });
25
+ const responseContent = await result.text();
26
+ if (!result.ok) {
27
+ throw new Error(`fetch returned non-OK status: ${result.status} ${responseContent}`);
28
+ }
29
+ return {
30
+ content: responseContent,
31
+ };
32
+ }
@@ -0,0 +1,10 @@
1
+ import { getSatoshiValue } from "@getalby/lightning-tools";
2
+ export async function fiatToSats(params) {
3
+ const satoshi = await getSatoshiValue({
4
+ amount: params.amount,
5
+ currency: params.currency,
6
+ });
7
+ return {
8
+ amount_in_sats: satoshi,
9
+ };
10
+ }
@@ -0,0 +1,8 @@
1
+ import { Invoice } from "@getalby/lightning-tools";
2
+ export function parseInvoice(params) {
3
+ const { satoshi, ...invoice } = new Invoice({ pr: params.invoice });
4
+ return {
5
+ ...invoice,
6
+ amount_in_sats: satoshi,
7
+ };
8
+ }
@@ -0,0 +1,14 @@
1
+ import { LightningAddress } from "@getalby/lightning-tools";
2
+ export async function requestInvoice(params) {
3
+ const ln = new LightningAddress(params.lightning_address);
4
+ await ln.fetch();
5
+ const { satoshi, ...invoice } = await ln.requestInvoice({
6
+ satoshi: params.amount_in_sats,
7
+ comment: params.comment,
8
+ payerdata: params.payer_data,
9
+ });
10
+ return {
11
+ ...invoice,
12
+ amount_in_sats: satoshi,
13
+ };
14
+ }
@@ -0,0 +1,11 @@
1
+ import { getFiatValue } from "@getalby/lightning-tools";
2
+ export async function satsToFiat(params) {
3
+ const fiat = await getFiatValue({
4
+ satoshi: params.amount_in_sats,
5
+ currency: params.currency,
6
+ });
7
+ return {
8
+ amount: fiat,
9
+ currency: params.currency,
10
+ };
11
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ export const invoiceSchema = {
3
+ paymentRequest: z.string().describe("The BOLT-11 payment request"),
4
+ paymentHash: z.string().describe("Payment hash"),
5
+ preimage: z.string().nullable().describe("Payment preimage if available"),
6
+ verify: z
7
+ .string()
8
+ .nullable()
9
+ .describe("URL to verify if the email was paid (LNURL-verify)"),
10
+ amount_in_sats: z.number().describe("Amount in sats"),
11
+ expiry: z.number().nullish().describe("Expiry time in seconds"),
12
+ timestamp: z.number().describe("Creation unix timestamp"),
13
+ createdDate: z.string().describe("Creation date string"),
14
+ expiryDate: z.string().nullish().describe("Expiry date string"),
15
+ description: z.string().nullable().describe("Invoice description"),
16
+ successAction: z
17
+ .unknown()
18
+ .nullable()
19
+ .describe("Success action to initiate after the invoice has been paid"),
20
+ };
@@ -0,0 +1,9 @@
1
+ import { Invoice } from "@getalby/lightning-tools";
2
+ export function verifyPreimage(params) {
3
+ const invoice = new Invoice({ pr: params.invoice });
4
+ const valid = invoice.validatePreimage(params.preimage);
5
+ return {
6
+ valid,
7
+ payment_hash: invoice.paymentHash,
8
+ };
9
+ }
@@ -0,0 +1,8 @@
1
+ export async function cancelHoldInvoice(client, params) {
2
+ await client.cancelHoldInvoice({
3
+ payment_hash: params.payment_hash,
4
+ });
5
+ return {
6
+ payment_hash: params.payment_hash,
7
+ };
8
+ }
@@ -0,0 +1,6 @@
1
+ export async function getBalance(client) {
2
+ const balance = await client.getBalance();
3
+ return {
4
+ amount_in_sats: Math.floor(balance.balance / 1000),
5
+ };
6
+ }