Remote connect to all the major lightning node implementations with a standard interface.
- Supports all major nodes - CLN, LND, Phoenixd, *LNDK (WIP)
- Supports the main protocols - BOLT 11, BOLT 12, and NWC
- Also popular REST APIs (Custodial) - Strike, Speed, Blink
- Language Binding support for kotlin, swift, react-native, nodejs (typescript/javascript). No support for WASM (yet)
- Tor support
- Runs on Android, iOS, Linux, Windows and Mac
let lnd_node = LndNode::new(LndConfig { url, macaroon });
let cln_node = ClnNode::new(ClnConfig { url, rune });
let lnd_node_info = lnd_node.get_info();
let cln_node_info = cln_node.get_info();
let invoice_params = CreateInvoiceParams {
invoice_type: InvoiceType::Bolt11,
amount_msats: Some(2000),
description: Some("your memo"),
expiry: Some(1743355716),
..Default::default()
});
let lnd_invoice = lnd_node.create_invoice(invoice_params).await;
let cln_invoice = cln_node.create_invoice(invoice_params).await;
let pay_invoice_params = PayInvoiceParams{
invoice: "{lnbc1***}", // BOLT 11 payment request
fee_limit_percentage: Some(1.0), // 1% fee limit
allow_self_payment: Some(true), // This setting works with LND, but is simply ignored for CLN etc...
..Default::default(),
});
let lnd_pay_invoice = lnd_node.pay_invoice(pay_invoice_params);
let cln_pay_invoice = cln_node.pay_invoice(pay_invoice_params);
let lnd_invoice_status = lnd_node.lookup_invoice("{PAYMENT_HASH}");
let cln_invoice_status = cln_node.lookup_invoice("{PAYMENT_HASH}");
let list_txn_params = ListTransactionsParams {
from: 0,
limit: 10,
payment_hash: None, // Optionally pass in the payment hash, or None to search all
};
let lnd_txns = lnd_node.list_transactions(list_txn_params).await;
let cln_txns = cln_node.list_transactions(list_txn_params).await;
// See the tests for more examples
// LND - https://github.com/lightning-node-interface/lni/blob/master/crates/lni/lnd/lib.rs#L96
// CLN - https://github.com/lightning-node-interface/lni/blob/master/crates/lni/cln/lib.rs#L113
// Phoenixd - https://github.com/lightning-node-interface/lni/blob/master/crates/lni/phoenixd/lib.rs#L100
const lndNode = new LndNode({ url, macaroon });
const clnNode = new ClnNode({ url, rune });
const lndNodeInfo = lndNode.getInfo();
const clnNodeInfo = clnNode.getInfo();
const invoiceParams = {
invoiceType: InvoiceType.Bolt11,
amountMsats: 2000,
description: "your memo",
expiry: 1743355716,
});
const lndInvoice = await lndNode.createInvoice(invoiceParams);
const clnInvoice = await clnNode.createInvoice(invoiceParams);
const payInvoiceParams = {
invoice: "{lnbc1***}", // BOLT 11 payment request
feeLimitPercentage: 1, // 1% fee limit
allowSelfPayment: true, // This setting works with LND, but is simply ignored for CLN etc...
});
const lndPayInvoice = await lndNode.payInvoice(payInvoiceParams);
const clnPayInvoice = await clnNode.payInvoice(payInvoiceParams);
const lndInvoiceStatus = await lndNode.lookupInvoice("{PAYMENT_HASH}");
const clnInvoiceStatus = await clnNode.lookupInvoice("{PAYMENT_HASH}");
const listTxnParams = {
from: 0,
limit: 10,
payment_hash: None, // Optionally pass in the payment hash, or None to search all
};
const lndTxns = await lndNode.listTransactions(listTxnParams);
const clnTxns = await clnNode.listTransactions(listTxnParams);
// BOLT 11
node.create_invoice(CreateInvoiceParams) -> Result<Transaction, ApiError>
node.pay_invoice(PayInvoiceParams) -> Result<PayInvoiceResponse, ApiError>
// BOLT 12
node.get_offer(search: Option<String>) -> Result<PayCode, ApiError> // return the first offer or by search id
node.pay_offer(offer: String, amount_msats: i64, payer_note: Option<String>) -> Result<PayInvoiceResponse, ApiError>
node.list_offers(search: Option<String>) -> Result<Vec<PayCode>, ApiError>
// Lookup
node.decode(str: String) -> Result<String, ApiError>
node.lookup_invoice(payment_hash: String) -> Result<Transaction, ApiError>
node.list_transactions(ListTransactionsParams) -> Result<Transaction, ApiError>
node.get_info() -> Result<NodeInfo, ApiError> // returns NodeInfo and balances
// TODO - Not implemented
node.channel_info()
LNI does some simple event polling over http to get some basic invoice status events. Polling is used instead of a heavier grpc/pubsub (for now) to make sure the lib runs cross platform and stays lightweight.
Typescript for react-native
await node.onInvoiceEvents(
// polling params
{
paymentHash: TEST_PAYMENT_HASH,
pollingDelaySec: BigInt(3), // poll every 3 seconds
maxPollingSec: BigInt(60), // for up to 60 seconds
},
// callbacks for each polling round
// The polling ends if success or maxPollingSec timeout is hit
{
success(transaction: Transaction | undefined): void {
console.log('Received success invoice event:', transaction);
setResult('Success');
},
pending(transaction: Transaction | undefined): void {
console.log('Received pending event:', transaction);
},
failure(transaction: Transaction | undefined): void {
console.log('Received failure event:', transaction);
},
}
);
Typescript for nodejs
await node.onInvoiceEvents(
// polling params
{
paymentHash: process.env.LND_TEST_PAYMENT_HASH,
pollingDelaySec: 4,
maxPollingSec: 60,
},
// callback for each polling round
// The polling ends if success or maxPollingSec timeout is hit
(status, tx) => {
console.log("Invoice event:", status, tx);
}
);
Rust
struct OnInvoiceEventCallback {}
impl crate::types::OnInvoiceEventCallback for OnInvoiceEventCallback {
fn success(&self, transaction: Option<Transaction>) {
println!("success");
}
fn pending(&self, transaction: Option<Transaction>) {
println!("pending");
}
fn failure(&self, transaction: Option<Transaction>) {
println!("epic fail");
}
}
let params = crate::types::OnInvoiceEventParams {
payment_hash: TEST_PAYMENT_HASH.to_string(),
polling_delay_sec: 3,
max_polling_sec: 60,
};
let callback = OnInvoiceEventCallback {};
NODE.on_invoice_events(params, Box::new(callback));
cd crates/lni
cargo clean
cargo build
cargo test
lni
├── bindings
│ ├── lni_nodejs
│ ├── lni_react_native
├── crates
│ ├── lni
│ |─── lnd
│ |─── cln
│ |─── phoenixd
│ |─── nwc
│ |─── strike
│ |─── speed
│ |─── blink
cd bindings/lni_react_native
cat example/src/App.tsx
yarn start
*troubleshooting react-natve
:
- if you get an error like
uniffiEnsureInitialized
, then you might need to kill the app and restart. (ios simulator - double tap home button then swipe away app) - try updating the pods for ios
cd example/ios && pod install --repo-update && cd ../
- for ios open the xcode app - lni/bindings/lni_react_native/example/ios/LniExample.xcworkspace
- Then click the project in the left "LniExample" to select for the context menu
- In the top click "Product -> Clean build folder" and then build and run
- Lastly uninstalling the app from the mobile device
cd bindings/lni_nodejs
cat main.mjs
yarn
# then open ../../crates/lni/Cargo.toml and comment out #crate-type = ["staticlib"]
yarn build
node main.mjs
# These env vars are used for tests
TEST_RECEIVER_OFFER=lnotestoffer***
PHOENIX_MOBILE_OFFER=lnotestoffer***
PHOENIXD_URL=http://localhost:9740
PHOENIXD_PASSWORD=YOUR_HTTP_PASSWORD
PHOENIXD_TEST_PAYMENT_HASH=YOUR_TEST_PAYMENT_HASH
CLN_URL=http://localhost:3010
CLN_RUNE=YOUR_RUNE
CLN_TEST_PAYMENT_HASH=YOUR_HASH
CLN_OFER=lnotestoffer***
LND_URL=""
LND_MACAROON=""
LND_TEST_PAYMENT_HASH=""
LND_TEST_PAYMENT_REQUEST=""
NWC_URI="nostr+walletconnect://*"
NWC_TEST_PAYMENT_HASH=""
NWC_TEST_PAYMENT_REQUEST=""
STRIKE_API_KEY=""
STRIKE_TEST_PAYMENT_HASH=""
STRIKE_TEST_PAYMENT_REQUEST=""
BLINK_API_KEY=""
BLINK_TEST_PAYMENT_HASH=""
BLINK_TEST_PAYMENT_REQUEST=""
SPEED_API_KEY=""
SPEED_TEST_PAYMENT_HASH=""
SPEED_TEST_PAYMENT_REQUEST=""
-
- napi_rs
- https://napi.rs/docs/introduction/simple-package
cd bindings/lni_nodejs && cargo clean && cargo build --release && yarn && yarn build
- test
node main.mjs
-
- if you want to use the native node module (maybe for an electron app) you can reference the file
bindings/lni_nodejs/lni_js.${platform}-${arch}.node
. It would look something like in your project:const path = require("path"); const os = require("os"); const platform = os.platform(); const arch = os.arch(); const nativeModulePath = path.join( __dirname, `../../code/lni/bindings/lni_nodejs/lni_js.${platform}-${arch}.node` ); const { PhoenixdNode } = require(nativeModulePath);
- if you want to use the native node module (maybe for an electron app) you can reference the file
-
uniffi-bindgen-react-native
lib- https://jhugman.github.io/uniffi-bindgen-react-native/guides/getting-started.html
- sample https://github.com/ianthetechie/uniffi-starter
cd bindings/lni_react_native && ./build.sh
To include it in your react-native project:
- In this project run
cd bindings/lni_react_native && ./build.sh && yarn package --out lni_react_native.tgz
- This creates a
lni_react_native.tgz
. Copy this to your target React Native project and extract it into a folder calledmodules
in the root of your RN project. Make sure it extracts to themodules/lni_react_native
folder - Install deps:
yarn add uniffi-bindgen-react-native@0.29.0-0 react-native-builder-bob@0.39.0
- Install lni:
yarn add "lni_react_native@link:./modules/lni_react_native"
yarn cache clean && yarn clean:android && yarn
- import it in your RN component:
import { LndNode, LndConfig, ClnNode, ClnConfig, PhoenixdNode, PhoenixdConfig, StrikeNode, StrikeConfig, Transaction, ListTransactionsParams, } from 'lni_react_native';
- troubleshooting
npx react-native start --reset-cache
or if you have build error try symbolic links from node_modules to modules folder like this:yarn remove lni_react_native && rm -f node_modules/lni_react_native && ln -sf ../modules/lni_react_native node_modules/lni_react_native && yarn add "lni_react_native@link:./modules/lni_react_native"
if the app has build failures on android, undo the diff for build.gradle, i.e like the gradle version might be wrong 8.8.0 vs 7.2.1 etc..
To Update the package: if you updated the lni source code
- In this project run
cd bindings/lni_react_native && ./build.sh && yarn package --out lni_react_native.tgz
- This creates a
lni_react_native.tgz
. Copy this to your target React Native project and extract it into a folder calledmodules
in the root of your RN project. Make sure it extracts to themodules/lni_react_native
folder - Undo gradle changes to
modules/lni_react_native/android/build.gradle
to avoid version mismatches. you might need to update the following build.gradle config:
dependencies { classpath "com.android.tools.build:gradle:8.8.0" // noinspection DifferentKotlinGradleVersion classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } sourceSets { main { java.srcDirs += [ "generated/java", "generated/jni" ] } }
- Also might need to Undo changes to
LniReactNativeModule.kt
and add backimport com.facebook.fbreact.specs.NativeLniReactNativeSpec
- In the remote project re-add the package
lni_react_native
by running:
yarn remove lni_react_native && yarn clean && ln -sf ../modules/lni_react_native node_modules/lni_react_native && yarn add "lni_react_native@link:./modules/lni_react_native" yarn android
-
- https://mozilla.github.io/uniffi-rs/latest/
- Uses decorators like
#[cfg_attr(feature = "uniffi", uniffi::export)]
to foreign codegen
If you do not want to copy objects to the foreign language bindings we can simply use the features napi_rs
or uniffi
to turn on or off language specific decorators and then implement them in their respective bindings project.
Example:
#[cfg(feature = "napi_rs")]
use napi_derive::napi;
#[cfg_attr(feature = "napi_rs", napi(object))]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct PhoenixdConfig {
pub url: String,
pub password: String,
}
#[cfg_attr(feature = "napi_rs", napi(object))]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct PhoenixdNode {
pub url: String,
pub password: String,
}
Use the Tor Socks5 proxy settings if you are connecting to a .onion
hidden service. Make sure to include the "h"
in "socks5h://"
to resolve onion addresses properly. You can start up a Tor Socks5 proxy easily using Arti https://tpo.pages.torproject.net/core/arti/
example
LndNode::new(LndConfig {
url: "https://YOUR_LND_ONION_ADDRESS.onion",
macaroon: "YOUR_MACAROON",
socks5_proxy: Some("socks5h://127.0.0.1:9150".to_string()),
accept_invalid_certs: Some(true),
..Default::default()
})
Strike with SOCKS5 Proxy Support: Strike now supports the same SOCKS5 proxy and invalid certificate settings as LND:
StrikeNode::new(StrikeConfig {
base_url: Some("https://api.strike.me/v1".to_string()),
api_key: "YOUR_API_KEY".to_string(),
http_timeout: Some(120),
socks5_proxy: Some("socks5h://127.0.0.1:9150".to_string()), // Tor proxy
accept_invalid_certs: Some(true), // Accept self-signed certificates
})
This is useful for:
- Connecting through Tor for privacy
- Testing with self-signed certificates in development
- Connecting through corporate proxies that use invalid certificates
- https://github.com/ZeusLN/zeus/blob/master/backends/LND.ts
- https://github.com/getAlby/hub/tree/master/lnclient
- https://github.com/fedimint/fedimint/blob/master/gateway/ln-gateway/src/lightning/lnd.rs
This project structure was inpired by this https://github.com/ianthetechie/uniffi-starter/ with the intention of
automating the creation of lni_react_native
https://jhugman.github.io/uniffi-bindgen-react-native/guides/getting-started.html
sequenceDiagram
participant App as Application (JS/Swift/Kotlin)
participant Binding as Language Binding (Node.js/React Native/UniFfi)
participant LNI as LNI Core (Rust)
participant Node as Lightning Node Implementation (CLN/LND/Phoenixd)
participant LN as Lightning Node (REST/gRPC API)
App->>Binding: Create config (URL, authentication)
Binding->>LNI: Initialize node with config
LNI->>LNI: Create node object (PhoenixdNode, ClnNode, etc.)
Note over App,LN: Example: Get Node Info
App->>Binding: node.getInfo()
Binding->>LNI: get_info()
LNI->>Node: api::get_info(url, auth)
Node->>LN: HTTP/REST request to /v1/getinfo
LN-->>Node: Response (JSON)
Node->>Node: Parse response
Node-->>LNI: NodeInfo object
LNI-->>Binding: NodeInfo struct
Binding-->>App: NodeInfo object
Note over App,LN: Example: Create Invoice
App->>Binding: node.createInvoice(params)
Binding->>LNI: create_invoice(params)
LNI->>Node: api::create_invoice(url, auth, params)
Node->>LN: HTTP/REST request to create invoice
LN-->>Node: Response with invoice data
Node->>Node: Parse response
Node-->>LNI: Transaction object
LNI-->>Binding: Transaction struct
Binding-->>App: Transaction object
Note over App,LN: Example: Pay Invoice/Offer
App->>Binding: node.payOffer(offer, amount, note)
Binding->>LNI: pay_offer(offer, amount, note)
LNI->>Node: api::pay_offer(url, auth, offer, amount, note)
Node->>LN: HTTP/REST request to pay
LN-->>Node: Payment response
Node->>Node: Parse response
Node-->>LNI: PayInvoiceResponse object
LNI-->>Binding: PayInvoiceResponse struct
Binding-->>App: PayInvoiceResponse object
- [] bug in fees (bad conversion? msats)
- make interface
- napi-rs for nodejs
- uniffi bindings for Android and IOS
- react-native - uniffi-bindgen-react-native
- async promise architecture for bindings
- Tor Socks5 fetch https://tpo.pages.torproject.net/core/arti/guides/starting-arti
- Simple event polling
- implement lightning nodes
- phoenixd
- cln
- lnd
- lndk
- ldk_node? (Alby hub?)
- eclair
- strike (BOLT 12 support?, BOLT 11 blinded path support?)
- nwc
- blink
- speed
- napi-rs https://napi.rs/docs/introduction/simple-package
- can we support more complex grpc in
- wasm?