diff --git a/crates/lni/alby/api.rs b/crates/lni/alby/api.rs new file mode 100644 index 0000000..c01a9f7 --- /dev/null +++ b/crates/lni/alby/api.rs @@ -0,0 +1,439 @@ +use std::time::Duration; + +use super::types::{ + AlbyBalancesResponse, AlbyCreateInvoiceRequest, AlbyCreateInvoiceResponse, + AlbyInfoResponse, AlbyLookupInvoiceResponse, AlbyPayInvoiceRequest, + AlbyPaymentResponse, AlbyTransactionsResponse, +}; +use super::AlbyConfig; +use crate::types::NodeInfo; +use crate::{ + ApiError, CreateInvoiceParams, OnInvoiceEventCallback, OnInvoiceEventParams, PayCode, + PayInvoiceParams, PayInvoiceResponse, Transaction, +}; +use reqwest::header; + +// Docs +// Based on Alby Hub API patterns from wails handlers + +fn async_client(config: &AlbyConfig) -> reqwest::Client { + let mut headers = reqwest::header::HeaderMap::new(); + + // Add Authorization header + let auth_header = format!("Bearer {}", config.api_key); + headers.insert( + "Authorization", + header::HeaderValue::from_str(&auth_header).unwrap(), + ); + + // Add Alby-specific headers + headers.insert( + "AlbyHub-Name", + header::HeaderValue::from_str(&config.alby_hub_name).unwrap(), + ); + headers.insert( + "AlbyHub-Region", + header::HeaderValue::from_str(&config.alby_hub_region).unwrap(), + ); + + headers.insert( + "Content-Type", + header::HeaderValue::from_static("application/json"), + ); + + // Create HTTP client with optional SOCKS5 proxy following the same pattern as other implementations + if let Some(proxy_url) = config.socks5_proxy.clone() { + if !proxy_url.is_empty() { + let client_builder = reqwest::Client::builder() + .default_headers(headers.clone()) + .danger_accept_invalid_certs(true); + + match reqwest::Proxy::all(&proxy_url) { + Ok(proxy) => { + let mut builder = client_builder.proxy(proxy); + if config.http_timeout.is_some() { + builder = builder.timeout(std::time::Duration::from_secs( + config.http_timeout.unwrap_or_default() as u64, + )); + } + match builder.build() { + Ok(client) => return client, + Err(_) => {} // Fall through to default client creation + } + } + Err(_) => {} // Fall through to default client creation + } + } + } + + // Default client creation + let mut client_builder = reqwest::Client::builder().default_headers(headers); + if config.accept_invalid_certs.unwrap_or(false) { + client_builder = client_builder.danger_accept_invalid_certs(true); + } + if config.http_timeout.is_some() { + client_builder = client_builder.timeout(std::time::Duration::from_secs( + config.http_timeout.unwrap_or_default() as u64, + )); + } + client_builder.build().unwrap_or_else(|_| reqwest::Client::new()) +} + +fn get_base_url(config: &AlbyConfig) -> &str { + config.base_url.as_deref().unwrap_or("https://my.albyhub.com/api") +} + +pub async fn get_info(config: AlbyConfig) -> Result { + let client = async_client(&config); + + // Get node info from Alby Hub API + let info_response = client + .get(&format!("{}/info", get_base_url(&config))) + .send() + .await + .map_err(|e| ApiError::Http { + reason: e.to_string(), + })?; + + if !info_response.status().is_success() { + let status = info_response.status(); + let error_text = info_response.text().await.unwrap_or_default(); + return Err(ApiError::Http { + reason: format!("HTTP {} - {}", status, error_text), + }); + } + + let info: AlbyInfoResponse = info_response.json().await.map_err(|e| ApiError::Json { + reason: e.to_string(), + })?; + + // Get balance from Alby Hub API (this is optional, as balance might not be available) + let balance_response = client + .get(&format!("{}/balances", get_base_url(&config))) + .send() + .await; + + let balance_msat = match balance_response { + Ok(response) if response.status().is_success() => { + match response.json::().await { + Ok(balances) => balances.balance.unwrap_or(0) * 1000, // Convert sats to msats + Err(_) => 0, // If parsing fails, default to 0 + } + } + _ => 0, // If request fails, default to 0 + }; + + Ok(NodeInfo { + alias: info.node_alias.unwrap_or_else(|| "Alby Hub".to_string()), + color: "".to_string(), // Alby Hub doesn't provide color + pubkey: "".to_string(), // Alby Hub doesn't provide pubkey in info endpoint + network: info.network, + block_height: 0, // Alby Hub doesn't provide block height in info endpoint + block_hash: "".to_string(), // Alby Hub doesn't provide block hash in info endpoint + send_balance_msat: balance_msat, + receive_balance_msat: 0, // Alby Hub doesn't distinguish send/receive balance + ..Default::default() + }) +} + +pub async fn create_invoice( + config: AlbyConfig, + params: CreateInvoiceParams, +) -> Result { + let client = async_client(&config); + + let request = AlbyCreateInvoiceRequest { + amount: params.amount_msats.unwrap_or(0) / 1000, // Convert msat to sat + description: params.description.clone(), + expiry: params.expiry, + }; + + let response = client + .post(&format!("{}/invoices", get_base_url(&config))) + .json(&request) + .send() + .await + .map_err(|e| ApiError::Http { + reason: format!("Failed to create invoice: {}", e), + })?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(ApiError::Http { + reason: format!("HTTP {} - {}", status, error_text), + }); + } + + let create_response: AlbyCreateInvoiceResponse = response.json().await.map_err(|e| ApiError::Json { + reason: format!("Failed to parse create invoice response: {}", e), + })?; + + Ok(Transaction { + type_: "incoming".to_string(), + invoice: create_response.payment_request, + preimage: "".to_string(), + payment_hash: create_response.payment_hash, + amount_msats: create_response.amount * 1000, // Convert sat to msat + fees_paid: 0, + created_at: parse_timestamp(&create_response.created_at), + expires_at: parse_timestamp(&create_response.expires_at), + settled_at: 0, + description: create_response.description, + description_hash: "".to_string(), + payer_note: Some("".to_string()), + external_id: Some("".to_string()), + }) +} + +pub async fn pay_invoice( + config: AlbyConfig, + params: PayInvoiceParams, +) -> Result { + let client = async_client(&config); + + let request = AlbyPayInvoiceRequest { + invoice: params.invoice.clone(), + amount: params.amount_msats.map(|a| a / 1000), // Convert msat to sat if provided + }; + + let response = client + .post(&format!("{}/payments/{}", get_base_url(&config), params.invoice)) + .json(&request) + .send() + .await + .map_err(|e| ApiError::Http { + reason: format!("Failed to pay invoice: {}", e), + })?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(ApiError::Http { + reason: format!("HTTP {} - {}", status, error_text), + }); + } + + let payment_response: AlbyPaymentResponse = response.json().await.map_err(|e| ApiError::Json { + reason: format!("Failed to parse pay invoice response: {}", e), + })?; + + if payment_response.status != "settled" && payment_response.status != "succeeded" { + return Err(ApiError::Api { + reason: format!("Payment failed with status: {}", payment_response.status), + }); + } + + Ok(PayInvoiceResponse { + payment_hash: payment_response.payment_hash, + preimage: payment_response.payment_preimage, + fee_msats: payment_response.fee * 1000, // Convert sat to msat + }) +} + +pub async fn lookup_invoice( + config: AlbyConfig, + payment_hash: Option, + _from: Option, + _limit: Option, + _search: Option, +) -> Result { + let payment_hash_str = payment_hash.unwrap_or_default(); + let client = async_client(&config); + + let response = client + .get(&format!("{}/transactions/{}", get_base_url(&config), payment_hash_str)) + .send() + .await + .map_err(|e| ApiError::Http { + reason: format!("Failed to lookup invoice: {}", e), + })?; + + if response.status() == reqwest::StatusCode::NOT_FOUND { + return Err(ApiError::Json { + reason: "Invoice not found".to_string(), + }); + } + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(ApiError::Http { + reason: format!("HTTP {} - {}", status, error_text), + }); + } + + let invoice: AlbyLookupInvoiceResponse = response.json().await.map_err(|e| ApiError::Json { + reason: format!("Failed to parse lookup invoice response: {}", e), + })?; + + Ok(Transaction { + type_: "incoming".to_string(), + invoice: invoice.payment_request, + preimage: invoice.payment_preimage.unwrap_or_default(), + payment_hash: invoice.payment_hash, + amount_msats: invoice.amount * 1000, // Convert sat to msat + fees_paid: invoice.fee.unwrap_or(0) * 1000, // Convert sat to msat + created_at: parse_timestamp(&invoice.created_at), + expires_at: parse_timestamp(&invoice.expires_at), + settled_at: invoice.settled_at.as_ref().map(|s| parse_timestamp(s)).unwrap_or(0), + description: invoice.description.unwrap_or_default(), + description_hash: "".to_string(), + payer_note: Some("".to_string()), + external_id: Some("".to_string()), + }) +} + +pub async fn list_transactions( + config: AlbyConfig, + from: i64, + limit: i64, + _search: Option, +) -> Result, ApiError> { + let client = async_client(&config); + + let mut url = format!("{}/transactions", get_base_url(&config)); + let mut params = vec![]; + + if limit > 0 { + params.push(format!("limit={}", limit)); + } + if from > 0 { + params.push(format!("offset={}", from)); + } + + if !params.is_empty() { + url.push('?'); + url.push_str(¶ms.join("&")); + } + + let response = client + .get(&url) + .send() + .await + .map_err(|e| ApiError::Http { + reason: format!("Failed to list transactions: {}", e), + })?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(ApiError::Http { + reason: format!("HTTP {} - {}", status, error_text), + }); + } + + let txns: AlbyTransactionsResponse = response.json().await.map_err(|e| ApiError::Json { + reason: format!("Failed to parse list transactions response: {}", e), + })?; + + let mut transactions: Vec = txns + .transactions + .into_iter() + .map(|txn| Transaction { + type_: txn.type_, + invoice: txn.payment_request.unwrap_or_default(), + preimage: txn.payment_preimage.unwrap_or_default(), + payment_hash: txn.payment_hash, + amount_msats: txn.amount * 1000, // Convert sat to msat + fees_paid: txn.fee.unwrap_or(0) * 1000, // Convert sat to msat + created_at: parse_timestamp(&txn.created_at), + expires_at: 0, // Not provided in list response + settled_at: txn.settled_at.as_ref().map(|s| parse_timestamp(s)).unwrap_or(0), + description: txn.description.unwrap_or_default(), + description_hash: "".to_string(), + payer_note: Some("".to_string()), + external_id: Some("".to_string()), + }) + .collect(); + + transactions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + Ok(transactions) +} + +pub fn decode(_config: &AlbyConfig, _invoice_str: String) -> Result { + // Alby Hub doesn't have a specific decode endpoint + // For now, we'll return a simple error indicating this isn't supported + Err(ApiError::Api { + reason: "Invoice decode not implemented for Alby Hub".to_string(), + }) +} + +pub async fn on_invoice_events( + config: AlbyConfig, + params: OnInvoiceEventParams, + callback: Box, +) { + let start_time = std::time::Instant::now(); + + loop { + if start_time.elapsed() > Duration::from_secs(params.max_polling_sec as u64) { + callback.failure(None); + break; + } + + let lookup_result = lookup_invoice( + config.clone(), + params.payment_hash.clone(), + None, + None, + params.search.clone(), + ) + .await; + + match lookup_result { + Ok(transaction) => { + if transaction.settled_at > 0 { + callback.success(Some(transaction)); + break; + } else { + callback.pending(Some(transaction)); + } + } + Err(_) => { + callback.failure(None); + // Continue polling on error + } + } + + tokio::time::sleep(Duration::from_secs(params.polling_delay_sec as u64)).await; + } +} + +pub fn get_offer(_config: &AlbyConfig, _search: Option) -> Result { + Err(ApiError::Api { + reason: "Bolt12 offers not implemented for Alby Hub".to_string(), + }) +} + +pub fn list_offers(_config: &AlbyConfig, _search: Option) -> Result, ApiError> { + Err(ApiError::Api { + reason: "Bolt12 offers not implemented for Alby Hub".to_string(), + }) +} + +pub fn pay_offer( + _config: &AlbyConfig, + _offer: String, + _amount_msats: i64, + _payer_note: Option, +) -> Result { + Err(ApiError::Api { + reason: "Bolt12 offers not implemented for Alby Hub".to_string(), + }) +} + +fn parse_timestamp(timestamp_str: &str) -> i64 { + // Try to parse ISO 8601 timestamp + if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(timestamp_str) { + return dt.timestamp(); + } + + // Try to parse as Unix timestamp + if let Ok(timestamp) = timestamp_str.parse::() { + return timestamp; + } + + // Default to 0 if parsing fails + 0 +} \ No newline at end of file diff --git a/crates/lni/alby/lib.rs b/crates/lni/alby/lib.rs new file mode 100644 index 0000000..c6b3d83 --- /dev/null +++ b/crates/lni/alby/lib.rs @@ -0,0 +1,323 @@ +#[cfg(feature = "napi_rs")] +use napi_derive::napi; + +use crate::types::NodeInfo; +use crate::{ + ApiError, CreateInvoiceParams, LightningNode, ListTransactionsParams, LookupInvoiceParams, + PayCode, PayInvoiceParams, PayInvoiceResponse, Transaction, +}; + +#[cfg_attr(feature = "napi_rs", napi(object))] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[derive(Debug, Clone)] +pub struct AlbyConfig { + #[cfg_attr(feature = "uniffi", uniffi(default = Some("https://my.albyhub.com/api")))] + pub base_url: Option, + pub api_key: String, + pub alby_hub_name: String, + pub alby_hub_region: String, + #[cfg_attr(feature = "uniffi", uniffi(default = Some("")))] + pub socks5_proxy: Option, // Some("socks5h://127.0.0.1:9150") or Some("".to_string()) + #[cfg_attr(feature = "uniffi", uniffi(default = Some(true)))] + pub accept_invalid_certs: Option, + #[cfg_attr(feature = "uniffi", uniffi(default = Some(120)))] + pub http_timeout: Option, +} + +impl Default for AlbyConfig { + fn default() -> Self { + Self { + base_url: Some("https://my.albyhub.com/api".to_string()), + api_key: "".to_string(), + alby_hub_name: "".to_string(), + alby_hub_region: "".to_string(), + socks5_proxy: Some("".to_string()), + accept_invalid_certs: Some(false), + http_timeout: Some(60), + } + } +} + +#[cfg_attr(feature = "napi_rs", napi(object))] +#[cfg_attr(feature = "uniffi", derive(uniffi::Object))] +#[derive(Debug, Clone)] +pub struct AlbyNode { + pub config: AlbyConfig, +} + +// Constructor is inherent, not part of the trait +#[cfg_attr(feature = "uniffi", uniffi::export)] +impl AlbyNode { + #[cfg_attr(feature = "uniffi", uniffi::constructor)] + pub fn new(config: AlbyConfig) -> Self { + Self { config } + } +} + +#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))] +#[async_trait::async_trait] +impl LightningNode for AlbyNode { + async fn get_info(&self) -> Result { + crate::alby::api::get_info(self.config.clone()).await + } + + async fn create_invoice(&self, params: CreateInvoiceParams) -> Result { + crate::alby::api::create_invoice(self.config.clone(), params).await + } + + async fn pay_invoice(&self, params: PayInvoiceParams) -> Result { + crate::alby::api::pay_invoice(self.config.clone(), params).await + } + + async fn lookup_invoice( + &self, + params: LookupInvoiceParams, + ) -> Result { + crate::alby::api::lookup_invoice( + self.config.clone(), + params.payment_hash, + None, + None, + params.search, + ) + .await + } + + async fn list_transactions( + &self, + params: ListTransactionsParams, + ) -> Result, ApiError> { + crate::alby::api::list_transactions( + self.config.clone(), + params.from, + params.limit, + params.search, + ) + .await + } + + async fn decode(&self, str: String) -> Result { + crate::alby::api::decode(&self.config, str) + } + + async fn on_invoice_events( + &self, + params: crate::types::OnInvoiceEventParams, + callback: Box, + ) { + crate::alby::api::on_invoice_events(self.config.clone(), params, callback).await + } + + async fn get_offer(&self, search: Option) -> Result { + crate::alby::api::get_offer(&self.config, search) + } + + async fn list_offers(&self, search: Option) -> Result, ApiError> { + crate::alby::api::list_offers(&self.config, search) + } + + async fn pay_offer( + &self, + offer: String, + amount_msats: i64, + payer_note: Option, + ) -> Result { + crate::alby::api::pay_offer(&self.config, offer, amount_msats, payer_note) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::InvoiceType; + use dotenv::dotenv; + use lazy_static::lazy_static; + use std::env; + use std::sync::{Arc, Mutex}; + + lazy_static! { + static ref BASE_URL: String = { + dotenv().ok(); + env::var("ALBY_BASE_URL").unwrap_or_else(|_| "https://my.albyhub.com/api".to_string()) + }; + static ref API_KEY: String = { + dotenv().ok(); + env::var("ALBY_API_KEY").expect("ALBY_API_KEY must be set") + }; + static ref ALBY_HUB_NAME: String = { + dotenv().ok(); + env::var("ALBY_HUB_NAME").expect("ALBY_HUB_NAME must be set") + }; + static ref ALBY_HUB_REGION: String = { + dotenv().ok(); + env::var("ALBY_HUB_REGION").expect("ALBY_HUB_REGION must be set") + }; + static ref TEST_PAYMENT_HASH: String = { + dotenv().ok(); + env::var("ALBY_TEST_PAYMENT_HASH").expect("ALBY_TEST_PAYMENT_HASH must be set") + }; + static ref TEST_PAYMENT_REQUEST: String = { + dotenv().ok(); + env::var("ALBY_TEST_PAYMENT_REQUEST") + .expect("ALBY_TEST_PAYMENT_REQUEST must be set") + }; + static ref NODE: AlbyNode = { + AlbyNode::new(AlbyConfig { + base_url: Some(BASE_URL.clone()), + api_key: API_KEY.clone(), + alby_hub_name: ALBY_HUB_NAME.clone(), + alby_hub_region: ALBY_HUB_REGION.clone(), + http_timeout: Some(120), + socks5_proxy: Some("".to_string()), + accept_invalid_certs: Some(false), + }) + }; + } + + #[tokio::test] + async fn test_get_info() { + match NODE.get_info().await { + Ok(info) => { + dbg!("info: {:?}", info); + } + Err(e) => { + println!("Alby get_info failed (expected if no API key): {:?}", e); + // Don't panic as this requires valid API key + } + } + } + + #[tokio::test] + async fn test_create_invoice() { + let amount_msats = 5000; // 5 sats + let description = "Test Alby invoice".to_string(); + let expiry = 3600; + + match NODE + .create_invoice(CreateInvoiceParams { + invoice_type: InvoiceType::Bolt11, + amount_msats: Some(amount_msats), + description: Some(description.clone()), + expiry: Some(expiry), + ..Default::default() + }) + .await + { + Ok(txn) => { + println!("Alby create_invoice: {:?}", txn); + assert!( + !txn.invoice.is_empty(), + "Alby create_invoice Invoice should not be empty" + ); + } + Err(e) => { + println!( + "Alby create_invoice failed (expected if no API key): {:?}", + e + ); + // Don't panic as this requires valid API key + } + } + } + + #[tokio::test] + async fn test_lookup_invoice() { + match NODE + .lookup_invoice(LookupInvoiceParams { + payment_hash: Some(TEST_PAYMENT_HASH.to_string()), + ..Default::default() + }) + .await + { + Ok(txn) => { + println!("Alby lookup invoice: {:?}", txn); + assert!( + txn.amount_msats >= 0, + "Invoice should contain a valid amount" + ); + } + Err(e) => { + if e.to_string().contains("not found") { + assert!(true, "Invoice not found as expected"); + } else { + println!( + "Alby lookup invoice failed (expected if no API key): {:?}", + e + ); + } + } + } + } + + #[tokio::test] + async fn test_list_transactions() { + let params = ListTransactionsParams { + from: 0, + limit: 10, + payment_hash: None, + search: None, + }; + match NODE.list_transactions(params).await { + Ok(txns) => { + println!("Alby transactions: {:?}", txns); + assert!(true, "Should be able to list transactions"); + } + Err(e) => { + println!( + "Alby list transactions failed (expected if no API key): {:?}", + e + ); + // Don't panic as this requires valid API key + } + } + } + + #[tokio::test] + async fn test_on_invoice_events() { + struct OnInvoiceEventCallback { + events: Arc>>, + } + + impl crate::types::OnInvoiceEventCallback for OnInvoiceEventCallback { + fn success(&self, transaction: Option) { + dbg!(&transaction); + let mut events = self.events.lock().unwrap(); + events.push(format!("{} - {:?}", "success", transaction)); + } + fn pending(&self, transaction: Option) { + let mut events = self.events.lock().unwrap(); + events.push(format!("{} - {:?}", "pending", transaction)); + } + fn failure(&self, transaction: Option) { + let mut events = self.events.lock().unwrap(); + events.push(format!("{} - {:?}", "failure", transaction)); + } + } + + let events = Arc::new(Mutex::new(Vec::new())); + let callback = OnInvoiceEventCallback { + events: events.clone(), + }; + + // Use the real test payment hash from environment + let params = crate::types::OnInvoiceEventParams { + payment_hash: Some(TEST_PAYMENT_HASH.to_string()), + polling_delay_sec: 2, + max_polling_sec: 5, // Short timeout for test + ..Default::default() + }; + + // Start the event listener + NODE.on_invoice_events(params, Box::new(callback)).await; + + // Check that some events were captured + let events_guard = events.lock().unwrap(); + println!("Alby events captured: {:?}", *events_guard); + + // We expect at least one event (even if it's a failure due to invoice not found) + assert!( + !events_guard.is_empty(), + "Should capture at least one event" + ); + } +} \ No newline at end of file diff --git a/crates/lni/alby/types.rs b/crates/lni/alby/types.rs new file mode 100644 index 0000000..8028dd5 --- /dev/null +++ b/crates/lni/alby/types.rs @@ -0,0 +1,140 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct AlbyInfoResponse { + #[serde(rename = "backendType")] + pub backend_type: String, + #[serde(rename = "setupCompleted")] + pub setup_completed: bool, + #[serde(rename = "oauthRedirect")] + pub oauth_redirect: bool, + pub running: bool, + pub unlocked: bool, + #[serde(rename = "albyAuthUrl")] + pub alby_auth_url: String, + #[serde(rename = "nextBackupReminder")] + pub next_backup_reminder: String, + #[serde(rename = "albyUserIdentifier")] + pub alby_user_identifier: String, + #[serde(rename = "albyAccountConnected")] + pub alby_account_connected: bool, + pub version: String, + pub network: String, + #[serde(rename = "enableAdvancedSetup")] + pub enable_advanced_setup: bool, + #[serde(rename = "ldkVssEnabled")] + pub ldk_vss_enabled: bool, + #[serde(rename = "vssSupported")] + pub vss_supported: bool, + #[serde(rename = "startupState")] + pub startup_state: String, + #[serde(rename = "startupError")] + pub startup_error: String, + #[serde(rename = "startupErrorTime")] + pub startup_error_time: String, + #[serde(rename = "autoUnlockPasswordSupported")] + pub auto_unlock_password_supported: bool, + #[serde(rename = "autoUnlockPasswordEnabled")] + pub auto_unlock_password_enabled: bool, + pub currency: String, + pub relay: String, + #[serde(rename = "nodeAlias")] + pub node_alias: Option, // This can be empty/missing, so using Option + #[serde(rename = "mempoolUrl")] + pub mempool_url: String, +} + +#[derive(Debug, Deserialize)] +pub struct AlbyBalance { + pub balance: i64, +} + +#[derive(Debug, Deserialize)] +pub struct AlbyBalancesResponse { + #[serde(rename = "balance")] + pub balance: Option, + #[serde(rename = "unit")] + pub unit: Option, +} + +#[derive(Debug, Deserialize)] +pub struct AlbyCreateInvoiceResponse { + pub payment_request: String, + pub payment_hash: String, + pub amount: i64, + pub description: String, + pub created_at: String, + pub expires_at: String, +} + +#[derive(Debug, Deserialize)] +pub struct AlbyPaymentResponse { + pub payment_hash: String, + pub payment_preimage: String, + pub destination: String, + pub amount: i64, + pub fee: i64, + pub status: String, + pub created_at: String, + pub settled_at: Option, +} + +#[derive(Debug, Deserialize)] +pub struct AlbyTransactionResponse { + pub payment_hash: String, + pub payment_request: Option, + pub payment_preimage: Option, + pub amount: i64, + pub fee: Option, + pub status: String, + pub created_at: String, + pub settled_at: Option, + pub description: Option, + pub type_: String, +} + +#[derive(Debug, Deserialize)] +pub struct AlbyTransactionsResponse { + pub transactions: Vec, + pub total: Option, + pub limit: Option, + pub offset: Option, +} + +#[derive(Debug, Deserialize)] +pub struct AlbyLookupInvoiceResponse { + pub payment_hash: String, + pub payment_request: String, + pub payment_preimage: Option, + pub amount: i64, + pub fee: Option, + pub status: String, + pub created_at: String, + pub settled_at: Option, + pub expires_at: String, + pub description: Option, +} + +#[derive(Debug, Deserialize)] +pub struct AlbyDecodeResponse { + pub payment_hash: String, + pub amount_msat: i64, + pub description: String, + pub destination: String, + pub expiry: i64, + pub timestamp: i64, +} + +// Request types +#[derive(Debug, serde::Serialize)] +pub struct AlbyCreateInvoiceRequest { + pub amount: i64, + pub description: Option, + pub expiry: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct AlbyPayInvoiceRequest { + pub invoice: String, + pub amount: Option, +} \ No newline at end of file diff --git a/crates/lni/lib.rs b/crates/lni/lib.rs index 4494857..18df240 100644 --- a/crates/lni/lib.rs +++ b/crates/lni/lib.rs @@ -72,6 +72,13 @@ pub mod speed { pub use lib::{SpeedConfig, SpeedNode}; } +pub mod alby { + pub mod api; + pub mod lib; + pub mod types; + pub use lib::{AlbyConfig, AlbyNode}; +} + pub mod types; pub use types::*; diff --git a/crates/lni/types.rs b/crates/lni/types.rs index 103264f..8e83bf7 100644 --- a/crates/lni/types.rs +++ b/crates/lni/types.rs @@ -3,13 +3,14 @@ use napi_derive::napi; use serde::{Deserialize, Serialize}; use async_trait::async_trait; -use crate::{cln::ClnNode, lnd::LndNode, phoenixd::PhoenixdNode, nwc::NwcNode, ApiError}; +use crate::{alby::AlbyNode, cln::ClnNode, lnd::LndNode, phoenixd::PhoenixdNode, nwc::NwcNode, ApiError}; pub enum LightningNodeEnum { Phoenixd(PhoenixdNode), Lnd(LndNode), Cln(ClnNode), Nwc(NwcNode), + Alby(AlbyNode), } #[async_trait]