Skip to content

Commit d4f2c0d

Browse files
committed
add support for HODL invoices
1 parent 1a873f7 commit d4f2c0d

File tree

11 files changed

+2440
-53
lines changed

11 files changed

+2440
-53
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ The node currently exposes the following APIs:
203203
- `/assetmetadata` (POST)
204204
- `/backup` (POST)
205205
- `/btcbalance` (POST)
206+
- `/cancelhodlinvoice` (POST)
206207
- `/changepassword` (POST)
207208
- `/checkindexerurl` (POST)
208209
- `/checkproxyendpoint` (POST)
@@ -217,7 +218,9 @@ The node currently exposes the following APIs:
217218
- `/getassetmedia` (POST)
218219
- `/getchannelid` (POST)
219220
- `/getpayment` (POST)
221+
- `/getpaymentpreimage` (POST)
220222
- `/getswap` (POST)
223+
- `/hodlinvoice` (POST)
221224
- `/init` (POST)
222225
- `/invoicestatus` (POST)
223226
- `/issueassetcfa` (POST)
@@ -248,6 +251,7 @@ The node currently exposes the following APIs:
248251
- `/sendbtc` (POST)
249252
- `/sendonionmessage` (POST)
250253
- `/sendpayment` (POST)
254+
- `/settlehodlinvoice` (POST)
251255
- `/shutdown` (POST)
252256
- `/signmessage` (POST)
253257
- `/sync` (POST)

