Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Show ready-to-copy settle and cancel commands in `holdinvoice` output
- Simplify LND funding step in README to a single command instead of clipboard-based two-step flow

### Fixed
- Validate LNURL-withdraw callback invoices by millisatoshis (`num_msat`) to preserve msat precision for min/max range checks
- Preserve LNURL-pay invoice millisatoshi precision by creating invoices with LND `value_msat` instead of truncating callback amounts to sats

### Added
- `bolt11` command in `bitcoin-cli` for creating regular Lightning invoices (supports `--msat` and `-m` memo)
- LND hold invoice commands in `bitcoin-cli`: `holdinvoice`, `settleinvoice`, `cancelinvoice`
Expand Down
6 changes: 3 additions & 3 deletions lnurl-server/routes/pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ router.get('/:paymentId/callback', asyncHandler(async (req, res) => {
throw new ValidationError(validationErrors.join(', '));
}

const amountMsat = parseInt(amount);
const amountMsat = parseInt(amount, 10);
const amountSats = Math.floor(amountMsat / 1000);

// Create invoice
const invoice = await lndService.createInvoice(
amountSats,
const invoice = await lndService.createInvoiceMsat(
amountMsat,
comment ? `LNURL Payment ${paymentId} - ${comment}` : `LNURL Payment ${paymentId}`,
3600
);
Expand Down
17 changes: 8 additions & 9 deletions lnurl-server/routes/withdraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,19 @@ router.get('/callback', asyncHandler(async (req, res) => {
const minValue = minWithdrawable ? parseInt(minWithdrawable, 10) : config.limits.minWithdrawable;
const maxValue = maxWithdrawable ? parseInt(maxWithdrawable, 10) : config.limits.maxWithdrawable;

// Convert msats to sats for validation
const minSats = Math.ceil(minValue / 1000);
const maxSats = Math.floor(maxValue / 1000);

try {
// Decode the invoice to get the amount
const decodedInvoice = await lndService.decodePayReq(pr);
const invoiceAmountSats = decodedInvoice.num_satoshis;
const invoiceAmountSats = parseInt(decodedInvoice.num_satoshis, 10);
const invoiceAmountMsat = decodedInvoice.num_msat
? parseInt(decodedInvoice.num_msat, 10)
: invoiceAmountSats * 1000;

Logger.withdrawal('processing', { k1, amount: invoiceAmountSats, minSats, maxSats });
Logger.withdrawal('processing', { k1, amountSats: invoiceAmountSats, amountMsat: invoiceAmountMsat, minWithdrawable: minValue, maxWithdrawable: maxValue });

// Validate amount is within limits
if (!Validation.isAmountInRange(invoiceAmountSats, minSats, maxSats)) {
throw new ValidationError(`Amount out of range (${minSats} - ${maxSats} sats)`);
// Validate msat amount is within limits
if (!Validation.isAmountInRange(invoiceAmountMsat, minValue, maxValue)) {
throw new ValidationError(`Amount out of range (${minValue} - ${maxValue} msat)`);
}

// Pay the invoice
Expand Down
8 changes: 8 additions & 0 deletions lnurl-server/services/lnd.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ class LNDService {
});
}

async createInvoiceMsat(valueMsat, memo, expiry = 3600) {
return this.call('invoices', {
value_msat: valueMsat.toString(),
memo: memo,
expiry: expiry
});
}

async getInvoice(paymentHash) {
return this.rest(`/v1/invoice/${paymentHash}`, 'GET');
}
Expand Down