Skip to content

Commit 7f04d8a

Browse files
committed
feat: update SDK + CLI for Lightning (LNbits)
SDK (packages/sdk/src/lightning.js): - Add enableWallet() (replaces provisionNode, kept as deprecated alias) - Add registerAddress(), getAddress(), checkAddressAvailable() - Add createInvoice() for BOLT11 - Update sendPayment() to accept lightning addresses + bolt11 - Add wallet_id and direction params to listPayments() - Mark BOLT12 createOffer() as deprecated (LNbits limitation) - Update all JSDoc: Greenlight → LNbits, custodial model CLI (packages/sdk/bin/coinpay.js): - Add 'ln' / 'lightning' command group - ln enable, ln address, ln address-check, ln invoice - ln send, ln payments, ln balance
1 parent 13031cd commit 7f04d8a

File tree

3 files changed

+347
-61
lines changed

3 files changed

+347
-61
lines changed

packages/sdk/bin/coinpay.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2567,6 +2567,147 @@ async function main() {
25672567
}
25682568

25692569
try {
2570+
2571+
async function handleLightning(subcommand, args, flags) {
2572+
const client = createClient();
2573+
2574+
switch (subcommand) {
2575+
case 'enable': {
2576+
const walletId = flags['wallet-id'] || flags['wallet'];
2577+
if (!walletId) {
2578+
console.error(colors.red + 'Error: --wallet-id required' + colors.reset);
2579+
process.exit(1);
2580+
}
2581+
const mnemonic = await getDecryptedMnemonic(flags);
2582+
const result = await client.lightning.enableWallet({
2583+
wallet_id: walletId,
2584+
mnemonic,
2585+
business_id: flags['business-id'],
2586+
});
2587+
console.log(colors.green + '⚡ Lightning wallet enabled!' + colors.reset);
2588+
console.log(JSON.stringify(result, null, 2));
2589+
break;
2590+
}
2591+
2592+
case 'address': {
2593+
const walletId = flags['wallet-id'] || flags['wallet'];
2594+
const username = flags['username'] || args[0];
2595+
if (!walletId) {
2596+
console.error(colors.red + 'Error: --wallet-id required' + colors.reset);
2597+
process.exit(1);
2598+
}
2599+
if (!username) {
2600+
// GET current address
2601+
const result = await client.lightning.getAddress(walletId);
2602+
if (result.lightning_address) {
2603+
console.log(colors.green + '⚡ ' + result.lightning_address + colors.reset);
2604+
} else {
2605+
console.log('No Lightning Address registered. Use: coinpay ln address --username <name>');
2606+
}
2607+
} else {
2608+
// POST register address
2609+
const result = await client.lightning.registerAddress({ wallet_id: walletId, username });
2610+
console.log(colors.green + '⚡ Registered: ' + result.lightning_address + colors.reset);
2611+
}
2612+
break;
2613+
}
2614+
2615+
case 'address-check': {
2616+
const username = flags['username'] || args[0];
2617+
if (!username) {
2618+
console.error(colors.red + 'Error: --username required' + colors.reset);
2619+
process.exit(1);
2620+
}
2621+
const result = await client.lightning.checkAddressAvailable(username);
2622+
console.log(result.available
2623+
? colors.green + '✓ ' + username + ' is available' + colors.reset
2624+
: colors.red + '✗ ' + username + ' is taken' + colors.reset);
2625+
break;
2626+
}
2627+
2628+
case 'invoice': {
2629+
const walletId = flags['wallet-id'] || flags['wallet'];
2630+
const amount = parseInt(flags['amount'] || args[0]);
2631+
if (!walletId || !amount) {
2632+
console.error(colors.red + 'Error: --wallet-id and --amount required' + colors.reset);
2633+
process.exit(1);
2634+
}
2635+
const result = await client.lightning.createInvoice({
2636+
wallet_id: walletId,
2637+
amount_sats: amount,
2638+
description: flags['description'] || flags['desc'] || '',
2639+
});
2640+
console.log(colors.green + '⚡ Invoice created' + colors.reset);
2641+
console.log(JSON.stringify(result, null, 2));
2642+
break;
2643+
}
2644+
2645+
case 'send': {
2646+
const walletId = flags['wallet-id'] || flags['wallet'];
2647+
const destination = flags['to'] || args[0];
2648+
const amount = flags['amount'] ? parseInt(flags['amount']) : undefined;
2649+
if (!walletId || !destination) {
2650+
console.error(colors.red + 'Error: --wallet-id and --to required' + colors.reset);
2651+
process.exit(1);
2652+
}
2653+
const result = await client.lightning.sendPayment({
2654+
wallet_id: walletId,
2655+
destination,
2656+
amount_sats: amount,
2657+
});
2658+
console.log(colors.green + '⚡ Payment sent!' + colors.reset);
2659+
console.log(JSON.stringify(result, null, 2));
2660+
break;
2661+
}
2662+
2663+
case 'payments': {
2664+
const walletId = flags['wallet-id'] || flags['wallet'];
2665+
if (!walletId) {
2666+
console.error(colors.red + 'Error: --wallet-id required' + colors.reset);
2667+
process.exit(1);
2668+
}
2669+
const result = await client.lightning.listPayments({
2670+
wallet_id: walletId,
2671+
direction: flags['direction'],
2672+
limit: flags['limit'] ? parseInt(flags['limit']) : undefined,
2673+
});
2674+
const payments = result.data?.payments || result.payments || [];
2675+
if (payments.length === 0) {
2676+
console.log('No Lightning payments found.');
2677+
} else {
2678+
for (const p of payments) {
2679+
const dir = p.direction === 'outgoing' ? colors.red + '↑ SENT' : colors.green + '↓ RECV';
2680+
const sats = Math.floor((p.amount_msat || 0) / 1000);
2681+
console.log(`${dir}${colors.reset} ${sats} sats ${p.status} ${p.created_at}`);
2682+
}
2683+
}
2684+
break;
2685+
}
2686+
2687+
case 'balance': {
2688+
const walletId = flags['wallet-id'] || flags['wallet'];
2689+
if (!walletId) {
2690+
console.error(colors.red + 'Error: --wallet-id required' + colors.reset);
2691+
process.exit(1);
2692+
}
2693+
// Use the web-wallet balance endpoint filtered by LN
2694+
const result = await client.request(`/web-wallet/balance?wallet_id=${walletId}`);
2695+
const balances = result.data?.balances || result.balances || [];
2696+
const ln = balances.find(b => b.chain === 'LN' || b.currency === 'LN');
2697+
if (ln) {
2698+
console.log(colors.green + '⚡ ' + ln.balance + ' BTC (' + Math.floor(parseFloat(ln.balance) * 100_000_000) + ' sats)' + colors.reset);
2699+
} else {
2700+
console.log('No Lightning balance. Enable Lightning first: coinpay ln enable');
2701+
}
2702+
break;
2703+
}
2704+
2705+
default:
2706+
console.log(`Unknown lightning command: ${subcommand}`);
2707+
showHelp();
2708+
}
2709+
}
2710+
25702711
switch (command) {
25712712
case 'config':
25722713
await handleConfig(subcommand, args);
@@ -2596,6 +2737,11 @@ async function main() {
25962737
await handleEscrow(subcommand, args, flags);
25972738
break;
25982739

2740+
case 'ln':
2741+
case 'lightning':
2742+
await handleLightning(subcommand, args, flags);
2743+
break;
2744+
25992745
case 'webhook':
26002746
await handleWebhook(subcommand, args, flags);
26012747
break;

packages/sdk/src/lightning.js

Lines changed: 101 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/**
22
* Lightning Module for CoinPay SDK
33
*
4-
* Manages BOLT12 Lightning Network operations: node provisioning,
5-
* offer creation, and payment tracking.
4+
* Manages Lightning Network operations: wallet provisioning (via LNbits),
5+
* lightning address registration, invoice creation, payment sending, and history.
6+
*
7+
* Lightning wallets are custodial — funds are held on CoinPay's server.
68
*/
79

8-
const DEFAULT_BASE_URL = 'https://coinpayportal.com/api';
9-
1010
/**
11-
* Lightning client for BOLT12 operations
11+
* Lightning client for LN operations
1212
*/
1313
export class LightningClient {
1414
#client;
@@ -21,24 +21,31 @@ export class LightningClient {
2121
}
2222

2323
// ──────────────────────────────────────────────
24-
// Nodes
24+
// Nodes / Wallet Provisioning
2525
// ──────────────────────────────────────────────
2626

2727
/**
28-
* Provision a Greenlight CLN node for a wallet.
28+
* Enable Lightning for a wallet (provisions an LNbits custodial wallet).
2929
* @param {Object} params
3030
* @param {string} params.wallet_id - Wallet UUID
31-
* @param {string} params.mnemonic - BIP39 mnemonic (used server-side to derive LN keys)
31+
* @param {string} params.mnemonic - BIP39 mnemonic
3232
* @param {string} [params.business_id] - Optional business UUID
33-
* @returns {Promise<Object>} The provisioned node
33+
* @returns {Promise<Object>} The provisioned node record
3434
*/
35-
async provisionNode({ wallet_id, mnemonic, business_id }) {
35+
async enableWallet({ wallet_id, mnemonic, business_id }) {
3636
return this.#client.request('/lightning/nodes', {
3737
method: 'POST',
3838
body: JSON.stringify({ wallet_id, mnemonic, business_id }),
3939
});
4040
}
4141

42+
/**
43+
* @deprecated Use enableWallet() instead
44+
*/
45+
async provisionNode(params) {
46+
return this.enableWallet(params);
47+
}
48+
4249
/**
4350
* Get node status by ID.
4451
* @param {string} nodeId
@@ -58,31 +65,81 @@ export class LightningClient {
5865
}
5966

6067
// ──────────────────────────────────────────────
61-
// Offers
68+
// Lightning Address
6269
// ──────────────────────────────────────────────
6370

6471
/**
65-
* Create a BOLT12 offer.
72+
* Register a Lightning Address (username@coinpayportal.com).
73+
* Requires Lightning to be enabled first.
6674
* @param {Object} params
67-
* @param {string} params.wallet_id - Wallet UUID (required for wallet ownership checks)
68-
* @param {string} params.node_id - Node UUID
69-
* @param {string} params.description - Human-readable description
70-
* @param {number} [params.amount_msat] - Fixed amount in millisatoshis (omit for any-amount)
71-
* @param {string} [params.currency] - Currency code (default: "BTC")
72-
* @param {string} [params.business_id] - Optional business UUID
73-
* @returns {Promise<Object>}
75+
* @param {string} params.wallet_id - Wallet UUID
76+
* @param {string} params.username - Desired username (3-32 chars, lowercase alphanumeric)
77+
* @returns {Promise<Object>} { lightning_address, username }
78+
*/
79+
async registerAddress({ wallet_id, username }) {
80+
return this.#client.request('/lightning/address', {
81+
method: 'POST',
82+
body: JSON.stringify({ wallet_id, username }),
83+
});
84+
}
85+
86+
/**
87+
* Get Lightning Address for a wallet.
88+
* @param {string} walletId - Wallet UUID
89+
* @returns {Promise<Object>} { lightning_address, username } or { lightning_address: null }
90+
*/
91+
async getAddress(walletId) {
92+
return this.#client.request(`/lightning/address?wallet_id=${walletId}`);
93+
}
94+
95+
/**
96+
* Check if a Lightning Address username is available.
97+
* @param {string} username
98+
* @returns {Promise<Object>} { available: boolean }
99+
*/
100+
async checkAddressAvailable(username) {
101+
return this.#client.request(`/lightning/address?username=${encodeURIComponent(username)}`);
102+
}
103+
104+
// ──────────────────────────────────────────────
105+
// Invoices
106+
// ──────────────────────────────────────────────
107+
108+
/**
109+
* Create a BOLT11 invoice to receive a payment.
110+
* @param {Object} params
111+
* @param {string} params.wallet_id - Wallet UUID
112+
* @param {number} params.amount_sats - Amount in satoshis
113+
* @param {string} [params.description] - Invoice description/memo
114+
* @returns {Promise<Object>} { payment_request, payment_hash, ... }
74115
*/
75-
async createOffer({ wallet_id, node_id, description, amount_msat, currency, business_id }) {
116+
async createInvoice({ wallet_id, amount_sats, description }) {
117+
return this.#client.request('/lightning/invoices', {
118+
method: 'POST',
119+
body: JSON.stringify({ wallet_id, amount_sats, description }),
120+
});
121+
}
122+
123+
// ──────────────────────────────────────────────
124+
// Offers (BOLT12) — currently not supported
125+
// ──────────────────────────────────────────────
126+
127+
/**
128+
* Create a BOLT12 offer.
129+
* @deprecated BOLT12 offers are not currently supported (LNbits limitation).
130+
* Use createInvoice() or registerAddress() instead.
131+
*/
132+
async createOffer(params) {
76133
return this.#client.request('/lightning/offers', {
77134
method: 'POST',
78-
body: JSON.stringify({ wallet_id, node_id, description, amount_msat, currency, business_id }),
135+
body: JSON.stringify(params),
79136
});
80137
}
81138

