diff --git a/bindings/lni_nodejs/Cargo.toml b/bindings/lni_nodejs/Cargo.toml index e2124d4..c0a0b99 100644 --- a/bindings/lni_nodejs/Cargo.toml +++ b/bindings/lni_nodejs/Cargo.toml @@ -15,7 +15,7 @@ async-trait = "0.1" thiserror = "1.0" serde = { version = "1", features=["derive"] } serde_json = "1" -reqwest = { version = "0.10", default-features = false, features = ["json", "rustls-tls", "blocking"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "blocking", "socks"] } tokio = { version = "1", features = ["full"] } napi = { version = "2.12.2", default-features = false, features = ["napi4", "tokio_rt", "async"] } napi-derive = "2.12.2" diff --git a/bindings/lni_nodejs/index.d.ts b/bindings/lni_nodejs/index.d.ts index ab6fa88..c1f5a94 100644 --- a/bindings/lni_nodejs/index.d.ts +++ b/bindings/lni_nodejs/index.d.ts @@ -6,10 +6,12 @@ export interface PhoenixdConfig { url: string password: string + socks5Proxy?: string + acceptInvalidCerts?: boolean + httpTimeout?: number } export interface PhoenixdNode { - url: string - password: string + config: PhoenixdConfig } export interface Bolt11Resp { amountSat: number @@ -26,18 +28,22 @@ export interface PhoenixPayInvoiceResp { export interface ClnConfig { url: string rune: string + socks5Proxy?: string + acceptInvalidCerts?: boolean + httpTimeout?: number } export interface ClnNode { - url: string - rune: string + config: ClnConfig } export interface LndConfig { url: string macaroon: string + socks5Proxy?: string + acceptInvalidCerts?: boolean + httpTimeout?: number } export interface LndNode { - url: string - macaroon: string + config: LndConfig } export const enum InvoiceType { Bolt11 = 'Bolt11', diff --git a/bindings/lni_nodejs/main.mjs b/bindings/lni_nodejs/main.mjs index 857ea50..342638d 100644 --- a/bindings/lni_nodejs/main.mjs +++ b/bindings/lni_nodejs/main.mjs @@ -132,19 +132,27 @@ async function lnd() { } async function test() { + // const config = { + // url: process.env.PHOENIXD_URL, + // password: process.env.PHOENIXD_PASSWORD, + // test_hash: process.env.PHOENIXD_TEST_PAYMENT_HASH, + // }; + // const node = new PhoenixdNode(config); const config = { - url: process.env.PHOENIXD_URL, - password: process.env.PHOENIXD_PASSWORD, - test_hash: process.env.PHOENIXD_TEST_PAYMENT_HASH, + url: process.env.LND_URL, + macaroon: process.env.LND_MACAROON, + socks5Proxy: "socks5h://127.0.0.1:9150", + acceptInvalidCerts: true, }; - const node = new PhoenixdNode(config); + const node = new LndNode(config); + console.log("Node info:", await node.getInfo()); } async function main() { // phoenixd(); - cln(); + // cln(); // lnd(); - // test(); + test(); } main(); diff --git a/bindings/lni_nodejs/src/cln.rs b/bindings/lni_nodejs/src/cln.rs index e008aa0..a146e60 100644 --- a/bindings/lni_nodejs/src/cln.rs +++ b/bindings/lni_nodejs/src/cln.rs @@ -25,15 +25,13 @@ impl ClnNode { #[napi] pub fn get_config(&self) -> ClnConfig { - ClnConfig { - url: self.inner.url.clone(), - rune: self.inner.rune.clone(), - } + self.inner.clone() } #[napi] pub async fn get_info(&self) -> napi::Result { - let info = lni::cln::api::get_info(self.inner.url.clone(), self.inner.rune.clone()) + let info = lni::cln::api::get_info(&self.inner) + .await .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(info) } @@ -44,8 +42,7 @@ impl ClnNode { params: CreateInvoiceParams, ) -> napi::Result { let txn = lni::cln::api::create_invoice( - self.inner.url.clone(), - self.inner.rune.clone(), + &self.inner, params.invoice_type, params.amount_msats, params.offer, @@ -63,16 +60,15 @@ impl ClnNode { &self, params: PayInvoiceParams, ) -> Result { - let invoice = - lni::cln::api::pay_invoice(self.inner.url.clone(), self.inner.rune.clone(), params) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let invoice = lni::cln::api::pay_invoice(&self.inner, params) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(invoice) } #[napi] pub async fn get_offer(&self, search: Option) -> Result { - let offer = lni::cln::api::get_offer(self.inner.url.clone(), self.inner.rune.clone(), search) + let offer = lni::cln::api::get_offer(&self.inner, search) .await .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(offer) @@ -80,10 +76,9 @@ impl ClnNode { #[napi] pub async fn list_offers(&self, search: Option) -> Result> { - let offers = - lni::cln::api::list_offers(self.inner.url.clone(), self.inner.rune.clone(), search) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let offers = lni::cln::api::list_offers(&self.inner, search) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(offers) } @@ -94,28 +89,17 @@ impl ClnNode { amount_msats: i64, payer_note: Option, ) -> napi::Result { - let offer = lni::cln::api::pay_offer( - self.inner.url.clone(), - self.inner.rune.clone(), - offer, - amount_msats, - payer_note, - ) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let offer = lni::cln::api::pay_offer(&self.inner, offer, amount_msats, payer_note) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(offer) } #[napi] pub async fn lookup_invoice(&self, payment_hash: String) -> napi::Result { - let txn = lni::cln::api::lookup_invoice( - self.inner.url.clone(), - self.inner.rune.clone(), - Some(payment_hash), - None, - None, - ) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let txn = lni::cln::api::lookup_invoice(&self.inner, Some(payment_hash), None, None) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(txn) } @@ -124,19 +108,15 @@ impl ClnNode { &self, params: lni::types::ListTransactionsParams, ) -> napi::Result> { - let txns = lni::cln::api::list_transactions( - self.inner.url.clone(), - self.inner.rune.clone(), - params.from, - params.limit, - ) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let txns = lni::cln::api::list_transactions(&self.inner, params.from, params.limit) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(txns) } #[napi] pub async fn decode(&self, str: String) -> Result { - let decoded = lni::cln::api::decode(self.inner.url.clone(), self.inner.rune.clone(), str) + let decoded = lni::cln::api::decode(&self.inner, str) .await .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(decoded) diff --git a/bindings/lni_nodejs/src/lnd.rs b/bindings/lni_nodejs/src/lnd.rs index b10cc10..f6b246f 100644 --- a/bindings/lni_nodejs/src/lnd.rs +++ b/bindings/lni_nodejs/src/lnd.rs @@ -28,12 +28,16 @@ impl LndNode { LndConfig { url: self.inner.url.clone(), macaroon: self.inner.macaroon.clone(), + socks5_proxy: self.inner.socks5_proxy.clone(), + accept_invalid_certs: self.inner.accept_invalid_certs, + http_timeout: self.inner.http_timeout, } } #[napi] pub async fn get_info(&self) -> napi::Result { - let info = lni::lnd::api::get_info(self.inner.url.clone(), self.inner.macaroon.clone()) + let info = lni::lnd::api::get_info(&self.inner) + .await .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(info) } @@ -43,10 +47,9 @@ impl LndNode { &self, params: CreateInvoiceParams, ) -> napi::Result { - let txn = - lni::lnd::api::create_invoice(self.inner.url.clone(), self.inner.macaroon.clone(), params) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let txn = lni::lnd::api::create_invoice(&self.inner, params) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(txn) } @@ -55,28 +58,25 @@ impl LndNode { &self, params: PayInvoiceParams, ) -> Result { - let invoice = - lni::lnd::api::pay_invoice(self.inner.url.clone(), self.inner.macaroon.clone(), params) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let invoice = lni::lnd::api::pay_invoice(&self.inner, params) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(invoice) } #[napi] pub async fn get_offer(&self, search: Option) -> Result { - let offer = - lni::lnd::api::get_offer(self.inner.url.clone(), self.inner.macaroon.clone(), search) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let offer = lni::lnd::api::get_offer(&self.inner, search) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(offer) } #[napi] pub async fn list_offers(&self, search: Option) -> Result> { - let offers = - lni::lnd::api::list_offers(self.inner.url.clone(), self.inner.macaroon.clone(), search) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let offers = lni::lnd::api::list_offers(&self.inner, search) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(offers) } @@ -87,26 +87,17 @@ impl LndNode { amount_msats: i64, payer_note: Option, ) -> napi::Result { - let offer = lni::lnd::api::pay_offer( - self.inner.url.clone(), - self.inner.macaroon.clone(), - offer, - amount_msats, - payer_note, - ) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let offer = lni::lnd::api::pay_offer(&self.inner, offer, amount_msats, payer_note) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(offer) } #[napi] pub async fn lookup_invoice(&self, payment_hash: String) -> napi::Result { - let txn = lni::lnd::api::lookup_invoice( - self.inner.url.clone(), - self.inner.macaroon.clone(), - Some(payment_hash), - ) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let txn = lni::lnd::api::lookup_invoice(&self.inner, Some(payment_hash)) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(txn) } @@ -115,19 +106,15 @@ impl LndNode { &self, params: lni::types::ListTransactionsParams, ) -> napi::Result> { - let txns = lni::lnd::api::list_transactions( - self.inner.url.clone(), - self.inner.macaroon.clone(), - params.from, - params.limit, - ) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let txns = lni::lnd::api::list_transactions(&self.inner, params.from, params.limit) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(txns) } #[napi] pub async fn decode(&self, str: String) -> Result { - let decoded = lni::lnd::api::decode(self.inner.url.clone(), self.inner.macaroon.clone(), str) + let decoded = lni::lnd::api::decode(&self.inner, str) .await .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(decoded) diff --git a/bindings/lni_nodejs/src/phoenixd.rs b/bindings/lni_nodejs/src/phoenixd.rs index 1f4313c..bdca2da 100644 --- a/bindings/lni_nodejs/src/phoenixd.rs +++ b/bindings/lni_nodejs/src/phoenixd.rs @@ -26,15 +26,13 @@ impl PhoenixdNode { #[napi] pub fn get_config(&self) -> PhoenixdConfig { - PhoenixdConfig { - url: self.inner.url.clone(), - password: self.inner.password.clone(), - } + self.inner.clone() } #[napi] pub async fn get_info(&self) -> napi::Result { - let info = lni::phoenixd::api::get_info(self.inner.url.clone(), self.inner.password.clone()) + let info = lni::phoenixd::api::get_info(&self.inner) + .await .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(info) } @@ -45,8 +43,7 @@ impl PhoenixdNode { params: CreateInvoiceParams, ) -> napi::Result { let txn = lni::phoenixd::api::create_invoice( - self.inner.url.clone(), - self.inner.password.clone(), + &self.inner, params.invoice_type, params.amount_msats, params.description, @@ -63,30 +60,25 @@ impl PhoenixdNode { &self, params: PayInvoiceParams, ) -> Result { - let invoice = - lni::phoenixd::api::pay_invoice(self.inner.url.clone(), self.inner.password.clone(), params) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let invoice = lni::phoenixd::api::pay_invoice(&self.inner, params) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(invoice) } #[napi] pub async fn get_offer(&self) -> Result { - let paycode = - lni::phoenixd::api::get_offer(self.inner.url.clone(), self.inner.password.clone()) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let paycode = lni::phoenixd::api::get_offer(&self.inner) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(paycode) } #[napi] pub async fn lookup_invoice(&self, payment_hash: String) -> napi::Result { - let txn = lni::phoenixd::api::lookup_invoice( - self.inner.url.clone(), - self.inner.password.clone(), - payment_hash, - ) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let txn = lni::phoenixd::api::lookup_invoice(&self.inner, payment_hash) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(txn) } @@ -97,15 +89,9 @@ impl PhoenixdNode { amount_msats: i64, payer_note: Option, ) -> napi::Result { - let offer = lni::phoenixd::api::pay_offer( - self.inner.url.clone(), - self.inner.password.clone(), - offer, - amount_msats, - payer_note, - ) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let offer = lni::phoenixd::api::pay_offer(&self.inner, offer, amount_msats, payer_note) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(offer) } @@ -114,14 +100,9 @@ impl PhoenixdNode { &self, params: crate::ListTransactionsParams, ) -> napi::Result> { - let txns = lni::phoenixd::api::list_transactions( - self.inner.url.clone(), - self.inner.password.clone(), - params.from, - params.limit, - None, - ) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; + let txns = lni::phoenixd::api::list_transactions(&self.inner, params.from, params.limit, None) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; Ok(txns) } } @@ -147,6 +128,7 @@ mod tests { PhoenixdNode::new(PhoenixdConfig { url: URL.clone(), password: PASSWORD.clone(), + ..Default::default() }) }; } diff --git a/bindings/lni_uniffi/Cargo.toml b/bindings/lni_uniffi/Cargo.toml index d2ef1c2..a6794c4 100644 --- a/bindings/lni_uniffi/Cargo.toml +++ b/bindings/lni_uniffi/Cargo.toml @@ -16,7 +16,7 @@ uniffi = { version = "0.28" } thiserror = "1.0" serde = { version = "1", features=["derive"] } serde_json = "1" -reqwest = { version = "0.10", default-features = false, features = ["json", "rustls-tls", "blocking"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "blocking", "socks"] } tokio = { version = "1", features = ["full"] } [build-dependencies] diff --git a/bindings/lni_uniffi/src/lni.udl b/bindings/lni_uniffi/src/lni.udl index 4c17e10..1d555e4 100644 --- a/bindings/lni_uniffi/src/lni.udl +++ b/bindings/lni_uniffi/src/lni.udl @@ -3,6 +3,9 @@ namespace lni {}; dictionary PhoenixdConfig { string url; string password; + string? socks5_proxy; + boolean? accept_invalid_certs; + i64? http_timeout; }; interface PhoenixdNode { @@ -34,6 +37,9 @@ interface PhoenixdNode { dictionary ClnConfig { string url; string rune; + string? socks5_proxy; + boolean? accept_invalid_certs; + i64? http_timeout; }; interface ClnNode { @@ -71,6 +77,9 @@ interface ClnNode { dictionary LndConfig { string url; string macaroon; + string? socks5_proxy; + boolean? accept_invalid_certs; + i64? http_timeout; }; interface LndNode { diff --git a/crates/lni/Cargo.toml b/crates/lni/Cargo.toml index 594c3a9..dea6fdb 100644 --- a/crates/lni/Cargo.toml +++ b/crates/lni/Cargo.toml @@ -8,10 +8,11 @@ name = "lni" path = "lib.rs" [dependencies] -reqwest = { version = "0.10", default-features = false, features = [ +reqwest = { version = "0.12", default-features = false, features = [ "json", "rustls-tls", "blocking", + "socks", ] } async-trait = "0.1" thiserror = "1.0" diff --git a/crates/lni/cln/api.rs b/crates/lni/cln/api.rs index f2c9dbb..5e20077 100644 --- a/crates/lni/cln/api.rs +++ b/crates/lni/cln/api.rs @@ -2,6 +2,7 @@ use super::types::{ Bolt11Resp, Bolt12Resp, ChannelWrapper, FetchInvoiceResponse, InfoResponse, InvoicesResponse, ListOffersResponse, PayResponse, }; +use super::ClnConfig; use crate::types::NodeInfo; use crate::{ calculate_fee_msats, ApiError, InvoiceType, PayCode, PayInvoiceParams, PayInvoiceResponse, @@ -11,10 +12,29 @@ use reqwest::header; // https://docs.corelightning.org/reference/get_list_methods_resource -pub fn get_info(url: String, rune: String) -> Result { - let req_url = format!("{}/v1/getinfo", url); - println!("Constructed URL: {} rune {}", req_url, rune); - let client = clnrest_client(rune.clone()); +fn clnrest_client(config: &ClnConfig) -> reqwest::blocking::Client { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert("Rune", header::HeaderValue::from_str(&config.rune).unwrap()); + let mut client = reqwest::blocking::ClientBuilder::new().default_headers(headers); + if config.socks5_proxy.is_some() { + let proxy = reqwest::Proxy::all(&config.socks5_proxy.clone().unwrap_or_default()).unwrap(); + client = client.proxy(proxy); + } + if config.accept_invalid_certs.is_some() { + client = client.danger_accept_invalid_certs(true); + } + if config.http_timeout.is_some() { + client = client.timeout(std::time::Duration::from_secs( + config.http_timeout.unwrap_or_default() as u64, + )); + } + client.build().unwrap() +} + +pub async fn get_info(config: &ClnConfig) -> Result { + let req_url = format!("{}/v1/getinfo", config.url); + println!("Constructed URL: {} rune {}", req_url, config.rune); + let client = clnrest_client(config); let response = client .post(&req_url) .header("Content-Type", "application/json") @@ -25,7 +45,7 @@ pub fn get_info(url: String, rune: String) -> Result { let info: InfoResponse = serde_json::from_str(&response_text)?; // https://github.com/ZeusLN/zeus/blob/master/backends/CoreLightningRequestHandler.ts#L28 - let funds_url = format!("{}/v1/listfunds", url); + let funds_url = format!("{}/v1/listfunds", config.url); let funds_response = client .post(&funds_url) .header("Content-Type", "application/json") @@ -51,12 +71,13 @@ pub fn get_info(url: String, rune: String) -> Result { // Unsettled channels (previously inactive) unsettled_send_balance_msat += channel.our_amount_msat; unsettled_receive_balance_msat += channel.amount_msat - channel.our_amount_msat; - } else if channel.state == "CHANNELD_AWAITING_LOCKIN" + } else if channel.state == "CHANNELD_AWAITING_LOCKIN" || channel.state == "DUALOPEND_AWAITING_LOCKIN" || channel.state == "DUALOPEND_OPEN_INIT" || channel.state == "DUALOPEND_OPEN_COMMITTED" || channel.state == "DUALOPEND_OPEN_COMMIT_READY" - || channel.state == "OPENINGD" { + || channel.state == "OPENINGD" + { // Pending open channels pending_open_send_balance += channel.our_amount_msat; pending_open_receive_balance += channel.amount_msat - channel.our_amount_msat; @@ -83,8 +104,7 @@ pub fn get_info(url: String, rune: String) -> Result { // invoice - amount_msat label description expiry fallbacks preimage exposeprivatechannels cltv pub async fn create_invoice( - url: String, - rune: String, + config: &ClnConfig, invoice_type: InvoiceType, amount_msats: Option, offer: Option, @@ -92,7 +112,7 @@ pub async fn create_invoice( description_hash: Option, expiry: Option, ) -> Result { - let client = clnrest_client(rune.clone()); + let client = clnrest_client(config); let amount_msat_str: String = amount_msats.map_or("any".to_string(), |amt| amt.to_string()); let mut params: Vec<(&str, Option)> = vec![]; params.push(( @@ -107,8 +127,8 @@ pub async fn create_invoice( )); match invoice_type { InvoiceType::Bolt11 => { - let req_url = format!("{}/v1/invoice", url); - let response: reqwest::blocking::Response = client + let req_url = format!("{}/v1/invoice", config.url); + let response = client .post(&req_url) .header("Content-Type", "application/json") .json(&serde_json::json!(params @@ -150,8 +170,7 @@ pub async fn create_invoice( }); } let fetch_invoice_resp = fetch_invoice_from_offer( - url.clone(), - rune.clone(), + config, offer.clone().unwrap(), amount_msats.unwrap_or(0), // TODO make this optional if the lno already has amount in it Some(description.clone().unwrap_or_default()), @@ -178,12 +197,11 @@ pub async fn create_invoice( } pub async fn pay_invoice( - url: String, - rune: String, + config: &ClnConfig, invoice_params: PayInvoiceParams, ) -> Result { - let client = clnrest_client(rune.clone()); - let pay_url = format!("{}/v1/pay", url); + let client = clnrest_client(config); + let pay_url = format!("{}/v1/pay", config.url); let mut params: Vec<(&str, Option)> = vec![]; params.push(( @@ -235,7 +253,7 @@ pub async fn pay_invoice( println!("PayInvoice params: {:?}", ¶ms_json); - let pay_response: reqwest::blocking::Response = client + let pay_response = client .post(&pay_url) .header("Content-Type", "application/json") .json(¶ms_json) @@ -260,10 +278,10 @@ pub async fn pay_invoice( } // decode - bolt11 invoice (lnbc) bolt12 invoice (lni) or bolt12 offer (lno) -pub async fn decode(url: String, rune: String, str: String) -> Result { - let client = clnrest_client(rune); - let req_url = format!("{}/v1/decode", url); - let response: reqwest::blocking::Response = client +pub async fn decode(config: &ClnConfig, str: String) -> Result { + let client = clnrest_client(config); + let req_url = format!("{}/v1/decode", config.url); + let response = client .post(&req_url) .header("Content-Type", "application/json") .json(&serde_json::json!({ @@ -277,12 +295,8 @@ pub async fn decode(url: String, rune: String, str: String) -> Result, -) -> Result { - let offers = list_offers(url.clone(), rune.clone(), search.clone()).await?; +pub async fn get_offer(config: &ClnConfig, search: Option) -> Result { + let offers = list_offers(config, search.clone()).await?; if offers.is_empty() { return Ok(PayCode { offer_id: "".to_string(), @@ -297,17 +311,16 @@ pub async fn get_offer( } pub async fn list_offers( - url: String, - rune: String, + config: &ClnConfig, search: Option, ) -> Result, ApiError> { - let client = clnrest_client(rune); - let req_url = format!("{}/v1/listoffers", url); + let client = clnrest_client(config); + let req_url = format!("{}/v1/listoffers", config.url); let mut params = vec![]; if let Some(search) = search { params.push(("offer_id", Some(search))) } - let response: reqwest::blocking::Response = client + let response = client .post(&req_url) .header("Content-Type", "application/json") .json(&serde_json::json!(params @@ -326,14 +339,13 @@ pub async fn list_offers( } pub async fn create_offer( - url: String, - rune: String, + config: &ClnConfig, amount_msats: Option, description: Option, expiry: Option, ) -> Result { - let client = clnrest_client(rune); - let req_url = format!("{}/v1/offer", url); + let client = clnrest_client(config); + let req_url = format!("{}/v1/offer", config.url); let mut params: Vec<(&str, Option)> = vec![]; if let Some(amount_msats) = amount_msats { params.push(("amount", Some(format!("{}msat", amount_msats)))) @@ -344,7 +356,7 @@ pub async fn create_offer( if let Some(description) = description_clone { params.push(("description", Some(description))) } - let response: reqwest::blocking::Response = client + let response = client .post(&req_url) .header("Content-Type", "application/json") .json(&serde_json::json!(params @@ -377,15 +389,14 @@ pub async fn create_offer( } pub async fn fetch_invoice_from_offer( - url: String, - rune: String, + config: &ClnConfig, offer: String, amount_msats: i64, // TODO make optional if the lno already has amount in it payer_note: Option, ) -> Result { - let fetch_invoice_url = format!("{}/v1/fetchinvoice", url); - let client = clnrest_client(rune); - let response: reqwest::blocking::Response = client + let fetch_invoice_url = format!("{}/v1/fetchinvoice", config.url); + let client = clnrest_client(config); + let response = client .post(&fetch_invoice_url) .header("Content-Type", "application/json") .json(&serde_json::json!({ @@ -413,22 +424,16 @@ pub async fn fetch_invoice_from_offer( } pub async fn pay_offer( - url: String, - rune: String, + config: &ClnConfig, offer: String, amount_msats: i64, payer_note: Option, ) -> Result { - let client = clnrest_client(rune.clone()); - let fetch_invoice_resp = fetch_invoice_from_offer( - url.clone(), - rune.clone(), - offer.clone(), - amount_msats, - payer_note.clone(), - ) - .await - .unwrap(); + let client = clnrest_client(config); + let fetch_invoice_resp = + fetch_invoice_from_offer(config, offer.clone(), amount_msats, payer_note.clone()) + .await + .unwrap(); if (fetch_invoice_resp.invoice.is_empty()) { return Err(ApiError::Json { reason: "Missing BOLT 12 invoice".to_string(), @@ -436,8 +441,8 @@ pub async fn pay_offer( } // now pay the bolt 12 invoice lni - let pay_url = format!("{}/v1/pay", url); - let pay_response: reqwest::blocking::Response = client + let pay_url = format!("{}/v1/pay", config.url); + let pay_response = client .post(&pay_url) .header("Content-Type", "application/json") .json(&serde_json::json!({ @@ -465,14 +470,13 @@ pub async fn pay_offer( }) } -pub fn lookup_invoice( - url: String, - rune: String, +pub async fn lookup_invoice( + config: &ClnConfig, payment_hash: Option, from: Option, limit: Option, ) -> Result { - match lookup_invoices(url, rune, payment_hash, from, limit) { + match lookup_invoices(config, payment_hash, from, limit).await { Ok(transactions) => { if let Some(tx) = transactions.first() { Ok(tx.clone()) @@ -487,15 +491,14 @@ pub fn lookup_invoice( } // label, invstring, payment_hash, offer_id, index, start, limit -fn lookup_invoices( - url: String, - rune: String, +async fn lookup_invoices( + config: &ClnConfig, payment_hash: Option, from: Option, limit: Option, ) -> Result, ApiError> { - let list_invoices_url = format!("{}/v1/listinvoices", url); - let client = clnrest_client(rune); + let list_invoices_url = format!("{}/v1/listinvoices", config.url); + let client = clnrest_client(config); // 1) Build query for incoming transactions let mut params: Vec<(&str, Option)> = vec![]; @@ -511,7 +514,7 @@ fn lookup_invoices( } // Fetch incoming transactions - let response: reqwest::blocking::Response = client + let response = client .post(&list_invoices_url) .header("Content-Type", "application/json") //.json(&serde_json::json!(params)) @@ -554,24 +557,13 @@ fn lookup_invoices( Ok(transactions) } -pub fn list_transactions( - url: String, - rune: String, +pub async fn list_transactions( + config: &ClnConfig, from: i64, limit: i64, ) -> Result, ApiError> { - match lookup_invoices(url, rune, None, Some(from), Some(limit)) { + match lookup_invoices(config, None, Some(from), Some(limit)).await { Ok(transactions) => Ok(transactions), Err(e) => Err(e), } } - -fn clnrest_client(rune: String) -> reqwest::blocking::Client { - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert("Rune", header::HeaderValue::from_str(&rune).unwrap()); - reqwest::blocking::ClientBuilder::new() - .danger_accept_invalid_certs(true) - .default_headers(headers) - .build() - .unwrap() -} diff --git a/crates/lni/cln/lib.rs b/crates/lni/cln/lib.rs index 1ecb63b..47257a8 100644 --- a/crates/lni/cln/lib.rs +++ b/crates/lni/cln/lib.rs @@ -8,27 +8,38 @@ use crate::{ }; #[cfg_attr(feature = "napi_rs", napi(object))] +#[derive(Debug, Clone)] pub struct ClnConfig { pub url: String, pub rune: String, + pub socks5_proxy: Option, // socks5h://127.0.0.1:9150 + pub accept_invalid_certs: Option, + pub http_timeout: Option, +} +impl Default for ClnConfig { + fn default() -> Self { + Self { + url: "https://127.0.0.1:8080".to_string(), + rune: "".to_string(), + socks5_proxy: None, + accept_invalid_certs: Some(true), + http_timeout: Some(60), + } + } } #[cfg_attr(feature = "napi_rs", napi(object))] pub struct ClnNode { - pub url: String, - pub rune: String, + pub config: ClnConfig, } impl ClnNode { pub fn new(config: ClnConfig) -> Self { - Self { - url: config.url, - rune: config.rune, - } + Self { config } } pub async fn get_info(&self) -> Result { - crate::cln::api::get_info(self.url.clone(), self.rune.clone()) + crate::cln::api::get_info(&self.config).await } pub async fn create_invoice( @@ -36,8 +47,7 @@ impl ClnNode { params: CreateInvoiceParams, ) -> Result { crate::cln::api::create_invoice( - self.url.clone(), - self.rune.clone(), + &self.config, params.invoice_type, params.amount_msats, params.offer.clone(), @@ -52,15 +62,15 @@ impl ClnNode { &self, params: PayInvoiceParams, ) -> Result { - crate::cln::api::pay_invoice(self.url.clone(), self.rune.clone(), params).await + crate::cln::api::pay_invoice(&self.config, params).await } pub async fn get_offer(&self, search: Option) -> Result { - crate::cln::api::get_offer(self.url.clone(), self.rune.clone(), search).await + crate::cln::api::get_offer(&self.config, search).await } pub async fn list_offers(&self, search: Option) -> Result, ApiError> { - crate::cln::api::list_offers(self.url.clone(), self.rune.clone(), search).await + crate::cln::api::list_offers(&self.config, search).await } pub async fn pay_offer( @@ -69,43 +79,25 @@ impl ClnNode { amount_msats: i64, payer_note: Option, ) -> Result { - crate::cln::api::pay_offer( - self.url.clone(), - self.rune.clone(), - offer, - amount_msats, - payer_note, - ) - .await + crate::cln::api::pay_offer(&self.config, offer, amount_msats, payer_note).await } pub async fn lookup_invoice( &self, payment_hash: String, ) -> Result { - crate::cln::api::lookup_invoice( - self.url.clone(), - self.rune.clone(), - Some(payment_hash), - None, - None, - ) + crate::cln::api::lookup_invoice(&self.config, Some(payment_hash), None, None).await } pub async fn list_transactions( &self, params: ListTransactionsParams, ) -> Result, ApiError> { - crate::cln::api::list_transactions( - self.url.clone(), - self.rune.clone(), - params.from, - params.limit, - ) + crate::cln::api::list_transactions(&self.config, params.from, params.limit).await } pub async fn decode(&self, str: String) -> Result { - crate::cln::api::decode(self.url.clone(), self.rune.clone(), str).await + crate::cln::api::decode(&self.config, str).await } } @@ -140,6 +132,9 @@ mod tests { ClnNode::new(ClnConfig { url: URL.clone(), rune: RUNE.clone(), + // socks5_proxy: Some("socks5h://127.0.0.1:9150".to_string()), + // accept_invalid_certs: Some(true) + ..Default::default() }) }; } diff --git a/crates/lni/lnd/api.rs b/crates/lni/lnd/api.rs index 2b580e1..7977aa2 100644 --- a/crates/lni/lnd/api.rs +++ b/crates/lni/lnd/api.rs @@ -2,6 +2,7 @@ use super::types::{ BalancesResponse, Bolt11Resp, FetchInvoiceResponse, GetInfoResponse, ListInvoiceResponse, ListInvoiceResponseWrapper, LndPayInvoiceResponseWrapper, }; +use super::LndConfig; use crate::types::NodeInfo; use crate::{ calculate_fee_msats, ApiError, CreateInvoiceParams, InvoiceType, PayCode, PayInvoiceParams, @@ -12,27 +13,31 @@ use reqwest::header; // Docs // https://lightning.engineering/api-docs/api/lnd/rest-endpoints/ -fn client(macaroon: String) -> reqwest::blocking::Client { +fn client(config: &LndConfig) -> reqwest::blocking::Client { let mut headers = reqwest::header::HeaderMap::new(); headers.insert( "Grpc-Metadata-macaroon", - header::HeaderValue::from_str(&macaroon).unwrap(), + header::HeaderValue::from_str(&config.macaroon).unwrap(), ); - - // TODO Tor proxy - // let proxy = reqwest::Proxy::all("socks5h://127.0.0.1:9050").unwrap(); - - reqwest::blocking::ClientBuilder::new() - .danger_accept_invalid_certs(true) - .default_headers(headers) - //.proxy(proxy) - .build() - .unwrap() + let mut client = reqwest::blocking::ClientBuilder::new().default_headers(headers); + if config.socks5_proxy.is_some() { + let proxy = reqwest::Proxy::all(&config.socks5_proxy.clone().unwrap_or_default()).unwrap(); + client = client.proxy(proxy); + } + if config.accept_invalid_certs.is_some() { + client = client.danger_accept_invalid_certs(true); + } + if config.http_timeout.is_some() { + client = client.timeout(std::time::Duration::from_secs( + config.http_timeout.unwrap_or_default() as u64, + )); + } + client.build().unwrap() } -pub fn get_info(url: String, macaroon: String) -> Result { - let req_url = format!("{}/v1/getinfo", url); - let client = client(macaroon.clone()); +pub async fn get_info(config: &LndConfig) -> Result { + let req_url = format!("{}/v1/getinfo", config.url); + let client = client(config); let response = client.get(&req_url).send().unwrap(); let response_text = response.text().unwrap(); let response_text = response_text.as_str(); @@ -42,7 +47,7 @@ pub fn get_info(url: String, macaroon: String) -> Result { // /v1/balance/channels // https://lightning.engineering/api-docs/api/lnd/lightning/channel-balance/ // send_balance_msats, receive_balance_msats, pending_balance, inactive_balance - let balance_url = format!("{}/v1/balance/channels", url); + let balance_url = format!("{}/v1/balance/channels", config.url); let balance_response = client.get(&balance_url).send().unwrap(); let balance_response_text = balance_response.text().unwrap(); let balance_response_text = balance_response_text.as_str(); @@ -97,18 +102,17 @@ pub fn get_info(url: String, macaroon: String) -> Result { } pub async fn create_invoice( - url: String, - macaroon: String, + config: &LndConfig, invoice_params: CreateInvoiceParams, ) -> Result { - let client = client(macaroon.clone()); + let client = client(config); let amount_msat_str: String = invoice_params .amount_msats .map_or("any".to_string(), |amt| amt.to_string()); match invoice_params.invoice_type { InvoiceType::Bolt11 => { - let req_url = format!("{}/v1/invoices", url); - let response: reqwest::blocking::Response = client + let req_url = format!("{}/v1/invoices", config.url); + let response = client .post(&req_url) .json(&serde_json::json!({ "memo": invoice_params.description, @@ -157,11 +161,10 @@ pub async fn create_invoice( } pub async fn pay_invoice( - url: String, - macaroon: String, + config: &LndConfig, invoice_params: PayInvoiceParams, ) -> Result { - let client = client(macaroon.clone()); + let client = client(config); let mut params: Vec<(&str, Option)> = vec![]; params.push(( "payment_request", @@ -240,9 +243,8 @@ pub async fn pay_invoice( println!("PayInvoice params: {:?}", ¶ms_json); - let req_url = format!("{}/v2/router/send", url); - let response: reqwest::blocking::Response = - client.post(&req_url).json(¶ms_json).send().unwrap(); + let req_url = format!("{}/v2/router/send", config.url); + let response = client.post(&req_url).json(¶ms_json).send().unwrap(); println!("Status: {}", response.status()); let invoice_str = response.text().unwrap(); @@ -277,10 +279,10 @@ pub async fn pay_invoice( } // decode - bolt11 invoice (lnbc) TODO decode: bolt12 invoice (lni) or bolt12 offer (lno) -pub async fn decode(url: String, macaroon: String, str: String) -> Result { - let client = client(macaroon); - let req_url = format!("{}/v1/payreq/{}", url, str); - let response: reqwest::blocking::Response = client.get(&req_url).send().unwrap(); +pub async fn decode(config: &LndConfig, str: String) -> Result { + let client = client(config); + let req_url = format!("{}/v1/payreq/{}", config.url, str); + let response = client.get(&req_url).send().unwrap(); // TODO parse JSON response let decoded = response.text().unwrap(); let decoded = decoded.as_str(); @@ -288,19 +290,14 @@ pub async fn decode(url: String, macaroon: String, str: String) -> Result, -) -> Result { +pub async fn get_offer(config: &LndConfig, search: Option) -> Result { return Err(ApiError::Json { reason: "Bolt12 not implemented".to_string(), }); } pub async fn list_offers( - url: String, - macaroon: String, + config: &LndConfig, search: Option, ) -> Result, ApiError> { return Err(ApiError::Json { @@ -309,8 +306,7 @@ pub async fn list_offers( } pub async fn create_offer( - url: String, - macaroon: String, + config: &LndConfig, amount_msats: Option, description: Option, expiry: Option, @@ -321,8 +317,7 @@ pub async fn create_offer( } pub async fn fetch_invoice_from_offer( - url: String, - macaroon: String, + config: &LndConfig, offer: String, amount_msats: i64, // TODO make optional if the lno already has amount in it payer_note: Option, @@ -333,8 +328,7 @@ pub async fn fetch_invoice_from_offer( } pub async fn pay_offer( - url: String, - macaroon: String, + config: &LndConfig, offer: String, amount_msats: i64, payer_note: Option, @@ -344,17 +338,16 @@ pub async fn pay_offer( }); } -pub fn lookup_invoice( - url: String, - macaroon: String, +pub async fn lookup_invoice( + config: &LndConfig, payment_hash: Option, ) -> Result { let payment_hash_str = payment_hash.unwrap_or_default(); - let list_invoices_url = format!("{}/v1/invoice/{}", url, payment_hash_str); + let list_invoices_url = format!("{}/v1/invoice/{}", config.url, payment_hash_str); println!("list_invoices_url {}", &list_invoices_url); - let client = client(macaroon); + let client = client(config); // Fetch incoming transactions - let response: reqwest::blocking::Response = client.get(&list_invoices_url).send().unwrap(); + let response = client.get(&list_invoices_url).send().unwrap(); let status = response.status(); if status == reqwest::StatusCode::NOT_FOUND { return Err(ApiError::Json { @@ -402,20 +395,19 @@ pub fn lookup_invoice( }) } -pub fn list_transactions( - url: String, - macaroon: String, +pub async fn list_transactions( + config: &LndConfig, from: i64, limit: i64, ) -> Result, ApiError> { let list_txns_url = format!( "{}/v1/invoices?index_offest={}&num_max_invoices={}", - url, from, limit + config.url, from, limit ); - let client = client(macaroon); + let client = client(config); // Fetch incoming transactions - let response: reqwest::blocking::Response = client.get(&list_txns_url).send().unwrap(); + let response = client.get(&list_txns_url).send().unwrap(); let response_text = response.text().unwrap(); let response_text = response_text.as_str(); let txns: ListInvoiceResponseWrapper = serde_json::from_str(&response_text).unwrap(); diff --git a/crates/lni/lnd/lib.rs b/crates/lni/lnd/lib.rs index cd152b2..eb5d736 100644 --- a/crates/lni/lnd/lib.rs +++ b/crates/lni/lnd/lib.rs @@ -8,49 +8,60 @@ use crate::{ }; #[cfg_attr(feature = "napi_rs", napi(object))] +#[derive(Debug, Clone)] pub struct LndConfig { pub url: String, pub macaroon: String, + pub socks5_proxy: Option, // socks5h://127.0.0.1:9150 + pub accept_invalid_certs: Option, + pub http_timeout: Option, +} +impl Default for LndConfig { + fn default() -> Self { + Self { + url: "https://127.0.0.1:8080".to_string(), + macaroon: "".to_string(), + socks5_proxy: None, + accept_invalid_certs: Some(true), + http_timeout: Some(60), + } + } } #[cfg_attr(feature = "napi_rs", napi(object))] pub struct LndNode { - pub url: String, - pub macaroon: String, + pub config: LndConfig, } impl LndNode { pub fn new(config: LndConfig) -> Self { - Self { - url: config.url, - macaroon: config.macaroon, - } + Self { config } } pub async fn get_info(&self) -> Result { - crate::lnd::api::get_info(self.url.clone(), self.macaroon.clone()) + crate::lnd::api::get_info(&self.config).await } pub async fn create_invoice( &self, params: CreateInvoiceParams, ) -> Result { - crate::lnd::api::create_invoice(self.url.clone(), self.macaroon.clone(), params).await + crate::lnd::api::create_invoice(&self.config, params).await } pub async fn pay_invoice( &self, params: PayInvoiceParams, ) -> Result { - crate::lnd::api::pay_invoice(self.url.clone(), self.macaroon.clone(), params).await + crate::lnd::api::pay_invoice(&self.config, params).await } pub async fn get_offer(&self, search: Option) -> Result { - crate::lnd::api::get_offer(self.url.clone(), self.macaroon.clone(), search).await + crate::lnd::api::get_offer(&self.config, search).await } pub async fn list_offers(&self, search: Option) -> Result, ApiError> { - crate::lnd::api::list_offers(self.url.clone(), self.macaroon.clone(), search).await + crate::lnd::api::list_offers(&self.config, search).await } pub async fn pay_offer( @@ -59,37 +70,25 @@ impl LndNode { amount_msats: i64, payer_note: Option, ) -> Result { - crate::lnd::api::pay_offer( - self.url.clone(), - self.macaroon.clone(), - offer, - amount_msats, - payer_note, - ) - .await + crate::lnd::api::pay_offer(&self.config, offer, amount_msats, payer_note).await } pub async fn lookup_invoice( &self, payment_hash: String, ) -> Result { - crate::lnd::api::lookup_invoice(self.url.clone(), self.macaroon.clone(), Some(payment_hash)) + crate::lnd::api::lookup_invoice(&self.config, Some(payment_hash)).await } pub async fn list_transactions( &self, params: ListTransactionsParams, ) -> Result, ApiError> { - crate::lnd::api::list_transactions( - self.url.clone(), - self.macaroon.clone(), - params.from, - params.limit, - ) + crate::lnd::api::list_transactions(&self.config, params.from, params.limit).await } pub async fn decode(&self, str: String) -> Result { - crate::lnd::api::decode(self.url.clone(), self.macaroon.clone(), str).await + crate::lnd::api::decode(&self.config, str).await } } @@ -130,6 +129,9 @@ mod tests { LndNode::new(LndConfig { url: URL.clone(), macaroon: macaroon.clone(), + socks5_proxy: Some("socks5h://127.0.0.1:9150".to_string()), // Tor socks5 proxy using arti + accept_invalid_certs: Some(true), + ..Default::default() }) }; } @@ -226,15 +228,21 @@ mod tests { #[test] async fn test_pay_invoice() { - match NODE.pay_invoice(PayInvoiceParams{ - invoice: "".to_string(), // TODO remote grab a invoice maybe from LNURL - fee_limit_percentage: Some(1.0), // 1% fee limit - allow_self_payment: Some(true), - ..Default::default() - }).await { + match NODE + .pay_invoice(PayInvoiceParams { + invoice: "".to_string(), // TODO remote grab a invoice maybe from LNURL + fee_limit_percentage: Some(1.0), // 1% fee limit + allow_self_payment: Some(true), + ..Default::default() + }) + .await + { Ok(invoice_resp) => { // println!("Pay invoice resp: {:?}", invoice_resp); - assert!(!invoice_resp.payment_hash.is_empty(), "Payment Hash should not be empty"); + assert!( + !invoice_resp.payment_hash.is_empty(), + "Payment Hash should not be empty" + ); } Err(e) => { panic!("Failed to pay invoice: {:?}", e); diff --git a/crates/lni/phoenixd/api.rs b/crates/lni/phoenixd/api.rs index 44ce9dc..2adc7f2 100644 --- a/crates/lni/phoenixd/api.rs +++ b/crates/lni/phoenixd/api.rs @@ -2,8 +2,10 @@ use super::types::{ Bolt11Req, Bolt11Resp, InfoResponse, InvoiceResponse, OutgoingPaymentResponse, PayResponse, PhoenixPayInvoiceResp, }; +use super::PhoenixdConfig; use crate::{ - phoenixd::types::GetBalanceResponse, ApiError, InvoiceType, NodeInfo, PayCode, PayInvoiceParams, PayInvoiceResponse, Transaction + phoenixd::types::GetBalanceResponse, ApiError, InvoiceType, NodeInfo, PayCode, + PayInvoiceParams, PayInvoiceResponse, Transaction, }; use serde_urlencoded; @@ -13,19 +15,46 @@ use serde_urlencoded; // https://phoenix.acinq.co/server/api -pub fn get_info(url: String, password: String) -> Result { - let info_url = format!("{}/getinfo", url); - let client: reqwest::blocking::Client = reqwest::blocking::Client::new(); +fn client(config: &PhoenixdConfig) -> reqwest::blocking::Client { + let mut client = reqwest::blocking::ClientBuilder::new(); + if config.socks5_proxy.is_some() { + let proxy = reqwest::Proxy::all(&config.socks5_proxy.clone().unwrap_or_default()).unwrap(); + client = client.proxy(proxy); + } + if config.accept_invalid_certs.is_some() { + client = client.danger_accept_invalid_certs(true); + } + if config.http_timeout.is_some() { + client = client.timeout(std::time::Duration::from_secs( + config.http_timeout.unwrap_or_default() as u64, + )); + } + client.build().unwrap() +} + +pub async fn get_info(config: &PhoenixdConfig) -> Result { + let info_url = format!("{}/getinfo", config.url); + let client = client(config); - let response: Result = client.get(&info_url).basic_auth("", Some(password.clone())).send(); - let response_text = response.unwrap().text().unwrap(); + let response = client + .get(&info_url) + .basic_auth("", Some(config.password.clone())) + .send() + .expect("Failed to get node info"); + let response_text = response.text().unwrap(); println!("get node info response: {}", response_text); let info: InfoResponse = serde_json::from_str(&response_text)?; // /getbalance - let balance_url = format!("{}/getbalance", url); - let balance_response: Result = client.get(&balance_url).basic_auth("", Some(password)).send(); - let balance_response_text = balance_response.unwrap().text().unwrap(); + let balance_url = format!("{}/getbalance", config.url); + let balance_response = client + .get(&balance_url) + .basic_auth("", Some(config.password.clone())) + .send() + .expect("Failed to get balance"); + let balance_response_text = balance_response + .text() + .expect("Failed to parse get balance"); println!("balance_response: {}", balance_response_text); let balance: GetBalanceResponse = serde_json::from_str(&balance_response_text)?; @@ -45,18 +74,17 @@ pub fn get_info(url: String, password: String) -> Result { } pub async fn create_invoice( - url: String, - password: String, + config: &PhoenixdConfig, invoice_type: InvoiceType, amount_msats: Option, description: Option, description_hash: Option, expiry: Option, ) -> Result { - let client = reqwest::blocking::Client::new(); + let client = client(config); match invoice_type { InvoiceType::Bolt11 => { - let req_url = format!("{}/createinvoice", url); + let req_url = format!("{}/createinvoice", config.url); let bolt11_req = Bolt11Req { description: description.clone(), @@ -66,16 +94,16 @@ pub async fn create_invoice( webhook_url: None, // TODO }; - let response: reqwest::blocking::Response = client + let response = client .post(&req_url) - .basic_auth("", Some(password)) + .basic_auth("", Some(config.password.clone())) .form(&bolt11_req) .send() - .unwrap(); + .expect("Failed to create invoice"); println!("Status: {}", response.status()); - let invoice_str = response.text().unwrap(); + let invoice_str = response.text().expect("Failed to parse get invoice"); let invoice_str = invoice_str.as_str(); println!("Bolt11 {}", &invoice_str.to_string()); @@ -109,12 +137,11 @@ pub async fn create_invoice( } pub async fn pay_invoice( - url: String, - password: String, + config: &PhoenixdConfig, invoice_params: PayInvoiceParams, ) -> Result { - let client = reqwest::blocking::Client::new(); - let req_url = format!("{}/payinvoice", url); + let client = client(config); + let req_url = format!("{}/payinvoice", config.url); let mut params = vec![]; if invoice_params.amount_msats.is_some() { params.push(( @@ -123,14 +150,14 @@ pub async fn pay_invoice( )); } params.push(("invoice", Some(invoice_params.invoice.to_string()))); - let response: reqwest::blocking::Response = client + let response = client .post(&req_url) - .basic_auth("", Some(password)) + .basic_auth("", Some(config.password.clone())) .form(¶ms) .send() - .unwrap(); + .expect("Failed to pay invoice"); println!("Status: {}", response.status()); - let response_text = response.text().unwrap(); + let response_text = response.text().expect("Failed to parse pay invoice"); let pay_invoice_resp: PhoenixPayInvoiceResp = serde_json::from_str(&response_text).map_err(|e| ApiError::Json { reason: format!("Failed to parse pay_invoice response: {}", e), @@ -152,15 +179,15 @@ pub async fn decode(str: String) -> Result { // TODO On Phoenixd there is not currenly a way to create a new BOLT 12 offer // Get latest BOLT12 offer -pub async fn get_offer(url: String, password: String) -> Result { - let req_url = format!("{}/getoffer", url); - let client = reqwest::blocking::Client::new(); - let response: reqwest::blocking::Response = client +pub async fn get_offer(config: &PhoenixdConfig) -> Result { + let req_url = format!("{}/getoffer", config.url); + let client = client(config); + let response = client .get(&req_url) - .basic_auth("", Some(password)) + .basic_auth("", Some(config.password.clone())) .send() - .unwrap(); - let offer_str = response.text().unwrap(); + .expect("Failed to get offer"); + let offer_str = response.text().expect("Failed to parse get offer"); Ok(PayCode { offer_id: "".to_string(), bolt12: offer_str.to_string(), @@ -172,25 +199,24 @@ pub async fn get_offer(url: String, password: String) -> Result, ) -> Result { - let client = reqwest::blocking::Client::new(); - let req_url = format!("{}/payoffer", url); - let response: reqwest::blocking::Response = client + let req_url = format!("{}/payoffer", config.url); + let client = client(config); + let response = client .post(&req_url) - .basic_auth("", Some(password)) + .basic_auth("", Some(config.password.clone())) .form(&[ ("amountSat", (amount_msats / 1000).to_string()), ("offer", offer), ("message", payer_note.unwrap_or_default()), ]) .send() - .unwrap(); - let response_text = response.text().unwrap(); + .expect("Failed to pay offer"); + let response_text = response.text().expect("Failed to parse pay offer"); let response_text = response_text.as_str(); let pay_resp: PayResponse = match serde_json::from_str(&response_text) { Ok(resp) => resp, @@ -210,15 +236,18 @@ pub async fn pay_offer( // TODO implement list_offers, currently just one is returned by Phoenixd pub async fn list_offers() {} -pub fn lookup_invoice( - url: String, - password: String, +pub async fn lookup_invoice( + config: &PhoenixdConfig, payment_hash: String, ) -> Result { - let url = format!("{}/payments/incoming/{}", url, payment_hash); - let client: reqwest::blocking::Client = reqwest::blocking::Client::new(); - let response = client.get(&url).basic_auth("", Some(password)).send(); - let response_text = response.unwrap().text().unwrap(); + let url = format!("{}/payments/incoming/{}", config.url, payment_hash); + let client = client(config); + let response = client + .get(&url) + .basic_auth("", Some(config.password.clone())) + .send() + .expect("failed to lookup invoice"); + let response_text = response.text().expect("failed to parse lookup invoice"); let response_text = response_text.as_str(); let inv: InvoiceResponse = serde_json::from_str(&response_text)?; @@ -240,9 +269,8 @@ pub fn lookup_invoice( Ok(txn) } -pub fn list_transactions( - url: String, - password: String, +pub async fn list_transactions( + config: &PhoenixdConfig, from: i64, // until: i64, limit: i64, @@ -252,7 +280,7 @@ pub fn list_transactions( // invoice_type: Option, // not currently used but included for parity // search_term: Option, // not currently used but included for parity ) -> Result, ApiError> { - let client = reqwest::blocking::Client::new(); + let client = client(config); // 1) Build query for incoming transactions let mut incoming_params = vec![]; @@ -272,14 +300,17 @@ pub fn list_transactions( // Build the final incoming URL with query let incoming_query = serde_urlencoded::to_string(&incoming_params).unwrap(); - let incoming_url = format!("{}/payments/incoming?{}", url, incoming_query); + let incoming_url = format!("{}/payments/incoming?{}", config.url, incoming_query); // Fetch incoming transactions let incoming_resp = client .get(&incoming_url) - .basic_auth("", Some(password.clone())) - .send(); - let incoming_text = incoming_resp.unwrap().text().unwrap(); + .basic_auth("", Some(config.password.clone())) + .send() + .expect("Failed to get incoming payments"); + let incoming_text = incoming_resp + .text() + .expect("Failed to parse incoming payments"); let incoming_text = incoming_text.as_str(); let incoming_payments: Vec = serde_json::from_str(&incoming_text).unwrap(); @@ -329,14 +360,17 @@ pub fn list_transactions( // Build the final outgoing URL with query let outgoing_query = serde_urlencoded::to_string(&outgoing_params).unwrap(); - let outgoing_url = format!("{}/payments/outgoing?{}", url, outgoing_query); + let outgoing_url = format!("{}/payments/outgoing?{}", config.url, outgoing_query); // Fetch outgoing transactions let outgoing_resp = client .get(&outgoing_url) - .basic_auth("", Some(password)) - .send(); - let outgoing_text = outgoing_resp.unwrap().text().unwrap(); + .basic_auth("", Some(config.password.clone())) + .send() + .expect("Failed to get outgoing payments"); + let outgoing_text = outgoing_resp + .text() + .expect("failed to parse outgoing payments"); let outgoing_text = outgoing_text.as_str(); let outgoing_payments: Vec = serde_json::from_str(&outgoing_text).unwrap(); diff --git a/crates/lni/phoenixd/lib.rs b/crates/lni/phoenixd/lib.rs index 5cdccfd..a0da277 100644 --- a/crates/lni/phoenixd/lib.rs +++ b/crates/lni/phoenixd/lib.rs @@ -9,27 +9,38 @@ use crate::{ use crate::{CreateInvoiceParams, PayCode}; #[cfg_attr(feature = "napi_rs", napi(object))] +#[derive(Debug, Clone)] pub struct PhoenixdConfig { pub url: String, pub password: String, + pub socks5_proxy: Option, // socks5h://127.0.0.1:9150 + pub accept_invalid_certs: Option, + pub http_timeout: Option, +} +impl Default for PhoenixdConfig { + fn default() -> Self { + Self { + url: "https://127.0.0.1:8080".to_string(), + password: "".to_string(), + socks5_proxy: None, + accept_invalid_certs: Some(true), + http_timeout: Some(60), + } + } } #[cfg_attr(feature = "napi_rs", napi(object))] pub struct PhoenixdNode { - pub url: String, - pub password: String, + pub config: PhoenixdConfig, } impl PhoenixdNode { pub fn new(config: PhoenixdConfig) -> Self { - Self { - url: config.url, - password: config.password, - } + Self { config } } pub async fn get_info(&self) -> Result { - crate::phoenixd::api::get_info(self.url.clone(), self.password.clone()) + crate::phoenixd::api::get_info(&self.config).await } pub async fn create_invoice( @@ -37,8 +48,7 @@ impl PhoenixdNode { params: CreateInvoiceParams, ) -> Result { create_invoice( - self.url.clone(), - self.password.clone(), + &self.config, params.invoice_type, Some(params.amount_msats.unwrap_or_default()), params.description, @@ -52,11 +62,11 @@ impl PhoenixdNode { &self, params: PayInvoiceParams, ) -> Result { - pay_invoice(self.url.clone(), self.password.clone(), params).await + pay_invoice(&self.config, params).await } pub async fn get_offer(&self) -> Result { - crate::phoenixd::api::get_offer(self.url.clone(), self.password.clone()).await + crate::phoenixd::api::get_offer(&self.config).await } pub async fn pay_offer( @@ -65,34 +75,21 @@ impl PhoenixdNode { amount_msats: i64, payer_note: Option, ) -> Result { - crate::phoenixd::api::pay_offer( - self.url.clone(), - self.password.clone(), - offer, - amount_msats, - payer_note, - ) - .await + crate::phoenixd::api::pay_offer(&self.config, offer, amount_msats, payer_note).await } pub async fn lookup_invoice( &self, payment_hash: String, ) -> Result { - crate::phoenixd::api::lookup_invoice(self.url.clone(), self.password.clone(), payment_hash) + crate::phoenixd::api::lookup_invoice(&self.config, payment_hash).await } pub async fn list_transactions( &self, params: ListTransactionsParams, ) -> Result, ApiError> { - crate::phoenixd::api::list_transactions( - self.url.clone(), - self.password.clone(), - params.from, - params.limit, - None, - ) + crate::phoenixd::api::list_transactions(&self.config, params.from, params.limit, None).await } } @@ -119,6 +116,9 @@ mod tests { PhoenixdNode::new(PhoenixdConfig { url: URL.clone(), password: PASSWORD.clone(), + // socks5_proxy: "socks5h://127.0.0.1:9150".to_string().into(), + // accept_invalid_certs: true.into(), + ..Default::default() }) }; static ref TEST_PAYMENT_HASH: String = { @@ -182,7 +182,10 @@ mod tests { { Ok(txn) => { println!("txn: {:?}", txn); - assert!(!txn.payment_hash.is_empty(), "Payment hash should not be empty"); + assert!( + !txn.payment_hash.is_empty(), + "Payment hash should not be empty" + ); } Err(e) => { panic!("Failed to pay invoice: {:?}", e); diff --git a/readme.md b/readme.md index 28d2f87..906b546 100644 --- a/readme.md +++ b/readme.md @@ -1,66 +1,139 @@ -LNI - Lightning Node Interface -============================== +LNI Remote - Lightning Node Interface Remote +============================================ -LNI - Lightning Node Interface. Connect to the major lightning node implementations with a standard interface. +Remote connect to all the major lightning node implementations with a standard interface. -- Supports *CLN, *LND, *LNDK, *Phoenixd, *LNURL, *BOLT 11 and *BOLT 12 (WIP). +- Supports all major nodes - CLN, LND, Phoenixd, *LNDK, (WIP) +- Supports the main protocols - BOLT 11, BOLT 12, *LNURL and NWC +- Also popular REST apis - Strike - 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 logo -### Interface API +### Interface API Examples -#### LND +#### Rust ```rust -let lnd_node = LndNode::new("test_macaroon".to_string(), "https://127.0.0.1:8080".to_string()); -let lnd_result = lnd_node.pay_invoice("invoice".to_string()); -println!("Pay LND invoice result {}", lnd_result); -let lnd_txns = lnd_node.get_wallet_transactions("wallet_id".to_string()); -lnd_txns.iter().for_each(|txn| { - println!("LND Transaction amount: {}, date: {}, memo: {}", txn.amount(), txn.date(), txn.memo()); +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_macaroon = lnd_node.key(); + +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 ``` -#### CLN -```rust -let cln_node = ClnNode::new("test_rune".to_string(), "https://127.0.0.1:8081".to_string()); -let cln_result = cln_node.pay_invoice("invoice".to_string()); -println!("Pay CLN invoice result {}", cln_result); -let cln_txns = cln_node.get_wallet_transactions("wallet_id".to_string()); -cln_txns.iter().for_each(|txn| { - println!("CLN Transaction amount: {}, date: {}, memo: {}", txn.amount(), txn.date(), txn.memo()); +#### Typescript +```typescript +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, }); -let cln_rune = cln_node.key(); + +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); ``` #### Payments ```rust -lni.create_invoice(amount, expiration, memo, BOLT11 | BOLT12) -lni.pay_invoice() -lni.pay_offer(offer) -lni.fetch_invoice_from_offer('lno***') -lni.decode_invoice(invoice) -lni.check_invoice_status(invoice) +// BOLT 11 +node.create_invoice(CreateInvoiceParams) -> Result +node.pay_invoice(PayInvoiceParams) -> Result + +// BOLT 12 +node.get_offer(search: Option) -> Result // return the first offer or by search id +node.pay_offer(offer: String, amount_msats: i64, payer_note: Option) -> Result +node.list_offers(search: Option) -> Result, ApiError> + +// Lookup +node.decode(str: String) -> Result +node.lookup_invoice(payment_hash: String) -> Result +node.list_transactions(ListTransactionsParams) -> Result ``` #### Node Management -``` -lni.get_info() -lni.get_transactions(limit, skip) -lni.wallet_balance() +```rust +node.get_info() -> Result // returns NodeInfo and balances ``` #### Channel Management -``` -lni.fetch_channel_info() +```rust +// TODO - Not implemented +node.channel_info() ``` #### Event Polling -``` -await lni.on_invoice_events(invoice_id, (event) =>{ +```rust +// TODO - Not implemented +node.on_invoice_events(invoice_id, (event) =>{ console.log("Callback result:", result); }) ``` @@ -69,7 +142,7 @@ await lni.on_invoice_events(invoice_id, (event) =>{ Event Polling ============ LNI does some simple event polling over https to get some basic invoice status events. -Polling is used instead of a heavier grpc/pubsub (for now) event system to make sure the lib runs cross platform and stays lightweight. TODO websockets +Polling is used instead of a heavier grpc/pubsub (for now) event system to make sure the lib runs cross platform and stays lightweight. NOT YET IMPLEMENTED. TODO websockets Build ======= @@ -135,8 +208,8 @@ CLN_RUNE=YOUR_RUNE CLN_TEST_PAYMENT_HASH=YOUR_HASH ``` -Bindings -======== +Language Bindings +================= - nodejs - napi_rs @@ -194,7 +267,18 @@ pub struct PhoenixdNode { Tor === -Use Tor socks if connecting to a .onion hidden service by passing in socks5 proxy. (TODO WIP) +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 +```rust +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() +}) +``` Inspiration @@ -266,7 +350,8 @@ Todo - [X] uniffi bindings for Android and IOS - [X] react-native - uniffi-bindgen-react-native - [X] async promise architecture for bindings -- [ ] Tor Socks5 fetch https://tpo.pages.torproject.net/core/arti/guides/starting-arti +- [X] Tor Socks5 fetch https://tpo.pages.torproject.net/core/arti/guides/starting-arti +- [ ] Simple event polling - [ ] implement lightning nodes - [X] phoenixd - [X] cln