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
39 changes: 29 additions & 10 deletions docs/api-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,15 @@ All RPCs are unary (single request, single response) unless noted otherwise.
### On-Chain

| RPC | Description |
|------------------|----------------------------------------------------------------------|
| ---------------- | -------------------------------------------------------------------- |
| `OnchainReceive` | Generate a new on-chain funding address |
| `OnchainSend` | Send to a Bitcoin address (with optional fee rate and send-all mode) |
| `OnchainBumpFee` | Raise the fee of an unconfirmed outbound on-chain payment using RBF |

`OnchainBumpFee` replaces a payment's transaction while preserving its payment ID and recipient
amount. Use the `payment_id` from `ListPayments`. The optional `fee_rate_sat_per_vb` sets the new
total fee rate in sat/vB; omit it to use an automatic rate. The response contains the replacement
`txid`. Confirmed, inbound, Lightning, and channel funding payments are not eligible.

### BOLT11 Payments

Expand Down Expand Up @@ -145,15 +151,28 @@ a channel just-in-time when the invoice is paid.

### Channel Management

| RPC | Description |
|-----------------------|------------------------------------------------------------------------|
| `OpenChannel` | Open a new outbound channel (with optional push amount and fee config) |
| `CloseChannel` | Cooperatively close a channel |
| `ForceCloseChannel` | Force-close a channel unilaterally |
| `SpliceIn` | Add on-chain funds to an existing channel |
| `SpliceOut` | Remove funds from a channel back on-chain |
| `UpdateChannelConfig` | Update forwarding fees and CLTV expiry delta |
| `ListChannels` | List all channels with balances and configuration |
| RPC | Description |
| ----------------------- | ---------------------------------------------------------------------- |
| `OpenChannel` | Open a new outbound channel (with optional push amount and fee config) |
| `CloseChannel` | Cooperatively close a channel |
| `ForceCloseChannel` | Force-close a channel unilaterally |
| `SpliceIn` | Add on-chain funds to an existing channel |
| `SpliceOut` | Remove funds from a channel back on-chain |
| `BumpChannelFundingFee` | Raise the fee of a pending splice transaction |
| `UpdateChannelConfig` | Update forwarding fees and CLTV expiry delta |
| `ListChannels` | List all channels with balances and configuration |

> [!NOTE]
> `BumpChannelFundingFee` supports pending splices only. It preserves the splice amount and
> destination, and LDK Node selects the fee rate. General channel-opening fee bumps and
> caller-selected splice fee rates are not supported.

