Node API
The Node API provides Lightning Network channel management and payment operations.
JSON-RPC namespace: node
Endpoints
Peer Management
- Connect to Peer
- Disconnect from Peer
- Get Connected Peers
- Is Peer Connected
- Connect to Watchtower
- Get Connected Watchtowers
Zero-Conf Whitelist
Peer Blacklist
Node Configuration
Channel Operations
- Estimate Open Channel Fee
- Open Channel
- Estimate Deposit Channel Fee
- Deposit Channel
- Estimate Withdraw Channel Fee
- Withdraw Channel
- Estimate Update Channel Fee
- Update Channel
- Estimate Close Channel Fee
- Close Channel
- Estimate Force Close Channel Fee
- Force Close Channel
- Estimate Redeem Closed Channel Fee
- Redeem Closed Channel
Batch Channel Operations
- Batch Channel Operations
- Estimate Batch Channel Operations Fee
- Estimate Simulated Channel Operation Fee
- Estimate Simulated Channel Operations Fee
Payment Operations
Invoice Management
- Create Invoice
- Decode Invoice
- Estimate Pay Invoice Fee
- Pay Invoice
- Estimate Pay Empty Invoice Fee
- Pay Empty Invoice
- Resolve Hashlock Payment
- Reject Payment
- Register Preimage — moved to
preimage.SettlePreimage
Funding Allowances
- Set Funding Allowance
- Get Funding Allowance
- Increase Funding Allowance
- Decrease Funding Allowance
- Revoke Funding Allowance
Connect to Peer
Connect to a Lightning Network peer.
Method: ConnectToPeer
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network to connect on |
peer_url | string | YES | Peer connection URL |
Peer URL Format:
node_id@host:port
Response: Empty (success confirmation)
Example Request:
TypeScript
import { NodeServiceClient } from './proto/NodeServiceClientPb'
import { ConnectToPeerRequest } from './proto/node_pb'
const client = new NodeServiceClient('http://localhost:5003')
const request = new ConnectToPeerRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPeerUrl('02abc123...@lightning.example.com:9735')
await client.connectToPeer(request, {})
console.log('Connected to peer')
Go
import (
pb "github.com/hydra/hydra-go/proto"
)
client := pb.NewNodeServiceClient(conn)
req := &pb.ConnectToPeerRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
PeerUrl: "02abc123...@lightning.example.com:9735",
}
_, err := client.ConnectToPeer(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Connected to peer")
Rust
use hydra_app::node_service_client::NodeServiceClient;
use hydra_app::{ConnectToPeerRequest, Network};
let mut client = NodeServiceClient::new(channel);
let request = tonic::Request::new(ConnectToPeerRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
peer_url: "02abc123...@lightning.example.com:9735".to_string(),
});
client.connect_to_peer(request).await?;
println!("Connected to peer");
Get Connected Peers
Get list of currently connected peers.
Method: GetConnectedPeers
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network to query |
Response:
| Field | Type | Description |
|---|---|---|
node_ids | string[] | Array of connected node IDs |
Example Request:
TypeScript
const request = new GetConnectedPeersRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
const response = await client.getConnectedPeers(request, {})
const peers = response.getNodeIdsList()
console.log('Connected peers:', peers)
Go
req := &pb.GetConnectedPeersRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
}
resp, err := client.GetConnectedPeers(context.Background(), req)
if err != nil {
log.Fatal(err)
}
peers := resp.NodeIds
log.Println("Connected peers:", peers)
Rust
let request = tonic::Request::new(GetConnectedPeersRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
});
let response = client.get_connected_peers(request).await?;
let peers = response.into_inner().node_ids;
println!("Connected peers: {:?}", peers);
Disconnect from Peer
Disconnect from a connected peer.
Method: DisconnectFromPeer
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | Peer public key (hex) |
Response: Empty.
Example Request:
import { DisconnectFromPeerRequest } from './proto/node_pb'
const request = new DisconnectFromPeerRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
await client.disconnectFromPeer(request, {})
Is Peer Connected
Check whether a specific peer is currently connected.
Method: IsPeerConnected
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | Peer public key (hex) |
Response:
| Field | Type | Description |
|---|---|---|
is_connected | bool | True if currently connected |
Example Request:
import { IsPeerConnectedRequest } from './proto/node_pb'
const request = new IsPeerConnectedRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
const response = await client.isPeerConnected(request, {})
console.log('Connected:', response.getIsConnected())
Connect to Watchtower
Added 2026-08-02.
Attach this node to a watchtower on a specific network. A watchtower holds the states and revocations this node signs, so that a node which loses its local history can be handed back the coverage it gave away, and so that a channel stays defended while this node is offline.
Method: ConnectToWatchtower
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
watchtower_url | string | YES | The watchtower's address in <node_id>@<host>:<port> form — the same form ConnectToPeer takes, since a watchtower is reached over the same transport |
Response: Empty.
The watchtower session is independent of the ordinary peer session. The same remote node may be both a channel peer and a watchtower, and the two connections are tracked separately — connecting one does not connect or disconnect the other.
A watchtower cannot defend a channel whose counterparty is the watchtower. It acts on evidence against a counterparty, so there is nobody for it to act against. On mainnet the Hydranet node serves as both peer and watchtower on the same address — that coverage applies to your channels with other peers. See the Setup Guide.
You usually don't need this call. (2026-09-04) An EVM / Tron network block in
config.yamlcan declare awatchtowerslist; the node dials those itself once its initial chain sync completes and keeps them attached across restarts. UseConnectToWatchtowerto attach one at runtime, andGetConnectedWatchtowersto see what is actually attached. See the Setup Guide.
Example Request:
import { ConnectToWatchtowerRequest } from './proto/node_pb'
const request = new ConnectToWatchtowerRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setWatchtowerUrl('02abc123...@watchtower.example.com:9735')
await client.connectToWatchtower(request, {})
Get Connected Watchtowers
Added 2026-09-04.
List the watchtowers this node currently has a session with — the counterpart to ConnectToWatchtower, which until now you could call but never verify.
Method: GetConnectedWatchtowers
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
Response:
| Field | Type | Description |
|---|---|---|
node_ids | string[] | Node ids of the attached watchtowers |
Returns node ids, not the full <node_id>@<host>:<port> strings you connected with — match on the id.
Example Request:
import { GetConnectedWatchtowersRequest } from './proto/node_pb'
const request = new GetConnectedWatchtowersRequest()
request.setNetwork({ protocol: 2, id: '42161' })
const res = await client.getConnectedWatchtowers(request, {})
console.log(res.getNodeIdsList())
Empty is a meaningful answer. A node with
watchtowersconfigured attaches only after its initial chain sync completes, so an empty list early in a boot is normal — poll it, or check it once the node reports synced. An empty list on a long-running node means your channels are undefended while it is offline.
Get Zero-Conf Whitelist
Returns the list of peers permitted to use zero-confirmation channels with this node.
Method: GetZeroConfWhitelist
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
Response:
| Field | Type | Description |
|---|---|---|
node_ids | string[] | Hex-encoded public keys of whitelisted peers |
Example Request:
import { GetZeroConfWhitelistRequest } from './proto/node_pb'
const request = new GetZeroConfWhitelistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
const response = await client.getZeroConfWhitelist(request, {})
console.log('Whitelisted:', response.getNodeIdsList())
Add Peer To Zero-Conf Whitelist
Adds a peer to the zero-confirmation whitelist. Whitelisted peers can use channels before the funding transaction confirms on-chain.
Method: AddPeerToZeroConfWhitelist
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | Peer public key to whitelist |
Response: Empty.
Example Request:
import { AddPeerToZeroConfWhitelistRequest } from './proto/node_pb'
const request = new AddPeerToZeroConfWhitelistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
await client.addPeerToZeroConfWhitelist(request, {})
Remove Peer From Zero-Conf Whitelist
Removes a peer from the zero-confirmation whitelist.
Method: RemovePeerFromZeroConfWhitelist
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | Peer public key to remove |
Response: Empty.
Example Request:
import { RemovePeerFromZeroConfWhitelistRequest } from './proto/node_pb'
const request = new RemovePeerFromZeroConfWhitelistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
await client.removePeerFromZeroConfWhitelist(request, {})
Get Blacklist
Added 2026-07-20.
Returns the peer connection blacklist for a network — peers this node refuses connections with. The blacklist is also file-configurable; these RPCs control it at runtime.
This is the inverse of the zero-conf whitelist and independent of it: the whitelist grants a privilege to a peer you connect with, the blacklist refuses the connection outright.
Method: GetBlacklist
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
Response:
| Field | Type | Description |
|---|---|---|
node_ids | string[] | Hex-encoded public keys of blacklisted peers |
Example Request:
import { GetBlacklistRequest } from './proto/node_pb'
const request = new GetBlacklistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
const response = await client.getBlacklist(request, {})
console.log('Blacklisted peers:', response.getNodeIdsList())
Add Peer To Blacklist
Added 2026-07-20.
Adds a peer to the connection blacklist. Subsequent connection attempts involving that peer are refused.
Method: AddPeerToBlacklist
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | The peer's public key, hex-encoded |
Response: Empty.
Example Request:
import { AddPeerToBlacklistRequest } from './proto/node_pb'
const request = new AddPeerToBlacklistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
await client.addPeerToBlacklist(request, {})
Remove Peer From Blacklist
Added 2026-07-20.
Removes a peer from the connection blacklist, allowing connections with it again.
Method: RemovePeerFromBlacklist
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | The peer's public key, hex-encoded |
Response: Empty.
Example Request:
import { RemovePeerFromBlacklistRequest } from './proto/node_pb'
const request = new RemovePeerFromBlacklistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
await client.removePeerFromBlacklist(request, {})
Get Node Policy
Added 2026-05-21.
Read this node's policy configuration for a specific asset on a network. A "node policy" aggregates every parameter another party needs to know before negotiating a channel with this node — reserve rules, routing limits, channel timing rules, etc. Used by liquidity providers and any third party that wants to size on-chain deposits accurately, predict runtime reserves, or know the routing-fee / payment ceilings the channel will be subject to.
The response is composed of sub-messages, one per policy category. New categories are added in a backwards-compatible way, so check for the presence of each sub-message rather than treating the response as a fixed shape.
Method: GetNodePolicy
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
asset_id | string | YES | Asset whose policy you want to read — required because several policy values are denominated in the asset's smallest unit |
Response:
| Field | Type | Description |
|---|---|---|
reserve | ReservePolicy | Channel-reserve parameters. Drives the deposit sizing algebra deposit = (free + fixed) / (1 − rate) on the counterparty side. |
ReservePolicy — channel reserve policy for a single (network, asset) pair. All DecimalString amounts are in the asset's display denomination.
| Field | Type | Description |
|---|---|---|
counterparty_proportional_millionths | uint64 | Reserve ratio (in millionths) the node imposes on its counterparty when opening / accepting a channel. The runtime reserve locked on the counterparty side equals (free + fixed) × counterparty_proportional_millionths / 1_000_000. Range: [0, 1_000_000). |
max_self_proportional_millionths | uint64 | Maximum reserve ratio (in millionths) the node will accept on its own side. Used by callers as an upper-bound clamp on the rate they expect at runtime. Range: [0, 1_000_000). |
min_absolute | DecimalString | Absolute minimum reserve in the asset's display denomination. The runtime reserve is at least this value regardless of the proportional rule. |
fee_channel_reserve | DecimalString | Fixed-fee reserve (display denomination) added on top of the proportional reserve as a gas / dust buffer. Applies once per channel, regardless of how many assets the channel holds. |
Example Request:
import { GetNodePolicyRequest } from './proto/node_pb'
const request = new GetNodePolicyRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setAssetId('0x0000000000000000000000000000000000000000000000000000000000000000')
const response = await client.getNodePolicy(request, {})
const reserve = response.getReserve()
if (reserve) {
console.log('Counterparty reserve millionths:', reserve.getCounterpartyProportionalMillionths())
console.log('Min absolute:', reserve.getMinAbsolute()?.getValue())
console.log('Fee channel reserve:', reserve.getFeeChannelReserve()?.getValue())
}
Example Response:
{
"reserve": {
"counterparty_proportional_millionths": 10000,
"max_self_proportional_millionths": 50000,
"min_absolute": "0.0001",
"fee_channel_reserve": "0.00005"
}
}
Estimate Open Channel Fee
Estimate the onchain fee for opening a new channel.
Method: EstimateOpenChannelFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network to open channel on |
node_id | string | YES | Peer's node ID |
asset_amounts | map<string, DepositAmount> | YES | Map of asset_id → amount to allocate |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
DepositAmount — a oneof, exactly one set:
| Variant | Payload | Meaning |
|---|---|---|
all | {} | Deposit the entire available balance |
exact | { amount: DecimalString } | Deposit this amount |
dual_funded | { local, remote, self_deposit } | Dual-funded open with an explicit split — local / remote are the channel balances assigned to each side, self_deposit is what actually leaves your wallet |
FeeOption — also a oneof, exactly one set:
| Variant | Payload | Meaning |
|---|---|---|
low / medium / high | {} | Use the network's current fee estimate at that priority |
custom | { fee_rate: FeeRate } | Your own rate — FeeRate { max_fee_per_unit, priority_fee_per_unit }, both U256String (base units: sat/vB, wei) |
⚠️DecimalStringamounts are human-readable;U256Stringfee rates are notEvery
amounthere is aDecimalString:"0.1"is 0.1 BTC. Putting"10000000"there does not mean 0.1 BTC — it means ten million BTC.FeeRateis the opposite: it isU256Stringand is in base units. The two look identical on the wire ({value: "…"}), so check which one a field is before filling it in.
Both types above are
oneofs, so the examples nest one level deeper than a flat field would. The examples spell the variant names the way the standard generators emit them — prost names the Rust enum after the oneof itself (deposit_amount::Amount::Exact), protoc-gen-go wraps it (pb.DepositAmount_Exact_). If your generator names them differently, the shape is what matters: an amount is always a variant wrapping{ amount: DecimalString }, never a bare value.node.protoandbalance.protoare authoritative.
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated onchain fee |
Example Request:
TypeScript
const request = new EstimateOpenChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
request.setAssetAmountsMap({
'0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.1' } } }
})
request.setFeeOption({ medium: {} }) // low | medium | high | custom
const response = await client.estimateOpenChannelFee(request, {})
console.log('Estimated fee:', response.getFee(), 'BTC')
Go
req := &pb.EstimateOpenChannelFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
NodeId: "02abc123...",
AssetAmounts: map[string]*pb.Amount{
"0x0000000000000000000000000000000000000000000000000000000000000000": &pb.DepositAmount{Amount: &pb.DepositAmount_Exact_{Exact: &pb.DepositAmount_Exact{Amount: &pb.DecimalString{Value: "0.1"}}}},
},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.EstimateOpenChannelFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Estimated fee:", resp.Fee, "BTC")
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), DepositAmount {
amount: Some(deposit_amount::Amount::Exact(deposit_amount::Exact {
amount: Some(DecimalString { value: "0.1".to_string() }),
})),
});
let request = tonic::Request::new(EstimateOpenChannelFeeRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
node_id: "02abc123...".to_string(),
asset_amounts,
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.estimate_open_channel_fee(request).await?;
println!("Estimated fee: {} BTC", response.into_inner().fee);
Open Channel
Open a new Lightning channel with a peer.
Method: OpenChannel
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network to open on |
node_id | string | YES | Peer's node ID |
asset_amounts | map<string, DepositAmount> | YES | Assets to allocate |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
txid | string | Funding transaction ID |
channel_id | string | Channel identifier |
Example Request:
TypeScript
const request = new OpenChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
request.setAssetAmountsMap({
'0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.1' } } }
})
request.setFeeOption({ medium: {} })
const response = await client.openChannel(request, {})
console.log('Channel opened!')
console.log(' TX:', response.getTxid())
console.log(' Channel ID:', response.getChannelId())
Go
req := &pb.OpenChannelRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
NodeId: "02abc123...",
AssetAmounts: map[string]*pb.Amount{
"0x0000000000000000000000000000000000000000000000000000000000000000": &pb.DepositAmount{Amount: &pb.DepositAmount_Exact_{Exact: &pb.DepositAmount_Exact{Amount: &pb.DecimalString{Value: "0.1"}}}},
},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.OpenChannel(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Channel opened!")
log.Println(" TX:", resp.Txid)
log.Println(" Channel ID:", resp.ChannelId)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), DepositAmount {
amount: Some(deposit_amount::Amount::Exact(deposit_amount::Exact {
amount: Some(DecimalString { value: "0.1".to_string() }),
})),
});
let request = tonic::Request::new(OpenChannelRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
node_id: "02abc123...".to_string(),
asset_amounts,
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.open_channel(request).await?;
let inner = response.into_inner();
println!("Channel opened!");
println!(" TX: {}", inner.txid);
println!(" Channel ID: {}", inner.channel_id);
Estimate Deposit Channel Fee
Estimate fee for depositing assets into an existing channel.
Method: EstimateDepositChannelFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Channel to deposit to |
asset_amounts | map<string, DepositAmount> | YES | Assets to deposit |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated fee |
Example Request:
TypeScript
const request = new EstimateDepositChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetAmountsMap({
'0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.05' } } }
})
request.setFeeOption({ medium: {} })
const response = await client.estimateDepositChannelFee(request, {})
console.log('Deposit fee:', response.getFee())
Go
req := &pb.EstimateDepositChannelFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetAmounts: map[string]*pb.Amount{
"0x0000000000000000000000000000000000000000000000000000000000000000": &pb.DepositAmount{Amount: &pb.DepositAmount_Exact_{Exact: &pb.DepositAmount_Exact{Amount: &pb.DecimalString{Value: "0.05"}}}},
},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.EstimateDepositChannelFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Deposit fee:", resp.Fee)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), DepositAmount {
amount: Some(deposit_amount::Amount::Exact(deposit_amount::Exact {
amount: Some(DecimalString { value: "0.05".to_string() }),
})),
});
let request = tonic::Request::new(EstimateDepositChannelFeeRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_amounts,
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.estimate_deposit_channel_fee(request).await?;
println!("Deposit fee: {}", response.into_inner().fee);
Deposit Channel
Deposit additional assets into an existing channel.
Method: DepositChannel
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Channel to deposit to |
asset_amounts | map<string, DepositAmount> | YES | Assets to deposit |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
txid | string | Deposit transaction ID |
Example Request:
TypeScript
const request = new DepositChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetAmountsMap({
'0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.05' } } }
})
request.setFeeOption({ medium: {} })
const response = await client.depositChannel(request, {})
console.log('Deposit TX:', response.getTxid())
Go
req := &pb.DepositChannelRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetAmounts: map[string]*pb.Amount{
"0x0000000000000000000000000000000000000000000000000000000000000000": &pb.DepositAmount{Amount: &pb.DepositAmount_Exact_{Exact: &pb.DepositAmount_Exact{Amount: &pb.DecimalString{Value: "0.05"}}}},
},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.DepositChannel(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Deposit TX:", resp.Txid)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), DepositAmount {
amount: Some(deposit_amount::Amount::Exact(deposit_amount::Exact {
amount: Some(DecimalString { value: "0.05".to_string() }),
})),
});
let request = tonic::Request::new(DepositChannelRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_amounts,
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.deposit_channel(request).await?;
println!("Deposit TX: {}", response.into_inner().txid);
Estimate Withdraw Channel Fee
Estimate fee for withdrawing assets from a channel.
Method: EstimateWithdrawChannelFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Channel to withdraw from |
asset_amounts | map<string, WithdrawAmount> | YES | Amounts to withdraw |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
WithdrawAmount Object:
| Field | Type | Description |
|---|---|---|
self_withdrawal | Amount | Amount you withdraw |
counterparty_withdrawal | Amount | Amount the counterparty withdraws |
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated fee |
Example Request:
TypeScript
const request = new EstimateWithdrawChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetAmountsMap({
'0x0000000000000000000000000000000000000000000000000000000000000000': {
selfWithdrawal: { exact: { amount: { value: '0.03' } } },
counterpartyWithdrawal: { exact: { amount: { value: '0.02' } } }
}
})
request.setFeeOption({ medium: {} })
const response = await client.estimateWithdrawChannelFee(request, {})
console.log('Withdrawal fee:', response.getFee())
Go
req := &pb.EstimateWithdrawChannelFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetAmounts: map[string]*pb.WithdrawAmount{
"0x0000000000000000000000000000000000000000000000000000000000000000": {
SelfWithdrawal: &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.03"}}}},
CounterpartyWithdrawal: &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.02"}}}},
},
},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.EstimateWithdrawChannelFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Withdrawal fee:", resp.Fee)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), WithdrawAmount {
self_withdrawal: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.03".to_string() }) })) }),
counterparty_withdrawal: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.02".to_string() }) })) }),
});
let request = tonic::Request::new(EstimateWithdrawChannelFeeRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_amounts,
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.estimate_withdraw_channel_fee(request).await?;
println!("Withdrawal fee: {}", response.into_inner().fee);
Withdraw Channel
Withdraw assets from an existing channel.
Method: WithdrawChannel
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Channel to withdraw from |
asset_amounts | map<string, WithdrawAmount> | YES | Amounts to withdraw |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
txid | string | Withdrawal transaction ID |
Example Request:
TypeScript
const request = new WithdrawChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetAmountsMap({
'0x0000000000000000000000000000000000000000000000000000000000000000': {
selfWithdrawal: { exact: { amount: { value: '0.03' } } },
counterpartyWithdrawal: { exact: { amount: { value: '0.02' } } }
}
})
request.setFeeOption({ medium: {} })
const response = await client.withdrawChannel(request, {})
console.log('Withdrawal TX:', response.getTxid())
Go
req := &pb.WithdrawChannelRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetAmounts: map[string]*pb.WithdrawAmount{
"0x0000000000000000000000000000000000000000000000000000000000000000": {
SelfWithdrawal: &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.03"}}}},
CounterpartyWithdrawal: &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.02"}}}},
},
},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.WithdrawChannel(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Withdrawal TX:", resp.Txid)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), WithdrawAmount {
self_withdrawal: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.03".to_string() }) })) }),
counterparty_withdrawal: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.02".to_string() }) })) }),
});
let request = tonic::Request::new(WithdrawChannelRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_amounts,
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.withdraw_channel(request).await?;
println!("Withdrawal TX: {}", response.into_inner().txid);
Estimate Update Channel Fee
Estimates the on-chain fee for a generic channel update before executing it.
Method: EstimateUpdateChannelFee
Parameters: same as Update Channel — network, channel_id, asset_updates, fee_option.
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated fee in the network's native asset |
Update Channel
Added 2026-09-11.
The general form of a channel update. Each side's wallet may deposit or withdraw independently, and part of the resulting balance may be credited across in the state co-signed alongside the transaction.
DepositChannel and WithdrawChannel are unchanged — they are the two shapes of this that are common enough to have their own call. UpdateChannel is how you reach every other combination: a deposit on one side against a withdrawal on the other, a withdrawal that hands part of itself to the peer, a pure credit with no wallet movement at all.
Method: UpdateChannel
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Channel to update |
asset_updates | map<string, ChannelUpdateAmount> | YES | asset_id → what that asset's update asks for |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
txid | string | The update transaction ID |
ChannelUpdateAmount
Three parts, each independently optional:
| Field | Type | Description |
|---|---|---|
self_contribution | ChannelContribution | What your wallet contributes |
peer_contribution | ChannelContribution | What the counterparty's wallet contributes |
credit | BalanceCredit | What crosses between the two sides once both have settled |
ChannelContribution — a oneof over the direction that side's wallet moves:
| Arm | Payload | Meaning |
|---|---|---|
deposit | Amount | Out of the wallet, into the channel |
withdraw | Amount | Out of the channel, into the wallet |
BalanceCredit — a oneof over which way balance crosses:
| Arm | Payload | Meaning |
|---|---|---|
to_peer | DecimalString | Credit this much of your balance to the counterparty |
to_self | DecimalString | Credit this much of theirs to you |
Unset is not zero — it is cheaperLeaving a
contributionor acreditunset means that side's wallet does not move and nothing crosses. That is not the same as naming"0": an explicit zero is an amount the transaction still has to carry, and it costs more gas. Name only the part you care about.
A credit touches no wallet and reaches no chain
creditis settled in the co-signed state that follows the on-chain update, not in the transaction itself. It is how value moves between the two sides without either wallet paying for it — and it is what lets an exit carve its own fee out of what it withdraws.Your side must have granted the peer room for it: a credit
to_peeris bounded byallowed_paymenton that peer's funding allowance.
The two contributions must not cancelThe chain refuses an update that leaves the channel holding exactly what it already held. If your deposit on one side equals the peer's withdrawal on the other, you have described a payment, not an update — send it as one.
All resolves differently per direction
Amount.all is relative to whatever pot that side is drawing from:
withdraw: { all: {} }— that side's channel balance.deposit: { all: {} }— the local wallet. This makes it invalid onpeer_contribution, where the peer's wallet is not visible to your node; it is rejected.withdraw: { all: {} }on a side that also pays acredit— that side's withdrawable balance minus the credit. The on-chain movement settles first and the credit comes out of what is left, so the two never over-draw each other.
Example Request:
import { UpdateChannelRequest } from './proto/node_pb'
const BTC = '0x0000000000000000000000000000000000000000000000000000000000000000'
// Withdraw everything, handing 0.0001 to the peer out of what comes back —
// the shape a service-broadcast exit uses to pay its own fee.
const request = new UpdateChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetUpdatesMap({
[BTC]: {
selfContribution: { withdraw: { all: {} } },
// peerContribution left unset — their wallet does not move
credit: { toPeer: { value: '0.0001' } },
},
})
request.setFeeOption({ medium: {} })
console.log('update TX:', (await client.updateChannel(request, {})).getTxid())
Example Response:
{ "txid": "abc123def456..." }
The same operation is available atomically alongside others —
BatchChannelOperation.updatecarries aBatchUpdateChannelwith the samechannel_id+asset_updatespair.
Prefer
DepositChannel/WithdrawChannelwhen they say what you mean. They are the same machinery with a narrower request, and a reader of your code can tell at a glance which way funds are going.
Estimate Close Channel Fee
Estimate fee for cooperatively closing a channel.
Method: EstimateCloseChannelFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Channel to close |
asset_ids | string[] | YES | Assets to settle |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated fee |
Example Request:
TypeScript
const request = new EstimateCloseChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })
const response = await client.estimateCloseChannelFee(request, {})
console.log('Close fee:', response.getFee())
Go
req := &pb.EstimateCloseChannelFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetIds: []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.EstimateCloseChannelFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Close fee:", resp.Fee)
Rust
let request = tonic::Request::new(EstimateCloseChannelFeeRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.estimate_close_channel_fee(request).await?;
println!("Close fee: {}", response.into_inner().fee);
Close Channel
Cooperatively close a channel with your peer.
Method: CloseChannel
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Channel to close |
asset_ids | string[] | YES | Assets to settle |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
txid | string | Closing transaction ID |
Example Request:
TypeScript
const request = new CloseChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })
const response = await client.closeChannel(request, {})
console.log('Channel closed, TX:', response.getTxid())
Go
req := &pb.CloseChannelRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetIds: []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.CloseChannel(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Channel closed, TX:", resp.Txid)
Rust
let request = tonic::Request::new(CloseChannelRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.close_channel(request).await?;
println!("Channel closed, TX: {}", response.into_inner().txid);
Estimate Force Close Channel Fee
Estimate fee for force-closing an unresponsive channel.
Method: EstimateForceCloseChannelFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Channel to force close |
asset_ids | string[] | YES | Assets to settle |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated fee |
Example Request:
TypeScript
const request = new EstimateForceCloseChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })
const response = await client.estimateForceCloseChannelFee(request, {})
console.log('Force close fee:', response.getFee())
Go
req := &pb.EstimateForceCloseChannelFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetIds: []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.EstimateForceCloseChannelFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Force close fee:", resp.Fee)
Rust
let request = tonic::Request::new(EstimateForceCloseChannelFeeRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.estimate_force_close_channel_fee(request).await?;
println!("Force close fee: {}", response.into_inner().fee);
Force Close Channel
Force close a channel without peer cooperation.
Method: ForceCloseChannel
Important Notes:
- Use only when peer is unresponsive
- Assets locked until dispute period expires
- May require additional RedeemClosedChannel transaction
- Higher fees than cooperative close
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Channel to force close |
asset_ids | string[] | YES | Assets to settle |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
txid | string | Force close transaction ID |
Example Request:
TypeScript
const request = new ForceCloseChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })
const response = await client.forceCloseChannel(request, {})
console.log('Force close initiated, TX:', response.getTxid())
console.log('Assets will be spendable after dispute period')
Go
req := &pb.ForceCloseChannelRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetIds: []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.ForceCloseChannel(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Force close initiated, TX:", resp.Txid)
log.Println("Assets will be spendable after dispute period")
Rust
let request = tonic::Request::new(ForceCloseChannelRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.force_close_channel(request).await?;
println!("Force close initiated, TX: {}", response.into_inner().txid);
println!("Assets will be spendable after dispute period");
Estimate Redeem Closed Channel Fee
Estimate fee for redeeming a force-closed channel.
Method: EstimateRedeemClosedChannelFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Closed channel |
asset_ids | string[] | YES | Assets to redeem |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated fee |
Example Request:
TypeScript
const request = new EstimateRedeemClosedChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })
const response = await client.estimateRedeemClosedChannelFee(request, {})
console.log('Redemption fee:', response.getFee())
Go
req := &pb.EstimateRedeemClosedChannelFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetIds: []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.EstimateRedeemClosedChannelFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Redemption fee:", resp.Fee)
Rust
let request = tonic::Request::new(EstimateRedeemClosedChannelFeeRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.estimate_redeem_closed_channel_fee(request).await?;
println!("Redemption fee: {}", response.into_inner().fee);
Redeem Closed Channel
Redeem assets from a force-closed channel after dispute period.
Method: RedeemClosedChannel
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Closed channel |
asset_ids | string[] | YES | Assets to redeem |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
txid | string | Redemption transaction ID |
Example Request:
TypeScript
const request = new RedeemClosedChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })
const response = await client.redeemClosedChannel(request, {})
console.log('Assets redeemed, TX:', response.getTxid())
Go
req := &pb.RedeemClosedChannelRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetIds: []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
resp, err := client.RedeemClosedChannel(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Assets redeemed, TX:", resp.Txid)
Rust
let request = tonic::Request::new(RedeemClosedChannelRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let response = client.redeem_closed_channel(request).await?;
println!("Assets redeemed, TX: {}", response.into_inner().txid);
Batch Channel Operations
Executes a list of channel operations atomically in a single transaction — all of them land, or none do.
Method: BatchChannelOperations
BatchChannelOperation is a oneof over seven arms:
| Arm | Payload | Equivalent single call |
|---|---|---|
open | BatchOpenChannel | OpenChannel |
deposit | BatchDepositChannel | DepositChannel |
withdraw | BatchWithdrawChannel | WithdrawChannel |
close | BatchCloseChannel | CloseChannel |
force_close | BatchForceCloseChannel | ForceCloseChannel |
redeem | BatchRedeemChannel | RedeemClosedChannel |
update | BatchUpdateChannel — { channel_id, asset_updates } | (2026-09-11) UpdateChannel |
Each Batch* payload is its single call's request minus network and fee_option, which the batch supplies once for all of them.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
operations | BatchChannelOperation[] | YES | Operations to execute atomically |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
txid | string | Single transaction ID for the entire batch |
opened_channels | BatchOpenedChannel[] | Channels created by Open operations (carries the input index of the originating operation) |
Example Request:
import { BatchChannelOperationsRequest } from './proto/node_pb'
const request = new BatchChannelOperationsRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setOperationsList([/* BatchChannelOperation entries */])
request.setFeeOption({ medium: {} })
const response = await client.batchChannelOperations(request, {})
console.log('Batch TX:', response.getTxid())
Estimate Batch Channel Operations Fee
Estimates fees for a batch operation before executing it.
Method: EstimateBatchChannelOperationsFee
Parameters: Same as Batch Channel Operations — network, operations, fee_option.
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | The estimated total on-chain fee for the whole batch, in the network's native asset |
The estimate returns only the fee — no
txid, noopened_channels. Those exist onBatchChannelOperations, which actually broadcasts. An earlier revision of this page repeated that response table here.
Example:
const response = await client.estimateBatchChannelOperationsFee(request, {})
console.log('Batch fee:', response.getFee()?.getValue())
Estimate Simulated Channel Operation Fee
Estimates the fee for a single hypothetical operation against a simulated channel snapshot — useful for UI fee previews where the channel doesn't yet exist.
Method: EstimateSimulatedChannelOperationFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
channel | SimulatedChannel | YES | Hypothetical channel snapshot |
operation | SimulatedChannelOperation enum | YES | Which operation to price |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated fee, in the network's native currency |
SimulatedChannel
{ assets: SimulatedAssetChannel[] } — one entry per asset the hypothetical channel would hold:
| Field | Type | Description |
|---|---|---|
asset_id | string | The asset |
onchain_self_balance | DecimalString | Your currently committed on-chain balance |
onchain_counterparty_balance | DecimalString | The counterparty's currently committed on-chain balance |
latest_self_balance | DecimalString | Your latest off-chain balance |
latest_counterparty_balance | DecimalString | The counterparty's latest off-chain balance |
num_htlcs | uint64 | Pending HTLCs — they enlarge the commitment transaction, so they move the fee |
SimulatedChannelOperation
An enum, not a message:
| Value | Constant |
|---|---|
0 | SIMULATED_CHANNEL_OPERATION_UNSPECIFIED — invalid default |
1 | SIMULATED_CHANNEL_OPERATION_OPEN |
2 | SIMULATED_CHANNEL_OPERATION_DEPOSIT |
3 | SIMULATED_CHANNEL_OPERATION_WITHDRAW |
4 | SIMULATED_CHANNEL_OPERATION_COOPERATIVE_CLOSE |
5 | SIMULATED_CHANNEL_OPERATION_FORCE_CLOSE |
6 | SIMULATED_CHANNEL_OPERATION_CONFIRM_SETTLEMENT |
SimulatedChannelOpis a different type — the{ channel, operation }pair taken by the plural call, which prices several snapshots at once. The singular call takes the channel and the enum as separate fields.
Example:
import { EstimateSimulatedChannelOperationFeeRequest } from './proto/node_pb'
const request = new EstimateSimulatedChannelOperationFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannel({ assets: [{
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
onchainSelfBalance: { value: '0.1' },
onchainCounterpartyBalance: { value: '0' },
latestSelfBalance: { value: '0.1' },
latestCounterpartyBalance: { value: '0' },
numHtlcs: 0,
}] })
request.setOperation(2) // SIMULATED_CHANNEL_OPERATION_DEPOSIT
request.setFeeOption({ medium: {} })
const response = await client.estimateSimulatedChannelOperationFee(request, {})
console.log('Estimate:', response.getFee()?.getValue())
Estimate Simulated Channel Operations Fee
Estimates the total fee for a list of hypothetical operations across simulated channels — the multi-operation counterpart to the previous RPC.
Method: EstimateSimulatedChannelOperationsFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
operations | SimulatedChannelOp[] | YES | Simulated operations to estimate |
fee_option | FeeOption | YES | Fee priority, or a custom rate |
SimulatedChannelOp pairs a snapshot with the operation to price against it:
| Field | Type | Description |
|---|---|---|
channel | SimulatedChannel | The hypothetical channel |
operation | SimulatedChannelOperation enum | What to price |
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated total fee for all of them, in the network's native asset |
This is not the sum of the singular estimates. Operations batched into one transaction share its overhead, which is the reason to ask for them together.
Example:
import { EstimateSimulatedChannelOperationsFeeRequest } from './proto/node_pb'
const request = new EstimateSimulatedChannelOperationsFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setOperationsList([/* SimulatedChannelOp entries */])
request.setFeeOption({ medium: {} })
console.log('Total:', (await client.estimateSimulatedChannelOperationsFee(request, {})).getFee()?.getValue())
Send Channel Payment
Send a direct payment through a specific channel.
Method: SendChannelPayment
Use Cases:
- Direct peer payments
- Testing channel functionality
- Hashlock payments (HTLC)
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
channel_id | string | YES | Channel to use |
asset_amounts | map<string, Amount> | YES | Assets to send |
hashlock | Hashlock | NO | Optional hashlock for HTLC |
cltv_buffer_secs | uint64 | NO | Renamed from expiry_timeout_secs (2026-05-10). Optional HTLC effective lifetime in seconds (BOLT-11 tag c semantic). KeySend has no separate invoice-validity knob — this buffer alone bounds the HTLC. |
Hashlock — a oneof, not a struct with a hash field. Which arm you set decides who can settle the payment:
| Arm | Payload | Meaning |
|---|---|---|
known | { preimage: string } | You hold the preimage, hex-encoded. The recipient can claim as soon as the HTLC arrives |
unknown | { hash: string } | You hold only the hash, hex-encoded. The payment parks as pending_preimage until someone supplies the preimage — see ResolveHashlockPayment |
There is no
payment_hashfield onHashlock. An earlier revision of this page showed one; the proto has always carried the two-arm oneof. Sending{ hash }where you meant{ preimage }produces a payment nobody can settle.
Response:
| Field | Type | Description |
|---|---|---|
payment_id | string | Payment identifier |
Example Request:
TypeScript
const request = new SendChannelPaymentRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetAmountsMap({
'0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.001' } } }
})
// Optional: set hashlock for HTLC
// request.setHashlock({ paymentHash: 'abc123...' })
request.setCltvBufferSecs(3600) // HTLC lifetime, seconds
const response = await client.sendChannelPayment(request, {})
console.log('Payment sent:', response.getPaymentId())
Go
req := &pb.SendChannelPaymentRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
ChannelId: "ch_abc123",
AssetAmounts: map[string]*pb.Amount{
"0x0000000000000000000000000000000000000000000000000000000000000000": &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.001"}}}},
},
// Optional: set hashlock for HTLC
// Hashlock: &pb.Hashlock{PaymentHash: "abc123..."},
CltvBufferSecs: 3600, // HTLC lifetime, seconds
}
resp, err := client.SendChannelPayment(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Payment sent:", resp.PaymentId)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), Amount {
amount: Some(amount::Amount::Exact(amount::Exact {
amount: Some(DecimalString { value: "0.001".to_string() }),
})),
});
let request = tonic::Request::new(SendChannelPaymentRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
channel_id: "ch_abc123".to_string(),
asset_amounts,
// Optional: set hashlock for HTLC
// hashlock: Some(Hashlock {
// payment_hash: "abc123...".to_string(),
// }),
cltv_buffer_secs: Some(3600), // HTLC lifetime, seconds
});
let response = client.send_channel_payment(request).await?;
println!("Payment sent: {}", response.into_inner().payment_id);
Estimate Send Payment Fee
Estimate fee for a routed Lightning payment.
Method: EstimateSendPaymentFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
recipient_node_id | string | YES | Recipient's node ID |
asset_amounts | map<string, Amount> | YES | Assets to send |
hashlock | Hashlock | NO | Optional hashlock |
cltv_buffer_secs | uint64 | NO | Renamed from expiry_timeout_secs (2026-05-10). HTLC effective lifetime, seconds (BOLT-11 c). |
max_total_cltv_secs | uint64 | NO | Added 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass the invoice's cltv_buffer_secs; for permissive multi-hop routing omit. |
Response:
| Field | Type | Description |
|---|---|---|
fees | map<string, DecimalString> | Fees per asset |
Example Request:
TypeScript
const request = new EstimateSendPaymentFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setRecipientNodeId('03def456...')
request.setAssetAmountsMap({
'0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.005' } } }
})
const response = await client.estimateSendPaymentFee(request, {})
const fees = response.getFeesMap()
console.log('Routing fee:', fees.get('0x0000000000000000000000000000000000000000000000000000000000000000'), 'BTC')
Go
req := &pb.EstimateSendPaymentFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
RecipientNodeId: "03def456...",
AssetAmounts: map[string]*pb.Amount{
"0x0000000000000000000000000000000000000000000000000000000000000000": &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.005"}}}},
},
}
resp, err := client.EstimateSendPaymentFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
fees := resp.Fees
log.Println("Routing fee:", fees["0x0000000000000000000000000000000000000000000000000000000000000000"], "BTC")
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), Amount {
amount: Some(amount::Amount::Exact(amount::Exact {
amount: Some(DecimalString { value: "0.005".to_string() }),
})),
});
let request = tonic::Request::new(EstimateSendPaymentFeeRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
recipient_node_id: "03def456...".to_string(),
asset_amounts,
hashlock: None,
cltv_buffer_secs: None,
});
let response = client.estimate_send_payment_fee(request).await?;
let fees = response.into_inner().fees;
println!("Routing fee: {} BTC", fees.get("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap_or(&"0".to_string()));
Send Payment
Send a routed Lightning payment through the network.
Method: SendPayment
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
recipient_node_id | string | YES | Recipient's node ID |
asset_amounts | map<string, Amount> | YES | Assets to send |
hashlock | Hashlock | NO | Optional hashlock |
cltv_buffer_secs | uint64 | NO | Renamed from expiry_timeout_secs (2026-05-10). HTLC effective lifetime, seconds (BOLT-11 c). |
max_total_cltv_secs | uint64 | NO | Added 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass the invoice's cltv_buffer_secs; for permissive multi-hop routing omit. |
Response:
| Field | Type | Description |
|---|---|---|
payment_id | string | Payment identifier |
Example Request:
TypeScript
const request = new SendPaymentRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setRecipientNodeId('03def456...')
request.setAssetAmountsMap({
'0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.005' } } }
})
request.setCltvBufferSecs(3600)
const response = await client.sendPayment(request, {})
console.log('Payment routed:', response.getPaymentId())
Go
req := &pb.SendPaymentRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
RecipientNodeId: "03def456...",
AssetAmounts: map[string]*pb.Amount{
"0x0000000000000000000000000000000000000000000000000000000000000000": &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.005"}}}},
},
CltvBufferSecs: 3600,
}
resp, err := client.SendPayment(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Payment routed:", resp.PaymentId)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), Amount {
amount: Some(amount::Amount::Exact(amount::Exact {
amount: Some(DecimalString { value: "0.005".to_string() }),
})),
});
let request = tonic::Request::new(SendPaymentRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
recipient_node_id: "03def456...".to_string(),
asset_amounts,
hashlock: None,
cltv_buffer_secs: Some(3600),
});
let response = client.send_payment(request).await?;
println!("Payment routed: {}", response.into_inner().payment_id);
Create Invoice
Create a Lightning invoice for receiving payment.
Method: CreateInvoice
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
asset_id | string | YES | Asset to receive |
amount | DecimalString | NO | Amount (omit for zero-amount invoice) |
hashlock | Hashlock | NO | Optional hashlock |
expiry_timeout_secs | uint64 | NO | Invoice validity window in seconds (BOLT-11 x). Unchanged — only the payment-send RPCs renamed this field. |
cltv_buffer_secs | uint64 | NO | Added 2026-05-10. Extra HTLC lifetime past invoice expiry the receiver requires for safe settlement (BOLT-11 c). |
Response:
| Field | Type | Description |
|---|---|---|
invoice | Invoice | Invoice details |
Invoice Object:
| Field | Type | Description |
|---|---|---|
network | Network | The network this invoice belongs to |
payment_request | string | Encoded payment request string — the string you share with the payer |
payment_hash | string | Hex-encoded payment hash |
payment_secret | string | Payment secret |
amount | U256String? | Invoice amount, in base units (sats / wei) — not a DecimalString. Absent on an empty invoice |
asset_id | string | Asset identifier |
recipient | string | Recipient node id |
expiry_timestamp | Timestamp? | Invoice validity expiry. Absent when the invoice does not expire |
min_final_cltv_expiry_secs | uint64 | Added 2026-05-10. Minimum CLTV buffer (seconds) the receiver requires for the incoming HTLC. The HTLC's effective deadline is expiry_timestamp + min_final_cltv_expiry_secs. |
signature / signable_hash | bytes | Invoice signature and the hash it covers |
Invoice.amountisU256String, notDecimalString— one of the few amount fields in the API that really is in base units. See the note under Estimate Open Channel Fee.
Example Request:
TypeScript
const request = new CreateInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setAssetId('0x0000000000000000000000000000000000000000000000000000000000000000')
request.setAmount({ value: '0.001' }) // DecimalString — 0.001 BTC, not satoshis
request.setExpiryTimeoutSecs(3600)
const response = await client.createInvoice(request, {})
const invoice = response.getInvoice()
console.log('Payment request:', invoice.getPaymentRequest())
console.log('Share this with the payer')
Go
req := &pb.CreateInvoiceRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
AssetId: "0x0000000000000000000000000000000000000000000000000000000000000000",
Amount: "1000000",
ExpiryTimeoutSecs: 3600,
}
resp, err := client.CreateInvoice(context.Background(), req)
if err != nil {
log.Fatal(err)
}
invoice := resp.Invoice
log.Println("Payment request:", invoice.PaymentRequest)
log.Println("Share this with the payer")
Rust
let request = tonic::Request::new(CreateInvoiceRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
asset_id: "0x0000000000000000000000000000000000000000000000000000000000000000".to_string(),
amount: "1000000".to_string(),
hashlock: None,
expiry_timeout_secs: 3600,
});
let response = client.create_invoice(request).await?;
let invoice = response.into_inner().invoice.unwrap();
println!("Payment request: {}", invoice.payment_request);
println!("Share this with the payer");
Example (Zero-amount invoice):
TypeScript
const request = new CreateInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setAssetId('0x0000000000000000000000000000000000000000000000000000000000000000')
// No amount set - payer decides
const response = await client.createInvoice(request, {})
const invoice = response.getInvoice()
console.log('Zero-amount invoice:', invoice.getPaymentRequest())
Go
req := &pb.CreateInvoiceRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
AssetId: "0x0000000000000000000000000000000000000000000000000000000000000000",
// No amount set - payer decides
}
resp, err := client.CreateInvoice(context.Background(), req)
if err != nil {
log.Fatal(err)
}
invoice := resp.Invoice
log.Println("Zero-amount invoice:", invoice.PaymentRequest)
Rust
let request = tonic::Request::new(CreateInvoiceRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
asset_id: "0x0000000000000000000000000000000000000000000000000000000000000000".to_string(),
amount: String::new(), // No amount set - payer decides
hashlock: None,
expiry_timeout_secs: 0,
});
let response = client.create_invoice(request).await?;
let invoice = response.into_inner().invoice.unwrap();
println!("Zero-amount invoice: {}", invoice.payment_request);
Decode Invoice
Decode a Lightning invoice to view its details.
Method: DecodeInvoice
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
payment_request | string | YES | Encoded payment request |
Response:
| Field | Type | Description |
|---|---|---|
invoice | Invoice | Decoded invoice details |
Example Request:
TypeScript
const request = new DecodeInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentRequest('lnbc10m1...')
const response = await client.decodeInvoice(request, {})
const invoice = response.getInvoice()
console.log('Amount:', invoice.getAmount())
console.log('Asset:', invoice.getAssetId())
console.log('Expires:', invoice.getExpiry())
Go
req := &pb.DecodeInvoiceRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
PaymentRequest: "lnbc10m1...",
}
resp, err := client.DecodeInvoice(context.Background(), req)
if err != nil {
log.Fatal(err)
}
invoice := resp.Invoice
log.Println("Amount:", invoice.Amount)
log.Println("Asset:", invoice.AssetId)
log.Println("Expires:", invoice.Expiry)
Rust
let request = tonic::Request::new(DecodeInvoiceRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
payment_request: "lnbc10m1...".to_string(),
});
let response = client.decode_invoice(request).await?;
let invoice = response.into_inner().invoice.unwrap();
println!("Amount: {}", invoice.amount);
println!("Asset: {}", invoice.asset_id);
println!("Expires: {:?}", invoice.expiry);
Estimate Pay Invoice Fee
Estimate routing fee for paying an invoice.
Method: EstimatePayInvoiceFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
payment_request | string | YES | Invoice to pay |
max_total_cltv_secs | uint64 | NO | Added 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass a value matching the invoice's cltv_buffer_secs; for permissive multi-hop routing omit. |
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated routing fee |
Example Request:
TypeScript
const request = new EstimatePayInvoiceFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentRequest('lnbc10m1...')
const response = await client.estimatePayInvoiceFee(request, {})
console.log('Routing fee:', response.getFee(), 'BTC')
Go
req := &pb.EstimatePayInvoiceFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
PaymentRequest: "lnbc10m1...",
}
resp, err := client.EstimatePayInvoiceFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Routing fee:", resp.Fee, "BTC")
Rust
let request = tonic::Request::new(EstimatePayInvoiceFeeRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
payment_request: "lnbc10m1...".to_string(),
});
let response = client.estimate_pay_invoice_fee(request).await?;
println!("Routing fee: {} BTC", response.into_inner().fee);
Pay Invoice
Pay a Lightning invoice.
Method: PayInvoice
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
payment_request | string | YES | Invoice to pay |
max_total_cltv_secs | uint64 | NO | Added 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass a value matching the invoice's cltv_buffer_secs; for permissive multi-hop routing omit. |
Response:
| Field | Type | Description |
|---|---|---|
payment_id | string | Payment identifier |
Example Request:
TypeScript
const request = new PayInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentRequest('lnbc10m1...')
const response = await client.payInvoice(request, {})
console.log('Payment sent:', response.getPaymentId())
Go
req := &pb.PayInvoiceRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
PaymentRequest: "lnbc10m1...",
}
resp, err := client.PayInvoice(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Payment sent:", resp.PaymentId)
Rust
let request = tonic::Request::new(PayInvoiceRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
payment_request: "lnbc10m1...".to_string(),
});
let response = client.pay_invoice(request).await?;
println!("Payment sent: {}", response.into_inner().payment_id);
Estimate Pay Empty Invoice Fee
Estimate fee for paying a zero-amount invoice with a specific amount.
Method: EstimatePayEmptyInvoiceFee
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
payment_request | string | YES | Zero-amount invoice |
amount | Amount | YES | Amount to pay |
max_total_cltv_secs | uint64 | NO | Added 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass a value matching the invoice's cltv_buffer_secs; for permissive multi-hop routing omit. |
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated routing fee |
Example Request:
TypeScript
const request = new EstimatePayEmptyInvoiceFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentRequest('lnbc1...')
request.setAmount({ exact: { amount: { value: '0.02' } } })
const response = await client.estimatePayEmptyInvoiceFee(request, {})
console.log('Fee:', response.getFee())
Go
req := &pb.EstimatePayEmptyInvoiceFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
PaymentRequest: "lnbc1...",
Amount: &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.02"}}}},
}
resp, err := client.EstimatePayEmptyInvoiceFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Fee:", resp.Fee)
Rust
let request = tonic::Request::new(EstimatePayEmptyInvoiceFeeRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
payment_request: "lnbc1...".to_string(),
amount: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.02".to_string() }) })) }),
});
let response = client.estimate_pay_empty_invoice_fee(request).await?;
println!("Fee: {}", response.into_inner().fee);
Pay Empty Invoice
Pay a zero-amount invoice with a specific amount.
Method: PayEmptyInvoice
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
payment_request | string | YES | Zero-amount invoice |
amount | Amount | YES | Amount to pay |
max_total_cltv_secs | uint64 | NO | Added 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass a value matching the invoice's cltv_buffer_secs; for permissive multi-hop routing omit. |
Response:
| Field | Type | Description |
|---|---|---|
payment_id | string | Payment identifier |
Example Request:
TypeScript
const request = new PayEmptyInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentRequest('lnbc1...')
request.setAmount({ exact: { amount: { value: '0.02' } } })
const response = await client.payEmptyInvoice(request, {})
console.log('Payment sent:', response.getPaymentId())
Go
req := &pb.PayEmptyInvoiceRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
PaymentRequest: "lnbc1...",
Amount: &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.02"}}}},
}
resp, err := client.PayEmptyInvoice(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Payment sent:", resp.PaymentId)
Rust
let request = tonic::Request::new(PayEmptyInvoiceRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
payment_request: "lnbc1...".to_string(),
amount: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.02".to_string() }) })) }),
});
let response = client.pay_empty_invoice(request).await?;
println!("Payment sent: {}", response.into_inner().payment_id);
Resolve Hashlock Payment
Claim a hashlock payment by revealing the preimage.
Method: ResolveHashlockPayment
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
payment_id | string | YES | Payment to resolve |
payment_preimage | string | YES | Preimage (32 bytes hex) |
Response: Empty (success confirmation)
Example Request:
TypeScript
const request = new ResolveHashlockPaymentRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentId('payment_abc123')
request.setPaymentPreimage('0123456789abcdef...')
await client.resolveHashlockPayment(request, {})
console.log('Payment claimed')
Go
req := &pb.ResolveHashlockPaymentRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
PaymentId: "payment_abc123",
PaymentPreimage: "0123456789abcdef...",
}
_, err := client.ResolveHashlockPayment(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Payment claimed")
Rust
let request = tonic::Request::new(ResolveHashlockPaymentRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
payment_id: "payment_abc123".to_string(),
payment_preimage: "0123456789abcdef...".to_string(),
});
client.resolve_hashlock_payment(request).await?;
println!("Payment claimed");
Reject Payment
Reject an incoming payment.
Method: RejectPayment
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network |
payment_id | string | YES | Payment to reject |
Response: Empty (success confirmation)
Example Request:
TypeScript
const request = new RejectPaymentRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentId('payment_abc123')
await client.rejectPayment(request, {})
console.log('Payment rejected')
Go
req := &pb.RejectPaymentRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
PaymentId: "payment_abc123",
}
_, err := client.RejectPayment(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Payment rejected")
Rust
let request = tonic::Request::new(RejectPaymentRequest {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
payment_id: "payment_abc123".to_string(),
});
client.reject_payment(request).await?;
println!("Payment rejected");
Register Preimage
Moved (2026-06-11).
RegisterPreimagewas removed fromNodeService. Registering a revealed preimage now lives on the newPreimageService.SettlePreimage(JSON-RPC namespacepreimage), which does everythingRegisterPreimagedid — persist the preimage, claim held channel hashlock payments, arm the force-close — and additionally claims matching on-chain HTLCs on HTLC-capable networks. The request fields are identical (network, hexpayment_preimage).
Set Funding Allowance
Sets the per-asset funding allowance for a peer — how much they may fund, and how much of your balance they may be credited. Replaces any existing allowance.
Funding allowances let you accept incoming channel deposits, hashlock-funded payments, and service-broadcast operations from a known peer without approving each one by hand.
Method: SetFundingAllowance
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | Peer public key |
asset_allowances | map<string, FundingAllowance> | YES | Per-asset allowances |
FundingAllowance:
| Field | Type | Description |
|---|---|---|
allowed_deposit | DecimalString | Maximum deposit the peer may initiate |
allowed_payment | DecimalString | Maximum balance the peer may be credited out of this side, whatever moved it |
allowed_paymentcaps more than paymentsThe name is narrower than the rule. It bounds every way your balance can cross to that peer without a payment of your own — a claim out of a deposit, and equally the
BalanceCreditin a co-signed channel update, which is how a service-broadcast operation takes its fee.So read it as the ceiling on what that peer can end up holding at your expense, and size it against the largest such operation you intend to allow — not just against deposits.
Response: Empty.
Example Request:
import { SetFundingAllowanceRequest } from './proto/node_pb'
const request = new SetFundingAllowanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
request.getAssetAllowancesMap().set('0x0000000000000000000000000000000000000000000000000000000000000000', {
allowedDeposit: { value: '1.0' },
allowedPayment: { value: '0.5' }
})
await client.setFundingAllowance(request, {})
Get Funding Allowance
Returns the current remaining funding allowances for a peer.
Method: GetFundingAllowance
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | Peer public key |
Response:
| Field | Type | Description |
|---|---|---|
asset_allowances | map<string, FundingAllowance> | Remaining allowance per asset |
Example Request:
import { GetFundingAllowanceRequest } from './proto/node_pb'
const request = new GetFundingAllowanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
const response = await client.getFundingAllowance(request, {})
const map = response.getAssetAllowancesMap()
map.forEach((allowance, assetId) => {
console.log(assetId, allowance.toObject())
})
Increase Funding Allowance
Increases an existing funding allowance for a peer by a delta. Each entry in asset_allowances is added to the current allowance for that asset.
Method: IncreaseFundingAllowance
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | Peer public key |
asset_allowances | map<string, FundingAllowance> | YES | Deltas to add |
Response: Empty.
Example Request:
import { IncreaseFundingAllowanceRequest } from './proto/node_pb'
const request = new IncreaseFundingAllowanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
request.getAssetAllowancesMap().set('0x0000000000000000000000000000000000000000000000000000000000000000', {
allowedDeposit: { value: '0.5' },
allowedPayment: { value: '0.25' }
})
await client.increaseFundingAllowance(request, {})
Decrease Funding Allowance
Decreases an existing funding allowance for a peer by a delta. Subtracts each entry from the current allowance.
Method: DecreaseFundingAllowance
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | Peer public key |
asset_allowances | map<string, FundingAllowance> | YES | Deltas to subtract |
Response: Empty.
Example Request:
import { DecreaseFundingAllowanceRequest } from './proto/node_pb'
const request = new DecreaseFundingAllowanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
request.getAssetAllowancesMap().set('0x0000000000000000000000000000000000000000000000000000000000000000', {
allowedDeposit: { value: '0.1' },
allowedPayment: { value: '0.05' }
})
await client.decreaseFundingAllowance(request, {})
Revoke Funding Allowance
Revokes all funding allowances for a peer. Equivalent to setting every asset's allowance to zero.
Method: RevokeFundingAllowance
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
node_id | string | YES | Peer public key |
Response: Empty.
Example Request:
import { RevokeFundingAllowanceRequest } from './proto/node_pb'
const request = new RevokeFundingAllowanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
await client.revokeFundingAllowance(request, {})
Common Workflows
Open channel and wait for activation
There is no WaitForActiveAssetChannel RPC — that pattern was replaced by event subscriptions. Subscribe to event.SubscribeNodeEvents before calling OpenChannel, then watch for the channel-active update for your new channel_id.
import {
OpenChannelRequest,
EstimateOpenChannelFeeRequest,
} from './proto/node_pb'
import { SubscribeNodeEventsRequest } from './proto/event_pb'
async function openAndWaitForChannel(
node: NodeServiceClient,
events: EventServiceClient,
network: { protocol: number; id: string },
peerNodeId: string,
assetId: string,
amountDecimal: string,
) {
// 1. Subscribe BEFORE opening so we don't miss the activation event.
const sub = new SubscribeNodeEventsRequest()
sub.setNetwork(network)
const stream = events.subscribeNodeEvents(sub, {})
// 2. Open the channel.
const open = new OpenChannelRequest()
open.setNetwork(network)
open.setNodeId(peerNodeId)
open.getAssetAmountsMap().set(assetId, {
exact: { amount: { value: amountDecimal } } // DepositAmount oneof
})
open.setFeeOption({ medium: {} })
const opened = await node.openChannel(open, {})
const channelId = opened.getChannelId()
console.log('OpenChannel broadcast, channel:', channelId, 'tx:', opened.getTxid())
// 3. Wait for the channel to go active for this asset.
await new Promise<void>((resolve, reject) => {
stream.on('data', (evt: any) => {
const upd = evt.getChannelUpdate?.()
if (!upd) return
if (upd.getChannelId?.() !== channelId) return
// Asset-channel status `active` means it's ready to send & receive.
const assetChannel = upd.getAssetChannelsMap?.().get(assetId)
if (assetChannel?.getStatus?.()?.getActive?.()) {
stream.cancel()
resolve()
}
})
stream.on('error', reject)
})
console.log('Channel is active for', assetId)
return channelId
}
The exact field paths above (
channel_update,asset_channels,Active) match the currentevent.protoandchannel.proto. If you regenerate code from a newer proto and the names change, follow the proto rather than this snippet.
Create and pay invoice
TypeScript
async function createAndPayInvoice(
client: NodeServiceClient,
network: Network,
assetId: string,
amount: string
) {
// Create invoice
const createReq = new CreateInvoiceRequest()
createReq.setNetwork(network)
createReq.setAssetId(assetId)
createReq.setAmount(amount)
createReq.setExpiryTimeoutSecs(3600)
const createResp = await client.createInvoice(createReq, {})
const invoice = createResp.getInvoice()
const paymentRequest = invoice.getPaymentRequest()
console.log('Invoice created:', paymentRequest)
// Decode to verify
const decodeReq = new DecodeInvoiceRequest()
decodeReq.setNetwork(network)
decodeReq.setPaymentRequest(paymentRequest)
const decoded = await client.decodeInvoice(decodeReq, {})
console.log('Amount:', decoded.getInvoice()?.getAmount())
// Pay invoice
const payReq = new PayInvoiceRequest()
payReq.setNetwork(network)
payReq.setPaymentRequest(paymentRequest)
const payResp = await client.payInvoice(payReq, {})
console.log('Payment sent:', payResp.getPaymentId())
}
Go
func createAndPayInvoice(
client pb.NodeServiceClient,
network *pb.Network,
assetId string,
amount string,
) error {
// Create invoice
createReq := &pb.CreateInvoiceRequest{
Network: network,
AssetId: assetId,
Amount: amount,
ExpiryTimeoutSecs: 3600,
}
createResp, err := client.CreateInvoice(context.Background(), createReq)
if err != nil {
return err
}
invoice := createResp.Invoice
paymentRequest := invoice.PaymentRequest
log.Println("Invoice created:", paymentRequest)
// Decode to verify
decodeReq := &pb.DecodeInvoiceRequest{
Network: network,
PaymentRequest: paymentRequest,
}
decoded, err := client.DecodeInvoice(context.Background(), decodeReq)
if err != nil {
return err
}
log.Println("Amount:", decoded.Invoice.Amount)
// Pay invoice
payReq := &pb.PayInvoiceRequest{
Network: network,
PaymentRequest: paymentRequest,
}
payResp, err := client.PayInvoice(context.Background(), payReq)
if err != nil {
return err
}
log.Println("Payment sent:", payResp.PaymentId)
return nil
}
Rust
async fn create_and_pay_invoice(
client: &mut NodeServiceClient<Channel>,
network: Network,
asset_id: String,
amount: String,
) -> Result<(), Box<dyn std::error::Error>> {
// Create invoice
let create_req = tonic::Request::new(CreateInvoiceRequest {
network: Some(network.clone()),
asset_id,
amount,
hashlock: None,
expiry_timeout_secs: 3600,
});
let create_resp = client.create_invoice(create_req).await?;
let invoice = create_resp.into_inner().invoice.unwrap();
let payment_request = invoice.payment_request.clone();
println!("Invoice created: {}", payment_request);
// Decode to verify
let decode_req = tonic::Request::new(DecodeInvoiceRequest {
network: Some(network.clone()),
payment_request: payment_request.clone(),
});
let decoded = client.decode_invoice(decode_req).await?;
println!("Amount: {}", decoded.into_inner().invoice.unwrap().amount);
// Pay invoice
let pay_req = tonic::Request::new(PayInvoiceRequest {
network: Some(network),
payment_request,
});
let pay_resp = client.pay_invoice(pay_req).await?;
println!("Payment sent: {}", pay_resp.into_inner().payment_id);
Ok(())
}
Safe channel closure
TypeScript
async function safeCloseChannel(
client: NodeServiceClient,
network: Network,
channelId: string,
assetIds: string[]
) {
// Try cooperative close first
try {
const closeReq = new CloseChannelRequest()
closeReq.setNetwork(network)
closeReq.setChannelId(channelId)
closeReq.setAssetIdsList(assetIds)
closeReq.setFeeOption({ medium: {} })
const response = await client.closeChannel(closeReq, {})
console.log('Channel closed cooperatively:', response.getTxid())
return
} catch (error) {
console.log('Cooperative close failed, trying force close...')
}
// Force close if cooperative fails
const forceReq = new ForceCloseChannelRequest()
forceReq.setNetwork(network)
forceReq.setChannelId(channelId)
forceReq.setAssetIdsList(assetIds)
forceReq.setFeeOption({ high: {} })
const forceResp = await client.forceCloseChannel(forceReq, {})
console.log('Force close initiated:', forceResp.getTxid())
console.log('Wait for dispute period before redeeming')
}
Go
func safeCloseChannel(
client pb.NodeServiceClient,
network *pb.Network,
channelId string,
assetIds []string,
) error {
// Try cooperative close first
closeReq := &pb.CloseChannelRequest{
Network: network,
ChannelId: channelId,
AssetIds: assetIds,
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
closeResp, err := client.CloseChannel(context.Background(), closeReq)
if err == nil {
log.Println("Channel closed cooperatively:", closeResp.Txid)
return nil
}
log.Println("Cooperative close failed, trying force close...")
// Force close if cooperative fails
forceReq := &pb.ForceCloseChannelRequest{
Network: network,
ChannelId: channelId,
AssetIds: assetIds,
FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}
forceResp, err := client.ForceCloseChannel(context.Background(), forceReq)
if err != nil {
return err
}
log.Println("Force close initiated:", forceResp.Txid)
log.Println("Wait for dispute period before redeeming")
return nil
}
Rust
async fn safe_close_channel(
client: &mut NodeServiceClient<Channel>,
network: Network,
channel_id: String,
asset_ids: Vec<String>,
) -> Result<(), Box<dyn std::error::Error>> {
// Try cooperative close first
let close_req = tonic::Request::new(CloseChannelRequest {
network: Some(network.clone()),
channel_id: channel_id.clone(),
asset_ids: asset_ids.clone(),
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
match client.close_channel(close_req).await {
Ok(response) => {
println!("Channel closed cooperatively: {}", response.into_inner().txid);
return Ok(());
}
Err(_) => {
println!("Cooperative close failed, trying force close...");
}
}
// Force close if cooperative fails
let force_req = tonic::Request::new(ForceCloseChannelRequest {
network: Some(network),
channel_id,
asset_ids,
fee_option: Some(FeeOption {
fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
}),
});
let force_resp = client.force_close_channel(force_req).await?;
println!("Force close initiated: {}", force_resp.into_inner().txid);
println!("Wait for dispute period before redeeming");
Ok(())
}
Best Practices
- Subscribe to
event.SubscribeNodeEventsbefore opening or paying — channel-active and payment-completed signals come through the stream, not as a return value of the opening RPC. - Estimate fees first — every channel-mutating RPC has a paired
Estimate*variant that takes the same request and returns just the fee. - Try
CloseChannelbeforeForceCloseChannel— cooperative close is cheaper and faster. - Pick reasonable invoice expirations — 1–24 hours is the common range; very short expirations stress retry logic.
- Check capacity before sending — call
EstimateSendPaymentFeeorwatchOnlyNode.GetChannelto confirm the asset side has the balance you need. - Keep hashlock preimages secret until you're ready to release the funds — see
preimage.SettlePreimage(which replacedRegisterPreimage) andResolveHashlockPayment. - Reuse channels via
DepositChannelrather than opening a new one for the same peer / asset.
Channel Lifecycle
1. ConnectToPeer
↓
2. (optionally) AddPeerToZeroConfWhitelist
↓
3. SubscribeNodeEvents ← keep this stream open for steps 4–9
↓
4. OpenChannel
↓
5. wait for ChannelUpdate { asset_channels[…].status = Active }
↓
6. SendPayment / CreateInvoice + PayInvoice / SendChannelPayment
↓
7. DepositChannel (top up either side)
↓
8. WithdrawChannel (pull funds out without closing)
↓
9. CloseChannel → RedeemClosedChannel (if force closed)
Service-backed alternative for steps 4–5 and 7–9: use the Lease API (
liquidity.RequestChannelLiquidity/RequestChannelRelease) instead ofOpenChannel/DepositChannel/WithdrawChannel/CloseChannel. The lease flow handles the same lifecycle but the liquidity service signs and broadcasts the underlying transactions for you.