1
1
use crate :: config:: Config ;
2
2
use crate :: models:: PaymentRequestDetails ;
3
3
use anyhow:: Result ;
4
- use reqwest:: Client ;
4
+ use lnbits_rs:: { Client as LNBitsClient , Invoice , Payment , PaymentStatus } ;
5
+ use reqwest:: Client as HttpClient ;
5
6
use serde:: { Deserialize , Serialize } ;
6
7
use std:: sync:: Arc ;
7
8
use thiserror:: Error ;
@@ -25,35 +26,27 @@ pub enum LightningError {
25
26
/// Serialization error
26
27
#[ error( "Serialization error: {0}" ) ]
27
28
SerializationError ( #[ from] serde_json:: Error ) ,
29
+
30
+ /// LNBits client error
31
+ #[ error( "LNBits error: {0}" ) ]
32
+ LNBitsError ( #[ from] lnbits_rs:: Error ) ,
28
33
}
29
34
30
- /// Lightning payment provider
35
+ /// Lightning payment provider using LNBits
31
36
#[ derive( Debug , Clone ) ]
32
37
pub struct LightningProvider {
33
- client : Client ,
38
+ http_client : HttpClient ,
39
+ lnbits_client : Option < LNBitsClient > ,
34
40
config : Arc < Config > ,
35
41
}
36
42
37
- /// Request to create a Lightning invoice
38
- #[ derive( Debug , Serialize ) ]
39
- struct CreateInvoiceRequest {
40
- /// Amount in satoshis
41
- #[ serde( rename = "value" ) ]
42
- amount_sats : u64 ,
43
- /// Invoice memo/description
44
- memo : String ,
45
- /// Expiry in seconds
46
- expiry : u64 ,
47
- }
48
-
49
- /// Response from create invoice request
43
+ /// Invoice webhook event data from LNBits
50
44
#[ derive( Debug , Deserialize ) ]
51
- struct CreateInvoiceResponse {
52
- /// Payment request (BOLT11 invoice string)
53
- #[ serde( rename = "payment_request" ) ]
54
- payment_request : String ,
45
+ pub struct WebhookEvent {
55
46
/// Payment hash
56
- r_hash : String ,
47
+ pub payment_hash : String ,
48
+ /// Status of the payment (true if paid)
49
+ pub payment_status : bool ,
57
50
}
58
51
59
52
impl LightningProvider {
@@ -66,22 +59,42 @@ impl LightningProvider {
66
59
) ) ;
67
60
}
68
61
69
- if config. lnd_rest_endpoint . is_none ( ) {
70
- return Err ( LightningError :: ConfigError (
71
- "LND REST endpoint not configured" . to_string ( ) ,
72
- ) ) ;
73
- }
74
-
75
- if config. lnd_macaroon_hex . is_none ( ) {
62
+ // Create HTTP client
63
+ let http_client = HttpClient :: new ( ) ;
64
+
65
+ // Check for LNBits configuration
66
+ let lnbits_client = if let ( Some ( url) , Some ( admin_key) ) = ( & config. lnbits_url , & config. lnbits_admin_key ) {
67
+ // Create LNBits client
68
+ match LNBitsClient :: new ( url, admin_key) {
69
+ Ok ( client) => {
70
+ info ! ( "LNBits client initialized with admin key" ) ;
71
+ Some ( client)
72
+ }
73
+ Err ( err) => {
74
+ error ! ( "Failed to initialize LNBits client: {}" , err) ;
75
+ return Err ( LightningError :: LNBitsError ( err) ) ;
76
+ }
77
+ }
78
+ } else {
79
+ // Check for legacy LND config as fallback
80
+ if config. lnd_rest_endpoint . is_none ( ) {
81
+ return Err ( LightningError :: ConfigError (
82
+ "Neither LNBits nor LND configuration provided" . to_string ( ) ,
83
+ ) ) ;
84
+ }
85
+
86
+ // Using legacy LND client (not supported in this implementation)
87
+ error ! ( "Legacy LND REST API no longer supported - please use LNBits" ) ;
76
88
return Err ( LightningError :: ConfigError (
77
- "LND macaroon not configured " . to_string ( ) ,
89
+ "Legacy LND REST API no longer supported - please use LNBits " . to_string ( ) ,
78
90
) ) ;
79
- }
80
-
81
- // Create HTTP client
82
- let client = Client :: new ( ) ;
91
+ } ;
83
92
84
- Ok ( Self { client, config } )
93
+ Ok ( Self {
94
+ http_client,
95
+ lnbits_client,
96
+ config
97
+ } )
85
98
}
86
99
87
100
/// Create a Lightning invoice for the specified amount
@@ -90,54 +103,29 @@ impl LightningProvider {
90
103
amount_usd : f64 ,
91
104
description : & str ,
92
105
) -> Result < ( String , String ) , LightningError > {
106
+ // Get the LNBits client
107
+ let client = self . lnbits_client . as_ref ( ) . ok_or_else ( || {
108
+ LightningError :: ConfigError ( "LNBits client not configured" . to_string ( ) )
109
+ } ) ?;
110
+
93
111
// Convert USD to satoshis
94
112
let amount_sats = self . convert_usd_to_sats ( amount_usd) . await ?;
95
113
96
- // Create invoice with 30 minute expiry
97
- let expiry = 30 * 60 ; // 30 minutes in seconds
114
+ // Create invoice with 30 minute expiry (in seconds)
115
+ let expiry = 30 * 60 ;
98
116
99
- // Build the request to LND
100
- let request = CreateInvoiceRequest {
117
+ // Create the invoice
118
+ let invoice = client . create_invoice (
101
119
amount_sats,
102
- memo : description. to_string ( ) ,
103
- expiry,
104
- } ;
105
-
106
- // Get LND endpoint and macaroon
107
- let endpoint = self . config . lnd_rest_endpoint . as_ref ( ) . unwrap ( ) ;
108
- let macaroon = self . config . lnd_macaroon_hex . as_ref ( ) . unwrap ( ) ;
109
-
110
- // Construct the full URL
111
- let url = format ! ( "{}/v1/invoices" , endpoint) ;
112
-
113
- // Make the request to LND
114
- let response = self
115
- . client
116
- . post ( & url)
117
- . header ( "Grpc-Metadata-macaroon" , macaroon)
118
- . json ( & request)
119
- . send ( )
120
- . await ?;
121
-
122
- // Check for errors
123
- if !response. status ( ) . is_success ( ) {
124
- let status = response. status ( ) ;
125
- let error_text = response
126
- . text ( )
127
- . await
128
- . unwrap_or_else ( |_| "Unknown error" . to_string ( ) ) ;
129
- error ! ( "LND returned error: {} - {}" , status, error_text) ;
130
- return Err ( LightningError :: ApiError ( format ! (
131
- "LND API error: {}" ,
132
- error_text
133
- ) ) ) ;
134
- }
135
-
136
- // Parse the response
137
- let invoice_response: CreateInvoiceResponse = response. json ( ) . await ?;
120
+ description. to_string ( ) ,
121
+ Some ( expiry) ,
122
+ None , // webhook URL will be configured externally
123
+ ) . await ?;
138
124
139
125
info ! ( "Created Lightning invoice for {} sats" , amount_sats) ;
140
- Ok ( ( invoice_response. payment_request , invoice_response. r_hash ) )
126
+
127
+ // Return the payment request (BOLT11 invoice) and payment hash
128
+ Ok ( ( invoice. payment_request , invoice. payment_hash ) )
141
129
}
142
130
143
131
/// Convert USD to satoshis using current exchange rate
@@ -158,11 +146,37 @@ impl LightningProvider {
158
146
Ok ( amount_sats)
159
147
}
160
148
149
+ /// Check if a payment has been settled
150
+ pub async fn check_invoice ( & self , payment_hash : & str ) -> Result < bool , LightningError > {
151
+ // Get the LNBits client with invoice read key
152
+ let invoice_read_key = self . config . lnbits_invoice_read_key . as_ref ( )
153
+ . ok_or_else ( || LightningError :: ConfigError ( "LNBits invoice read key not configured" . to_string ( ) ) ) ?;
154
+
155
+ // Use the URL from the admin client
156
+ let url = self . config . lnbits_url . as_ref ( )
157
+ . ok_or_else ( || LightningError :: ConfigError ( "LNBits URL not configured" . to_string ( ) ) ) ?;
158
+
159
+ // Create a client with invoice read key for checking payment status
160
+ let client = LNBitsClient :: new ( url, invoice_read_key) ?;
161
+
162
+ // Check the payment status
163
+ let payment = client. get_payment ( payment_hash) . await ?;
164
+
165
+ Ok ( payment. paid )
166
+ }
167
+
161
168
/// Verify a webhook payload from Lightning provider
162
- pub fn verify_webhook ( & self , _body : & [ u8 ] , _signature : & str ) -> Result < bool , LightningError > {
163
- // In a real implementation, you would verify the signature here
164
- // For this example, we'll just return true
165
- Ok ( true )
169
+ pub fn verify_webhook ( & self , body : & [ u8 ] , signature : & str ) -> Result < WebhookEvent , LightningError > {
170
+ // For simplicity, this implementation doesn't verify signatures
171
+ // In a production environment, you should verify using your LNBits webhook key
172
+
173
+ // Try to parse the webhook event
174
+ let event: WebhookEvent = serde_json:: from_slice ( body)
175
+ . map_err ( |e| LightningError :: SerializationError ( e) ) ?;
176
+
177
+ debug ! ( "Parsed webhook event for payment_hash: {}" , event. payment_hash) ;
178
+
179
+ Ok ( event)
166
180
}
167
181
168
182
/// Generate payment details for the client
0 commit comments