Call it on the node that contributed to the splice, using the channel's `user_channel_id` and
`counterparty_node_id`. A channel with no pending splice returns an error. An empty response means
the fee bump has started; use [channel events](#event-streaming) to follow its progress.

The automatic increase can be below Bitcoin Core 29's minimum relay fee increase. Check that the
replacement transaction reaches the mempool; a successful RPC does not guarantee relay or confirmation.

### Payment History

Expand Down
107 changes: 103 additions & 4 deletions e2e-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::io::{BufRead, BufReader, Write};
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use corepc_node::Node;
use hex_conservative::DisplayHex;
Expand All @@ -20,10 +20,11 @@ use ldk_server_client::ldk_server_grpc::api::{GetNodeInfoRequest, GetNodeInfoRes
use ldk_server_client::ldk_server_grpc::events::event_envelope::Event;
use ldk_server_client::ldk_server_grpc::events::EventEnvelope;
use ldk_server_grpc::api::{
open_channel_request, GetBalancesRequest, ListChannelsRequest, OnchainReceiveRequest,
OpenChannelRequest,
open_channel_request, GetBalancesRequest, ListChannelsRequest, ListPaymentsRequest,
OnchainReceiveRequest, OpenChannelRequest,
};
use serde_json::Value;
use ldk_server_grpc::types::{payment_kind, Payment};
use serde_json::{json, Value};

const EVENT_TIMEOUT: Duration = Duration::from_secs(15);

Expand Down Expand Up @@ -51,6 +52,8 @@ impl TestBitcoind {

fn with_extra_args(extra_args: &[&str]) -> Self {
let mut conf = corepc_node::Conf::default();
// Match the pinned LDK Node splice fixtures' 0.1 sat/vB relay fee increase.
conf.args.push("-incrementalrelayfee=0.00000100");
conf.args.extend_from_slice(extra_args);

let bitcoind = match std::env::var("BITCOIND_EXE") {
Expand Down Expand Up @@ -542,6 +545,17 @@ pub async fn wait_for_event(
.expect("Timed out waiting for event")
}

/// Wait for a negotiated splice and return its funding transaction ID.
pub async fn splice_txid(events: &mut EventStream) -> String {
let event = wait_for_event(events, |e| matches!(e, Event::SpliceNegotiated(_))).await;
match event.event.unwrap() {
Event::SpliceNegotiated(splice) => {
splice.new_funding_txo.split(':').next().unwrap().to_string()
},
_ => unreachable!(),
}
}

/// Poll get_node_info until the server responds successfully.
async fn wait_for_server_ready(handle: &LdkServerHandle, timeout: Duration) -> GetNodeInfoResponse {
let start = std::time::Instant::now();
Expand Down Expand Up @@ -725,6 +739,91 @@ pub async fn mine_and_sync(
}
}

/// Wait for a transaction to enter the mempool and return its decoded details.
pub async fn wait_for_transaction(bitcoind: &TestBitcoind, txid: &str) -> Value {
tokio::time::timeout(Duration::from_secs(30), async {
loop {
let mempool: Vec<String> = bitcoind.bitcoind.client.call("getrawmempool", &[]).unwrap();
if mempool.iter().any(|id| id == txid) {
return bitcoind
.bitcoind
.client
.call("getrawtransaction", &[json!(txid), json!(true)])
.unwrap();
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
})
.await
.expect("transaction did not enter the mempool")
}

/// Wait for the on-chain wallet to complete another sync.
///
/// The pinned wallet records a replacement before its background sync sees the transaction.
pub async fn wait_for_wallet_sync(server: &LdkServerHandle) {
let after = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
tokio::time::timeout(Duration::from_secs(30), async {
loop {
let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap();
if info.latest_onchain_wallet_sync_timestamp.is_some_and(|timestamp| timestamp > after)
{
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
})
.await
.expect("wallet did not sync after the replacement");
}

/// Wait for a transaction to appear in the node's payment history.
pub async fn payment_for_tx(server: &LdkServerHandle, txid: &str) -> Payment {
tokio::time::timeout(Duration::from_secs(30), async {
loop {
let mut page_token = None;
loop {
let page = server
.client()
.list_payments(ListPaymentsRequest { page_token })
.await
.unwrap();
for payment in page.payments {
if let Some(payment_kind::Kind::Onchain(onchain)) =
payment.kind.as_ref().and_then(|kind| kind.kind.as_ref())
{
if onchain.txid == txid {
return payment;
}
}
}
page_token = page.next_page_token;
if page_token.is_none() {
break;
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
})
.await
.expect("payment was not recorded")
}

/// Check that a replacement preserves the 50,000 sat recipient output.
pub async fn assert_replacement(bitcoind: &TestBitcoind, old: &str, new: &str, address: &str) {
assert_ne!(old, new);
let tx = wait_for_transaction(bitcoind, new).await;
let recipient = tx["vout"]
.as_array()
.unwrap()
.iter()
.find(|output| output["scriptPubKey"]["address"] == address)
.unwrap();
assert_eq!(recipient["value"], json!(0.0005));
let mempool: Vec<String> = bitcoind.bitcoind.client.call("getrawmempool", &[]).unwrap();
assert!(!mempool.iter().any(|id| id == old));
}

/// Wait until the given client has at least one usable channel,
/// periodically mining blocks to trigger chain sync.
pub async fn wait_for_usable_channel(
Expand Down
Loading