openapi.yaml

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,24 @@ paths:
115115
application/json:
116116
schema:
117117
$ref: '#/components/schemas/BtcBalanceResponse'
118+
/cancelhodlinvoice:
119+
post:
120+
tags:
121+
- Invoices
122+
summary: Cancel a HODL invoice
123+
description: Cancel a held HTLC for a HODL invoice. Rejects cancellation if a settlement is already in progress.
124+
requestBody:
125+
content:
126+
application/json:
127+
schema:
128+
$ref: '#/components/schemas/InvoiceCancelRequest'
129+
responses:
130+
'200':
131+
description: Successful operation
132+
content:
133+
application/json:
134+
schema:
135+
$ref: '#/components/schemas/EmptyResponse'
118136
/changepassword:
119137
post:
120138
tags:
@@ -368,6 +386,24 @@ paths:
368386
application/json:
369387
schema:
370388
$ref: '#/components/schemas/GetPaymentResponse'
389+
/getpaymentpreimage:
390+
post:
391+
tags:
392+
- Payments
393+
summary: Get a payment preimage by its payment hash
394+
description: Get the preimage for an outbound payment when it has been completed successfully
395+
requestBody:
396+
content:
397+
application/json:
398+
schema:
399+
$ref: '#/components/schemas/GetPaymentPreimageRequest'
400+
responses:
401+
'200':
402+
description: Successful operation
403+
content:
404+
application/json:
405+
schema:
406+
$ref: '#/components/schemas/GetPaymentPreimageResponse'
371407
/getswap:
372408
post:
373409
tags:
@@ -386,6 +422,24 @@ paths:
386422
application/json:
387423
schema:
388424
$ref: '#/components/schemas/GetSwapResponse'
425+
/hodlinvoice:
426+
post:
427+
tags:
428+
- Invoices
429+
summary: Create a HODL LN invoice
430+
description: Create a BOLT11 invoice with a caller-provided payment hash; settlement is deferred until settle/cancel. Metadata is persisted first to preserve HODL semantics across restarts.
431+
requestBody:
432+
content:
433+
application/json:
434+
schema:
435+
$ref: '#/components/schemas/InvoiceHodlRequest'
436+
responses:
437+
'200':
438+
description: Successful operation
439+
content:
440+
application/json:
441+
schema:
442+
$ref: '#/components/schemas/InvoiceHodlResponse'
389443
/init:
390444
post:
391445
tags:
@@ -892,6 +946,24 @@ paths:
892946
application/json:
893947
schema:
894948
$ref: '#/components/schemas/SendPaymentResponse'
949+
/settlehodlinvoice:
950+
post:
951+
tags:
952+
- Invoices
953+
summary: Settle a HODL invoice
954+
description: Claim a held HTLC for a HODL invoice
955+
requestBody:
956+
content:
957+
application/json:
958+
schema:
959+
$ref: '#/components/schemas/InvoiceSettleRequest'
960+
responses:
961+
'200':
962+
description: Successful operation
963+
content:
964+
application/json:
965+
schema:
966+
$ref: '#/components/schemas/EmptyResponse'
895967
/shutdown:
896968
post:
897969
tags:
@@ -1519,6 +1591,23 @@ components:
15191591
properties:
15201592
payment:
15211593
$ref: '#/components/schemas/Payment'
1594+
GetPaymentPreimageRequest:
1595+
type: object
1596+
required:
1597+
- payment_hash
1598+
properties:
1599+
payment_hash:
1600+
type: string
1601+
example: b4cb2da889477082a2e47f37a07e646e60ef6f97ffa7a4d88c823efd673da94b
1602+
GetPaymentPreimageResponse:
1603+
type: object
1604+
properties:
1605+
status:
1606+
$ref: '#/components/schemas/HTLCStatus'
1607+
preimage:
1608+
type: string
1609+
nullable: true
1610+
example: eade701c7b23b8799465f4284ad84710fc16a776fbc6483001291149122695a8
15221611
GetSwapRequest:
15231612
type: object
15241613
properties:
@@ -1537,7 +1626,9 @@ components:
15371626
type: string
15381627
enum:
15391628
- Pending
1629+
- Claimable
15401630
- Succeeded
1631+
- Cancelled
15411632
- Failed
15421633
IndexerProtocol:
15431634
type: string
@@ -1556,11 +1647,65 @@ components:
15561647
mnemonic:
15571648
type: string
15581649
example: skill lamp please gown put season degree collect decline account monitor insane
1650+
InvoiceCancelRequest:
1651+
type: object
1652+
required:
1653+
- payment_hash
1654+
properties:
1655+
payment_hash:
1656+
type: string
1657+
example: 3febfae1e68b190c15461f4c2a3290f9af1dae63fd7d620d2bd61601869026cd
1658+
InvoiceHodlRequest:
1659+
type: object
1660+
required:
1661+
- payment_hash
1662+
- expiry_sec
1663+
properties:
1664+
amt_msat:
1665+
type: integer
1666+
example: 3000000
1667+
expiry_sec:
1668+
type: integer
1669+
example: 86400
1670+
asset_id:
1671+
type: string
1672+
example: rgb:CJkb4YZw-jRiz2sk-~PARPio-wtVYI1c-XAEYCqO-wTfvRZ8
1673+
asset_amount:
1674+
type: integer
1675+
example: 42
1676+
payment_hash:
1677+
type: string
1678+
example: 3febfae1e68b190c15461f4c2a3290f9af1dae63fd7d620d2bd61601869026cd
1679+
external_ref:
1680+
type: string
1681+
example: swap-123
1682+
InvoiceHodlResponse:
1683+
type: object
1684+
properties:
1685+
invoice:
1686+
type: string
1687+
example: lnbcrt30u1pjv6yzndqud3jxktt5w46x7unfv9kz6mn0v3jsnp4qdpc280eur52luxppv6f3nnj8l6vnd9g2hnv3qv6mjhmhvlzf6327pp5tjjasx6g9dqptea3fhm6yllq5wxzycnnvp8l6wcq3d6j2uvpryuqsp5l8az8x3g8fe05dg7cmgddld3da09nfjvky8xftwsk4cj8p2l7kfq9qyysgqcqpcxqzdylzlwfnkyw3jv344x4rzwgkk53ng0fhxy5rdduk4g5tpvea8xa6rfckkza35va28xjn2tqkhgarcxep5umm4x5k56wfcdvu95eq7qzp20vrl4xz76syapsa3c09j7lg5gerkaj63llj0ark7ph8hfketn6fkqzm8laf66dhsncm23wkwm5l5377we9e8lnlknnkwje5eefkccusqm6rqt8
1688+
payment_secret:
1689+
type: string
1690+
example: 777a7756c620868199ed5fdc35bee4095b5709d543e5c2bf0494396bf27d2ea2
1691+
InvoiceSettleRequest:
1692+
type: object
1693+
required:
1694+
- payment_hash
1695+
- payment_preimage
1696+
properties:
1697+
payment_hash:
1698+
type: string
1699+
example: b4cb2da889477082a2e47f37a07e646e60ef6f97ffa7a4d88c823efd673da94b
1700+
payment_preimage:
1701+
type: string
1702+
example: eade701c7b23b8799465f4284ad84710fc16a776fbc6483001291149122695a8
15591703
InvoiceStatus:
15601704
type: string
15611705
enum:
15621706
- Pending
15631707
- Succeeded
1708+
- Cancelled
15641709
- Failed
15651710
- Expired
15661711
InvoiceStatusRequest:

regtest.sh

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@ _wait_for_bitcoind() {
4949
done
5050
}
5151

52+
_wait_for_bitcoind_rpc() {
53+
# wait for RPC to accept requests
54+
start_time=$(date +%s)
55+
until $BITCOIN_CLI getblockcount >/dev/null 2>&1; do
56+
current_time=$(date +%s)
57+
if [ $((current_time - start_time)) -gt $TIMEOUT ]; then
58+
echo "Timeout waiting for bitcoind RPC to start"
59+
$COMPOSE logs bitcoind
60+
exit 1
61+
fi
62+
sleep 1
63+
done
64+
}
65+
5266
_wait_for_electrs() {
5367
# wait for electrs to have completed startup
5468
start_time=$(date +%s)
@@ -74,9 +88,11 @@ _start_services() {
7488
_die "port $port is already bound, services can't be started"
7589
fi
7690
done
77-
$COMPOSE up -d
91+
$COMPOSE up -d bitcoind
7892
echo && echo "preparing bitcoind wallet"
7993
_wait_for_bitcoind
94+
_wait_for_bitcoind_rpc
95+
$COMPOSE up -d electrs proxy
8096
$BITCOIN_CLI createwallet miner >/dev/null
8197
$BITCOIN_CLI -rpcwallet=miner -generate $INITIAL_BLOCKS >/dev/null
8298
echo "waiting for electrs to have completed startup"

src/disk.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,17 @@ use std::sync::Arc;
1515

1616
use crate::error::APIError;
1717
use crate::ldk::{
18-
ChannelIdsMap, InboundPaymentInfoStorage, NetworkGraph, OutboundPaymentInfoStorage,
19-
OutputSpenderTxes, SwapMap,
18+
ChannelIdsMap, ClaimablePaymentStorage, InboundPaymentInfoStorage, InvoiceMetadataStorage,
19+
NetworkGraph, OutboundPaymentInfoStorage, OutputSpenderTxes, SwapMap,
2020
};
2121
use crate::utils::{parse_peer_info, LOGS_DIR};
2222

2323
pub(crate) const LDK_LOGS_FILE: &str = "logs.txt";
2424

2525
pub(crate) const INBOUND_PAYMENTS_FNAME: &str = "inbound_payments";
2626
pub(crate) const OUTBOUND_PAYMENTS_FNAME: &str = "outbound_payments";
27+
pub(crate) const INVOICE_METADATA_FNAME: &str = "invoice_metadata";
28+
pub(crate) const CLAIMABLE_HTLCS_FNAME: &str = "claimable_htlcs";
2729

2830
pub(crate) const CHANNEL_PEER_DATA: &str = "channel_peer_data";
2931

@@ -178,6 +180,28 @@ pub(crate) fn read_outbound_payment_info(path: &Path) -> OutboundPaymentInfoStor
178180
}
179181
}
180182