82139
/**
83140
* Get offer by ID.
84141
* @param {string} offerId
85-
* @returns {Promise<Object>} Includes offer and qr_uri
142+
* @returns {Promise<Object>}
86143
*/
87144
async getOffer(offerId) {
88145
return this.#client.request(`/lightning/offers/${offerId}`);
@@ -93,9 +150,9 @@ export class LightningClient {
93150
* @param {Object} [params]
94151
* @param {string} [params.business_id]
95152
* @param {string} [params.node_id]
96-
* @param {string} [params.status] - "active" | "disabled" | "archived"
97-
* @param {number} [params.limit] - Default 20
98-
* @param {number} [params.offset] - Default 0
153+
* @param {string} [params.status]
154+
* @param {number} [params.limit]
155+
* @param {number} [params.offset]
99156
* @returns {Promise<Object>} { offers, total, limit, offset }
100157
*/
101158
async listOffers(params = {}) {
@@ -110,12 +167,31 @@ export class LightningClient {
110167
// Payments
111168
// ──────────────────────────────────────────────
112169

170+
/**
171+
* Send a Lightning payment. Accepts:
172+
* - BOLT11 invoice string
173+
* - Lightning address (user@domain)
174+
* @param {Object} params
175+
* @param {string} params.wallet_id - Wallet UUID
176+
* @param {string} params.destination - BOLT11 invoice or lightning address (user@domain)
177+
* @param {number} [params.amount_sats] - Amount in satoshis (required for lightning addresses, optional for invoices with amount)
178+
* @returns {Promise<Object>} { payment_hash, status }
179+
*/
180+
async sendPayment({ wallet_id, destination, amount_sats }) {
181+
return this.#client.request('/lightning/payments', {
182+
method: 'POST',
183+
body: JSON.stringify({ wallet_id, bolt12: destination, amount_sats }),
184+
});
185+
}
186+
113187
/**
114188
* List Lightning payments.
115189
* @param {Object} [params]
190+
* @param {string} [params.wallet_id]
116191
* @param {string} [params.business_id]
117192
* @param {string} [params.node_id]
118193
* @param {string} [params.offer_id]
194+
* @param {string} [params.direction] - "incoming" | "outgoing"
119195
* @param {string} [params.status] - "pending" | "settled" | "failed"
120196
* @param {number} [params.limit] - Default 50
121197
* @param {number} [params.offset] - Default 0
@@ -129,22 +205,6 @@ export class LightningClient {
129205
return this.#client.request(`/lightning/payments?${qs}`);
130206
}
131207

132-
/**
133-
* Send a payment to a BOLT12 offer or invoice.
134-
* @param {Object} params
135-
* @param {string} params.wallet_id - Wallet UUID (required for wallet ownership checks)
136-
* @param {string} params.node_id - Node UUID
137-
* @param {string} params.bolt12 - BOLT12 offer or invoice string
138-
* @param {number} params.amount_sats - Amount in satoshis
139-
* @returns {Promise<Object>}
140-
*/
141-
async sendPayment({ wallet_id, node_id, bolt12, amount_sats }) {
142-
return this.#client.request('/lightning/payments', {
143-
method: 'POST',
144-
body: JSON.stringify({ wallet_id, node_id, bolt12, amount_sats }),
145-
});
146-
}
147-
148208
/**
149209
* Get payment status by payment hash.
150210
* @param {string} paymentHash

0 commit comments

Comments
 (0)