Node API
The Node API provides Lightning Network channel management and payment operations.
JSON-RPC namespace: node
Endpoints
Peer Management
Zero-Conf Whitelist
Channel Operations
- Estimate Open Channel Fee
- Open Channel
- Estimate Deposit Channel Fee
- Deposit Channel
- Estimate Withdraw Channel Fee
- Withdraw 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
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:5001')
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())
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, {})
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, SendAmount> | YES | Map of asset_id → amount to allocate |
fee_rate | FeeRate | YES | Transaction fee rate |
SendAmount Object (one of):
| Field | Type | Description |
|---|---|---|
value | DecimalString | Exact amount |
max | - | Use maximum available |
FeeRate Object (one of):
| Field | Type | Description |
|---|---|---|
absolute | DecimalString | Absolute fee amount |
relative | DecimalString | Relative fee (e.g., sat/vB) |
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({
'BTC': { value: '10000000' } // 0.1 BTC
})
request.setFeeRate({ relative: '5' }) // 5 sat/vB
const response = await client.estimateOpenChannelFee(request, {})
console.log('Estimated fee:', response.getFee(), 'sats')
Go
req := &pb.EstimateOpenChannelFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
NodeId: "02abc123...",
AssetAmounts: map[string]*pb.SendAmount{
"BTC": {Value: "10000000"}, // 0.1 BTC
},
FeeRate: &pb.FeeRate{Relative: "5"}, // 5 sat/vB
}
resp, err := client.EstimateOpenChannelFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
log.Println("Estimated fee:", resp.Fee, "sats")
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("BTC".to_string(), SendAmount {
value: "10000000".to_string(), // 0.1 BTC
});
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_rate: Some(FeeRate {
relative: "5".to_string(), // 5 sat/vB
}),
});
let response = client.estimate_open_channel_fee(request).await?;
println!("Estimated fee: {} sats", 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, SendAmount> | YES | Assets to allocate |
fee_rate | FeeRate | YES | Transaction fee 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({
'BTC': { value: '10000000' }
})
request.setFeeRate({ relative: '5' })
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.SendAmount{
"BTC": {Value: "10000000"},
},
FeeRate: &pb.FeeRate{Relative: "5"},
}
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("BTC".to_string(), SendAmount {
value: "10000000".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_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
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, SendAmount> | YES | Assets to deposit |
fee_rate | FeeRate | YES | Transaction fee 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({
'BTC': { value: '5000000' } // Add 0.05 BTC
})
request.setFeeRate({ relative: '5' })
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.SendAmount{
"BTC": {Value: "5000000"}, // Add 0.05 BTC
},
FeeRate: &pb.FeeRate{Relative: "5"},
}
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("BTC".to_string(), SendAmount {
value: "5000000".to_string(), // Add 0.05 BTC
});
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_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
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, SendAmount> | YES | Assets to deposit |
fee_rate | FeeRate | YES | Transaction fee 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({
'BTC': { value: '5000000' }
})
request.setFeeRate({ relative: '5' })
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.SendAmount{
"BTC": {Value: "5000000"},
},
FeeRate: &pb.FeeRate{Relative: "5"},
}
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("BTC".to_string(), SendAmount {
value: "5000000".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_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
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_rate | FeeRate | YES | Transaction fee rate |
WithdrawAmount Object:
| Field | Type | Description |
|---|---|---|
self_withdrawal | SendAmount | Amount you withdraw |
counterparty_withdrawal | SendAmount | Amount 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({
'BTC': {
selfWithdrawal: { value: '3000000' },
counterpartyWithdrawal: { value: '2000000' }
}
})
request.setFeeRate({ relative: '5' })
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{
"BTC": {
SelfWithdrawal: &pb.SendAmount{Value: "3000000"},
CounterpartyWithdrawal: &pb.SendAmount{Value: "2000000"},
},
},
FeeRate: &pb.FeeRate{Relative: "5"},
}
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("BTC".to_string(), WithdrawAmount {
self_withdrawal: Some(SendAmount {
value: "3000000".to_string(),
}),
counterparty_withdrawal: Some(SendAmount {
value: "2000000".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_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
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_rate | FeeRate | YES | Transaction fee 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({
'BTC': {
selfWithdrawal: { value: '3000000' },
counterpartyWithdrawal: { value: '2000000' }
}
})
request.setFeeRate({ relative: '5' })
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{
"BTC": {
SelfWithdrawal: &pb.SendAmount{Value: "3000000"},
CounterpartyWithdrawal: &pb.SendAmount{Value: "2000000"},
},
},
FeeRate: &pb.FeeRate{Relative: "5"},
}
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("BTC".to_string(), WithdrawAmount {
self_withdrawal: Some(SendAmount {
value: "3000000".to_string(),
}),
counterparty_withdrawal: Some(SendAmount {
value: "2000000".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_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
let response = client.withdraw_channel(request).await?;
println!("Withdrawal TX: {}", response.into_inner().txid);
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_rate | FeeRate | YES | Transaction fee 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(['BTC'])
request.setFeeRate({ relative: '5' })
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{"BTC"},
FeeRate: &pb.FeeRate{Relative: "5"},
}
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!["BTC".to_string()],
fee_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
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_rate | FeeRate | YES | Transaction fee 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(['BTC'])
request.setFeeRate({ relative: '5' })
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{"BTC"},
FeeRate: &pb.FeeRate{Relative: "5"},
}
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!["BTC".to_string()],
fee_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
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_rate | FeeRate | YES | Transaction fee 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(['BTC'])
request.setFeeRate({ relative: '5' })
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{"BTC"},
FeeRate: &pb.FeeRate{Relative: "5"},
}
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!["BTC".to_string()],
fee_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
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_rate | FeeRate | YES | Transaction fee 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(['BTC'])
request.setFeeRate({ relative: '10' }) // Higher fee recommended
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{"BTC"},
FeeRate: &pb.FeeRate{Relative: "10"}, // Higher fee recommended
}
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!["BTC".to_string()],
fee_rate: Some(FeeRate {
relative: "10".to_string(), // Higher fee recommended
}),
});
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_rate | FeeRate | YES | Transaction fee 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(['BTC'])
request.setFeeRate({ relative: '5' })
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{"BTC"},
FeeRate: &pb.FeeRate{Relative: "5"},
}
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!["BTC".to_string()],
fee_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
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_rate | FeeRate | YES | Transaction fee 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(['BTC'])
request.setFeeRate({ relative: '5' })
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{"BTC"},
FeeRate: &pb.FeeRate{Relative: "5"},
}
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!["BTC".to_string()],
fee_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
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. Each operation can be an open, deposit, withdraw, close, or force-close.
Method: BatchChannelOperations
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
operations | BatchChannelOperation[] | YES | Operations to execute atomically |
tx_fee_rate | FeeRate | YES | Chain fee 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.setTxFeeRate({
maxFeePerUnit: { value: '50' },
priorityFeePerUnit: { value: '5' }
})
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.
Response:
| Field | Type | Description |
|---|---|---|
fee_estimate | FeeEstimate | Total fee estimate at low/medium/high priority |
Example:
const response = await client.estimateBatchChannelOperationsFee(request, {})
console.log('Fee estimate:', response.getFeeEstimate()?.toObject())
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 | SimulatedChannelOp | YES | Operation to simulate |
tx_fee_rate | FeeRate | YES | Chain fee rate |
Response:
| Field | Type | Description |
|---|---|---|
fee_estimate | FeeEstimate | Estimated fee |
Example:
import { EstimateSimulatedChannelOperationFeeRequest } from './proto/node_pb'
const request = new EstimateSimulatedChannelOperationFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannel(/* SimulatedChannel */)
request.setOperation(/* SimulatedChannelOp */)
request.setTxFeeRate({
maxFeePerUnit: { value: '50' },
priorityFeePerUnit: { value: '5' }
})
const response = await client.estimateSimulatedChannelOperationFee(request, {})
console.log('Estimate:', response.getFeeEstimate()?.toObject())
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 |
tx_fee_rate | FeeRate | YES | Chain fee rate |
Response:
| Field | Type | Description |
|---|---|---|
fee_estimate | FeeEstimate | Aggregate estimated fee |
Example:
import { EstimateSimulatedChannelOperationsFeeRequest } from './proto/node_pb'
const request = new EstimateSimulatedChannelOperationsFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setOperationsList([/* SimulatedChannelOp entries */])
request.setTxFeeRate({
maxFeePerUnit: { value: '50' },
priorityFeePerUnit: { value: '5' }
})
const response = await client.estimateSimulatedChannelOperationsFee(request, {})
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 Object:
| Field | Type | Description |
|---|---|---|
payment_hash | string | Payment hash (32 bytes hex) |
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({
'BTC': { value: '100000' } // 0.001 BTC
})
// 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{
"BTC": {Value: "100000"}, // 0.001 BTC
},
// 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("BTC".to_string(), Amount {
value: "100000".to_string(), // 0.001 BTC
});
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, SendAmount> | 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({
'BTC': { value: '500000' }
})
const response = await client.estimateSendPaymentFee(request, {})
const fees = response.getFeesMap()
console.log('Routing fee:', fees.get('BTC'), 'sats')
Go
req := &pb.EstimateSendPaymentFeeRequest{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
RecipientNodeId: "03def456...",
AssetAmounts: map[string]*pb.Amount{
"BTC": {Value: "500000"},
},
}
resp, err := client.EstimateSendPaymentFee(context.Background(), req)
if err != nil {
log.Fatal(err)
}
fees := resp.Fees
log.Println("Routing fee:", fees["BTC"], "sats")
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("BTC".to_string(), SendAmount {
value: "500000".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: {} sats", fees.get("BTC").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, SendAmount> | 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({
'BTC': { value: '500000' }
})
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{
"BTC": {Value: "500000"},
},
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("BTC".to_string(), SendAmount {
value: "500000".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 |
|---|---|---|
payment_request | string | Encoded payment request string |
payment_hash | string | Payment hash |
amount | DecimalString | Invoice amount (optional) |
asset_id | string | Asset identifier |
expiry | Timestamp | Invoice validity expiry |
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 + min_final_cltv_expiry_secs. |
Example Request:
TypeScript
const request = new CreateInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setAssetId('BTC')
request.setAmount('1000000') // 0.01 BTC
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: "BTC",
Amount: "1000000", // 0.01 BTC
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: "BTC".to_string(),
amount: "1000000".to_string(), // 0.01 BTC
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('BTC')
// 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: "BTC",
// 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: "BTC".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 |
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(), 'sats')
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, "sats")
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: {} sats", 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 |
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 | SendAmount | YES | Amount to pay |
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({ value: '2000000' }) // 0.02 BTC
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.SendAmount{Value: "2000000"}, // 0.02 BTC
}
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(SendAmount {
value: "2000000".to_string(), // 0.02 BTC
}),
});
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 | SendAmount | YES | Amount to pay |
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({ value: '2000000' })
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.SendAmount{Value: "2000000"},
}
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(SendAmount {
value: "2000000".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
Pre-registers a payment preimage with the node so incoming HTLCs that match the preimage's hash can be settled automatically. Useful for split-receive flows or external invoice systems.
Method: RegisterPreimage
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
payment_preimage | string | YES | Hex-encoded preimage to register |
Response: Empty.
Example Request:
import { RegisterPreimageRequest } from './proto/node_pb'
const request = new RegisterPreimageRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentPreimage('9f8e7d6c...')
await client.registerPreimage(request, {})
Set Funding Allowance
Sets the per-asset funding allowance for a peer — the maximum amount they can deposit to and the maximum payment they can claim from that deposit. Replaces any existing allowance.
Funding allowances let you accept incoming channel deposits or hashlock-funded payments from a known peer without manually approving each one.
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 payment the peer may claim |
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('BTC', {
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('BTC', {
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('BTC', {
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.setFeeRate({ relative: '5' })
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.setFeeRate({ relative: '10' })
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,
FeeRate: &pb.FeeRate{Relative: "5"},
}
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,
FeeRate: &pb.FeeRate{Relative: "10"},
}
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_rate: Some(FeeRate {
relative: "5".to_string(),
}),
});
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_rate: Some(FeeRate {
relative: "10".to_string(),
}),
});
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
RegisterPreimageandResolveHashlockPayment. - 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.