Skip to content

Commit 17ea616

Browse files
committed
add support for HODL invoices
1 parent fa667a6 commit 17ea616

22 files changed

Lines changed: 1691 additions & 114 deletions

README.md

Lines changed: 3 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,6 +218,7 @@ The node currently exposes the following APIs:
217218
- `/getassetmedia` (POST)
218219
- `/getchannelid` (POST)
219220
- `/getpayment` (POST)
221+
- `/getpaymentpreimage` (POST)
220222
- `/getswap` (POST)
221223
- `/init` (POST)
222224
- `/invoicestatus` (POST)
@@ -248,6 +250,7 @@ The node currently exposes the following APIs:
248250
- `/sendonionmessage` (POST)
249251
- `/sendpayment` (POST)
250252
- `/sendrgb` (POST)
253+
- `/settlehodlinvoice` (POST)
251254
- `/shutdown` (POST)
252255
- `/signmessage` (POST)
253256
- `/sync` (POST)

openapi.yaml

Lines changed: 106 additions & 2 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:
@@ -636,7 +672,7 @@ paths:
636672
tags:
637673
- Invoices
638674
summary: Get a LN invoice
639-
description: Get a LN invoice to receive a payment
675+
description: Get a LN invoice to receive a payment. Provide `payment_hash` to create a HODL invoice.
640676
requestBody:
641677
content:
642678
application/json:
@@ -892,6 +928,24 @@ paths:
892928
application/json:
893929
schema:
894930
$ref: '#/components/schemas/SendRgbResponse'
931+
/settlehodlinvoice:
932+
post:
933+
tags:
934+
- Invoices
935+
summary: Settle a HODL invoice
936+
description: Claim a held HTLC for a HODL invoice
937+
requestBody:
938+
content:
939+
application/json:
940+
schema:
941+
$ref: '#/components/schemas/SettleHodlInvoiceRequest'
942+
responses:
943+
'200':
944+
description: Successful operation
945+
content:
946+
application/json:
947+
schema:
948+
$ref: '#/components/schemas/SettleHodlInvoiceResponse'
895949
/shutdown:
896950
post:
897951
tags:
@@ -1245,6 +1299,14 @@ components:
12451299
$ref: '#/components/schemas/BtcBalance'
12461300
colored:
12471301
$ref: '#/components/schemas/BtcBalance'
1302+
CancelHodlInvoiceRequest:
1303+
type: object
1304+
required:
1305+
- payment_hash
1306+
properties:
1307+
payment_hash:
1308+
type: string
1309+
example: 3febfae1e68b190c15461f4c2a3290f9af1dae63fd7d620d2bd61601869026cd
12481310
ChangePasswordRequest:
12491311
type: object
12501312
properties:
@@ -1330,7 +1392,7 @@ components:
13301392
CheckProxyEndpointRequest:
13311393
type: object
13321394
properties:
1333-
proxy_url:
1395+
proxy_endpoint:
13341396
type: string
13351397
example: rpc://127.0.0.1:3000/json-rpc
13361398
CloseChannelRequest:
@@ -1519,6 +1581,23 @@ components:
15191581
properties:
15201582
payment:
15211583
$ref: '#/components/schemas/Payment'
1584+
GetPaymentPreimageRequest:
1585+
type: object
1586+
required:
1587+
- payment_hash
1588+
properties:
1589+
payment_hash:
1590+
type: string
1591+
example: b4cb2da889477082a2e47f37a07e646e60ef6f97ffa7a4d88c823efd673da94b
1592+
GetPaymentPreimageResponse:
1593+
type: object
1594+
properties:
1595+
status:
1596+
$ref: '#/components/schemas/HTLCStatus'
1597+
preimage:
1598+
type: string
1599+
nullable: true
1600+
example: eade701c7b23b8799465f4284ad84710fc16a776fbc6483001291149122695a8
15221601
GetSwapRequest:
15231602
type: object
15241603
properties:
@@ -1537,7 +1616,9 @@ components:
15371616
type: string
15381617
enum:
15391618
- Pending
1619+
- Claimable
15401620
- Succeeded
1621+
- Cancelled
15411622
- Failed
15421623
IndexerProtocol:
15431624
type: string
@@ -1561,6 +1642,7 @@ components:
15611642
enum:
15621643
- Pending
15631644
- Succeeded
1645+
- Cancelled
15641646
- Failed
15651647
- Expired
15661648
InvoiceStatusRequest:
@@ -1784,6 +1866,10 @@ components:
17841866
asset_amount:
17851867
type: integer
17861868
example: 42
1869+
payment_hash:
1870+
type: string
1871+
description: Optional. When provided, the invoice is created as HODL.
1872+
example: 3febfae1e68b190c15461f4c2a3290f9af1dae63fd7d620d2bd61601869026cd
17871873
LNInvoiceResponse:
17881874
type: object
17891875
properties:
@@ -2184,6 +2270,24 @@ components:
21842270
txid:
21852271
type: string
21862272
example: 7c2c95b9c2aa0a7d140495b664de7973b76561de833f0dd84def3efa08941664
2273+
SettleHodlInvoiceRequest:
2274+
type: object
2275+
required:
2276+
- payment_hash
2277+
- payment_preimage
2278+
properties:
2279+
payment_hash:
2280+
type: string
2281+
example: b4cb2da889477082a2e47f37a07e646e60ef6f97ffa7a4d88c823efd673da94b
2282+
payment_preimage:
2283+
type: string
2284+
example: eade701c7b23b8799465f4284ad84710fc16a776fbc6483001291149122695a8
2285+
SettleHodlInvoiceResponse:
2286+
type: object
2287+
properties:
2288+
changed:
2289+
type: boolean
2290+
example: true
21872291
SignMessageRequest:
21882292
type: object
21892293
properties:

regtest.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ _is_port_bound() {
3838
_wait_for_bitcoind() {
3939
# wait for bitcoind to be up
4040
start_time=$(date +%s)
41-
until $COMPOSE logs bitcoind |grep -q 'Bound to'; do
41+
until $BITCOIN_CLI getblockcount >/dev/null 2>&1; do
4242
current_time=$(date +%s)
4343
if [ $((current_time - start_time)) -gt $TIMEOUT ]; then
4444
echo "Timeout waiting for bitcoind to start"
@@ -74,9 +74,10 @@ _start_services() {
7474
_die "port $port is already bound, services can't be started"
7575
fi
7676
done
77-
$COMPOSE up -d
77+
$COMPOSE up -d bitcoind
7878
echo && echo "preparing bitcoind wallet"
7979
_wait_for_bitcoind
80+
$COMPOSE up -d electrs proxy
8081
$BITCOIN_CLI createwallet miner >/dev/null
8182
$BITCOIN_CLI -rpcwallet=miner -generate $INITIAL_BLOCKS >/dev/null
8283
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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ pub enum APIError {
161161
#[error("Invalid payment hash: {0}")]
162162
InvalidPaymentHash(String),
163163

164+
#[error("Invalid payment preimage")]
165+
InvalidPaymentPreimage,
166+
164167
#[error("Invalid payment secret")]
165168
InvalidPaymentSecret,
166169

@@ -212,6 +215,24 @@ pub enum APIError {
212215
#[error("Invalid transport endpoints: {0}")]
213216
InvalidTransportEndpoints(String),
214217

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

@@ -257,6 +278,9 @@ pub enum APIError {
257278
#[error("Output below the dust limit")]
258279
OutputBelowDustLimit,
259280

281+
#[error("Payment hash already used")]
282+
PaymentHashAlreadyUsed,
283+
260284
#[error("Payment not found: {0}")]
261285
PaymentNotFound(String),
262286

@@ -443,6 +467,8 @@ impl IntoResponse for APIError {
443467
| APIError::InvalidOnionData(_)
444468
| APIError::InvalidPassword(_)
445469
| APIError::InvalidPaymentHash(_)
470+
| APIError::PaymentHashAlreadyUsed
471+
| APIError::InvalidPaymentPreimage
446472
| APIError::InvalidPaymentSecret
447473
| APIError::InvalidPeerInfo(_)
448474
| APIError::InvalidPrecision(_)
@@ -457,10 +483,12 @@ impl IntoResponse for APIError {
457483
| APIError::InvalidTlvType(_)
458484
| APIError::InvalidTransportEndpoint(_)
459485
| APIError::InvalidTransportEndpoints(_)
486+
| APIError::InvoiceExpired
460487
| APIError::MediaFileEmpty
461488
| APIError::MediaFileNotProvided
462489
| APIError::MissingSwapPaymentPreimage
463490
| APIError::OutputBelowDustLimit
491+
| APIError::ClaimDeadlineExceeded
464492
| APIError::UnsupportedBackupVersion { .. } => {
465493
(StatusCode::BAD_REQUEST, self.to_string(), self.name())
466494
}
@@ -485,6 +513,8 @@ impl IntoResponse for APIError {
485513
| APIError::InvalidIndexer(_)
486514
| APIError::InvalidProxyEndpoint
487515
| APIError::InvalidProxyProtocol(_)
516+
| APIError::InvoiceNotHodl
517+
| APIError::InvoiceSettlingInProgress
488518
| APIError::LockedNode
489519
| APIError::MaxFeeExceeded(_)
490520
| APIError::MinFeeNotMet(_)
@@ -506,6 +536,10 @@ impl IntoResponse for APIError {
506536
| APIError::UnsupportedTransportType => {
507537
(StatusCode::FORBIDDEN, self.to_string(), self.name())
508538
}
539+
APIError::InvoiceAlreadySettled => {
540+
(StatusCode::CONFLICT, self.to_string(), self.name())
541+
}
542+
APIError::InvoiceNotClaimable => (StatusCode::NOT_FOUND, self.to_string(), self.name()),
509543
APIError::Network(_) | APIError::NoValidTransportEndpoint => (
510544
StatusCode::SERVICE_UNAVAILABLE,
511545
self.to_string(),

0 commit comments

Comments
 (0)