183+
pub(crate) fn read_invoice_metadata(path: &Path) -> InvoiceMetadataStorage {
184+
if let Ok(file) = File::open(path) {
185+
if let Ok(info) = InvoiceMetadataStorage::read(&mut BufReader::new(file)) {
186+
return info;
187+
}
188+
}
189+
InvoiceMetadataStorage {
190+
invoices: new_hash_map(),
191+
}
192+
}
193+
194+
pub(crate) fn read_claimable_htlcs(path: &Path) -> ClaimablePaymentStorage {
195+
if let Ok(file) = File::open(path) {
196+
if let Ok(info) = ClaimablePaymentStorage::read(&mut BufReader::new(file)) {
197+
return info;
198+
}
199+
}
200+
ClaimablePaymentStorage {
201+
payments: new_hash_map(),
202+
}
203+
}
204+
181205
pub(crate) fn read_output_spender_txes(path: &Path) -> OutputSpenderTxes {
182206
if let Ok(file) = File::open(path) {
183207
if let Ok(info) = OutputSpenderTxes::read(&mut BufReader::new(file)) {

src/error.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ pub enum APIError {
162162
#[error("Invalid payment hash: {0}")]
163163
InvalidPaymentHash(String),
164164

165+
#[error("Payment hash already used")]
166+
PaymentHashAlreadyUsed,
167+
165168
#[error("Invalid payment secret")]
166169
InvalidPaymentSecret,
167170

@@ -210,6 +213,24 @@ pub enum APIError {
210213
#[error("Invalid transport endpoints: {0}")]
211214
InvalidTransportEndpoints(String),
212215

216+
#[error("Invoice is expired")]
217+
InvoiceExpired,
218+
219+
#[error("HTLC claim deadline exceeded")]
220+
ClaimDeadlineExceeded,
221+
222+
#[error("Invoice is not marked as HODL")]
223+
InvoiceNotHodl,
224+
225+
#[error("No claimable HTLC found for this invoice")]
226+
InvoiceNotClaimable,
227+
228+
#[error("Invoice settlement is in progress")]
229+
InvoiceSettlingInProgress,
230+
231+
#[error("Invoice is already settled")]
232+
InvoiceAlreadySettled,
233+
213234
#[error("IO error: {0}")]
214235
IO(#[from] std::io::Error),
215236

@@ -234,6 +255,9 @@ pub enum APIError {
234255
#[error("Unable to find payment preimage, be sure you've provided the correct swap info")]
235256
MissingSwapPaymentPreimage,
236257

258+
#[error("Invalid payment preimage")]
259+
InvalidPaymentPreimage,
260+
237261
#[error("Network error: {0}")]
238262
Network(String),
239263

@@ -437,13 +461,16 @@ impl IntoResponse for APIError {
437461
| APIError::InvalidOnionData(_)
438462
| APIError::InvalidPassword(_)
439463
| APIError::InvalidPaymentHash(_)
464+
| APIError::PaymentHashAlreadyUsed
440465
| APIError::InvalidPaymentSecret
466+
| APIError::InvalidPaymentPreimage
441467
| APIError::InvalidPeerInfo(_)
442468
| APIError::InvalidPrecision(_)
443469
| APIError::InvalidPubkey
444470
| APIError::InvalidRecipientData(_)
445471
| APIError::InvalidRecipientID
446472
| APIError::InvalidRecipientNetwork
473+
| APIError::InvoiceExpired
447474
| APIError::InvalidSwap(_)
448475
| APIError::InvalidSwapString(_, _)
449476
| APIError::InvalidTicker(_)
@@ -454,6 +481,7 @@ impl IntoResponse for APIError {
454481
| APIError::MediaFileNotProvided
455482
| APIError::MissingSwapPaymentPreimage
456483
| APIError::OutputBelowDustLimit
484+
| APIError::ClaimDeadlineExceeded
457485
| APIError::UnsupportedBackupVersion { .. } => {
458486
(StatusCode::BAD_REQUEST, self.to_string(), self.name())
459487
}
@@ -499,6 +527,13 @@ impl IntoResponse for APIError {
499527
| APIError::UnsupportedTransportType => {
500528
(StatusCode::FORBIDDEN, self.to_string(), self.name())
501529
}
530+
APIError::InvoiceNotClaimable => (StatusCode::NOT_FOUND, self.to_string(), self.name()),
531+
APIError::InvoiceAlreadySettled => {
532+
(StatusCode::CONFLICT, self.to_string(), self.name())
533+
}
534+
APIError::InvoiceNotHodl | APIError::InvoiceSettlingInProgress => {
535+
(StatusCode::FORBIDDEN, self.to_string(), self.name())
536+
}
502537
APIError::Network(_) | APIError::NoValidTransportEndpoint => (
503538
StatusCode::SERVICE_UNAVAILABLE,
504539
self.to_string(),

0 commit comments

Comments
 (0)