spark_rust/wallet/handlers/lightning.rs
1use lightning_invoice::Bolt11Invoice;
2use rand::rngs::OsRng;
3use spark_cryptography::secret_sharing::shamir_new::VSS;
4use spark_protos::spark::StorePreimageShareRequest;
5use std::{collections::HashMap, str::FromStr};
6
7use crate::{
8 error::{CryptoError, IoError, NetworkError, SparkSdkError, ValidationError},
9 signer::traits::{derivation_path::SparkKeyType, SparkSigner},
10 wallet::internal_handlers::traits::{
11 leaves::LeavesInternalHandlers,
12 lightning::LightningInternalHandlers,
13 ssp::SspInternalHandlers,
14 transfer::{LeafKeyTweak, TransferInternalHandlers},
15 },
16 with_handler_lock, SparkSdk,
17};
18
19impl<S: SparkSigner + Send + Sync + Clone + 'static> SparkSdk<S> {
20 /// Pays a Lightning Network invoice using the Spark Service Provider (SSP) as an intermediary.
21 ///
22 /// Unlike traditional Lightning wallets, Spark doesn't directly connect to the Lightning Network.
23 /// Instead, it uses a cooperative approach where:
24 ///
25 /// 1. You provide your leaves (UTXOs) to the SSP
26 /// 2. The SSP makes the Lightning payment on your behalf
27 /// 3. The transaction is secured using cryptographic techniques
28 ///
29 /// This method handles the entire process including:
30 /// - Parsing and validating the Lightning invoice
31 /// - Selecting appropriate leaves to cover the invoice amount
32 /// - Executing a secure swap with the SSP
33 /// - Finalizing the payment through the Lightning Network
34 ///
35 /// # Arguments
36 ///
37 /// * `invoice` - A BOLT11 Lightning invoice string that you want to pay
38 ///
39 /// # Returns
40 ///
41 /// * `Ok(String)` - The payment ID if successful, which can be used to track the payment status
42 /// * `Err(SparkSdkError)` - If there was an error during the payment process
43 ///
44 /// # Example
45 ///
46 /// ```
47 /// # use spark_rust::SparkSdk;
48 /// # async fn example(sdk: SparkSdk) -> Result<(), Box<dyn std::error::Error>> {
49 /// // Lightning invoice to pay
50 /// let invoice = "lnbc1500n1p3zty3app5wkf0hagkc4egr8rl88msr4c5lp0ygt6gvzna5hdg4tpna65pzqdq0vehk7cnpwga5xzmnwvycqzpgxqyz5vqsp5v9ym7xsyf0qxqwzlmwjl3g0g9q2tg977h70hcheske9xlgfsggls9qyyssqtghx3qqpwm9zl4m398nm40wj8ryaz8v7v4rrdvczypdpy7qtc6rdrkklm9uxlkmtp3jf29yhqjw2vwmlp82y5ctft94k23cwgqd9llgy".to_string();
51 ///
52 /// // Pay the invoice
53 /// let payment_id = sdk.pay_lightning_invoice(&invoice).await?;
54 /// println!("Lightning payment initiated with ID: {}", payment_id);
55 /// # Ok(())
56 /// # }
57 /// ```
58 ///
59 /// # Note
60 ///
61 /// The payment incurs a service fee charged by the SSP. You can estimate this fee before
62 /// initiating the payment using the `get_lightning_send_fee_estimate` method.
63 #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
64 pub async fn pay_lightning_invoice(&self, invoice: &String) -> Result<String, SparkSdkError> {
65 with_handler_lock!(self, async {
66 let invoice = Bolt11Invoice::from_str(invoice)
67 .map_err(|err| SparkSdkError::from(IoError::Bolt11InvoiceDecoding(err.to_string())))?;
68
69 // get the invoice amount in sats, then validate the amount
70 let amount_sats = get_invoice_amount_sats(&invoice)?;
71 if amount_sats == 0 {
72 return Err(SparkSdkError::from(ValidationError::InvalidInput {
73 field: "Amount must be greater than 0".to_string(),
74 }));
75 }
76
77 // get the payment hash from the invoice
78 let payment_hash = invoice.payment_hash();
79
80 // refresh timelock nodes
81 self.refresh_timelock_nodes(None).await?;
82
83 // select leaves
84 let leaf_selection_response = self.prepare_leaves_for_amount(amount_sats).await?;
85
86 let unlocking_id = leaf_selection_response.unlocking_id.clone().unwrap();
87 let leaves = leaf_selection_response.leaves;
88
89 // prepare leaf tweaks
90 let network = self.get_network();
91 let mut leaf_tweaks = Vec::with_capacity(leaves.len());
92 for leaf in &leaves {
93 let tree_node = leaf.get_tree_node()?;
94 let old_signing_private_key = self.signer.expose_leaf_secret_key_for_transfer(
95 leaf.get_id().clone(),
96 SparkKeyType::BaseSigning,
97 0,
98 network.to_bitcoin_network(),
99 )?;
100
101 let new_signing_public_key = self.signer.new_ephemeral_keypair()?;
102
103 let leaf_tweak = LeafKeyTweak {
104 leaf: tree_node,
105 old_signing_private_key,
106 new_signing_public_key,
107 };
108 leaf_tweaks.push(leaf_tweak);
109 }
110
111 let swap_response = self
112 .swap_nodes_for_preimage(
113 leaf_tweaks.clone(),
114 &self.config.spark_config.ssp_identity_public_key,
115 payment_hash,
116 &invoice,
117 amount_sats,
118 0, // TODO: this must use the estimated fee.
119 false,
120 )
121 .await?;
122
123 #[cfg(feature = "telemetry")]
124 tracing::trace!(swap_response = ?swap_response, "swap_nodes_for_preimage");
125
126 // validate the swap response, check it has the transfer field non-empty
127 if swap_response.transfer.is_none() {
128 return Err(SparkSdkError::from(ValidationError::InvalidInput {
129 field: "Swap response did not contain a transfer".to_string(),
130 }));
131 }
132
133 let transfer = swap_response.transfer.unwrap();
134
135 // start the transfer
136 let transfer = self
137 .send_transfer_tweak_key(transfer, &leaf_tweaks, &HashMap::new())
138 .await?;
139 #[cfg(feature = "telemetry")]
140 tracing::trace!(transfer = ?transfer, "send_transfer_tweak_key");
141
142 // request Lightning send with the SSP
143 let lightning_send_response = self
144 .request_lightning_send_with_ssp(invoice.to_string(), payment_hash.to_string())
145 .await?;
146 #[cfg(feature = "telemetry")]
147 tracing::trace!(lightning_send_response = ?lightning_send_response, "request_lightning_send_with_ssp");
148
149 // delete the leaves after the transfer
150 let leaf_ids_to_remove: Vec<String> = leaves.iter().map(|l| l.get_id().clone()).collect();
151 self.leaf_manager
152 .unlock_leaves(unlocking_id.clone(), &leaf_ids_to_remove, true)?;
153 #[cfg(feature = "telemetry")]
154 tracing::trace!(unlocking_id = ?unlocking_id, leaf_ids_to_remove = ?leaf_ids_to_remove, "unlock_leaves");
155
156 Ok(lightning_send_response)
157 })
158 .await
159 }
160
161 /// Creates a Lightning Network invoice that others can pay to you.
162 ///
163 /// This function generates a BOLT11 Lightning invoice and distributes the payment preimage
164 /// using a threshold secret sharing scheme among Spark operators. When someone pays this
165 /// invoice via Lightning, the funds will be received by the SSP and then transferred to
166 /// your Spark wallet.
167 ///
168 /// The process involves:
169 /// 1. Generating a secure payment preimage and hash
170 /// 2. Creating the invoice through the SSP
171 /// 3. Distributing preimage shares to Spark operators using threshold secret sharing
172 /// 4. Returning the formatted BOLT11 invoice
173 ///
174 /// # Arguments
175 ///
176 /// * `amount_sats` - The amount in satoshis that you want to receive
177 /// * `memo` - Optional description/memo for the invoice
178 /// * `expiry_seconds` - Optional expiry time in seconds (defaults to 30 days if not specified)
179 ///
180 /// # Returns
181 ///
182 /// * `Ok(Bolt11Invoice)` - The generated Lightning invoice if successful
183 /// * `Err(SparkSdkError)` - If there was an error during invoice creation
184 ///
185 /// # Example
186 ///
187 /// ```
188 /// # use spark_rust::SparkSdk;
189 /// # async fn example(sdk: SparkSdk) -> Result<(), Box<dyn std::error::Error>> {
190 /// // Create an invoice for 50,000 satoshis
191 /// let amount_sats = 50_000;
192 /// let memo = Some("Payment for services".to_string());
193 /// let expiry = Some(3600 * 24); // 24 hours
194 ///
195 /// // Generate the Lightning invoice
196 /// let invoice = sdk.create_lightning_invoice(amount_sats, memo, expiry).await?;
197 ///
198 /// // Get the invoice string to share with the payer
199 /// let invoice_string = invoice.to_string();
200 /// println!("Lightning Invoice: {}", invoice_string);
201 /// # Ok(())
202 /// # }
203 /// ```
204 ///
205 /// # Note
206 ///
207 /// Receiving Lightning payments incurs a service fee charged by the SSP. You can estimate
208 /// this fee before creating an invoice using the `get_lightning_receive_fee_estimate` method.
209 #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
210 pub async fn create_lightning_invoice(
211 &self,
212 amount_sats: u64,
213 memo: Option<String>,
214 expiry_seconds: Option<i32>,
215 ) -> Result<Bolt11Invoice, SparkSdkError> {
216 // default expiry to 30 days
217 let expiry_seconds = expiry_seconds.unwrap_or(60 * 60 * 24 * 30);
218
219 // generate the preimage
220 // hash the preimage to get the payment hash
221 let preimage_sk = bitcoin::secp256k1::SecretKey::new(&mut OsRng);
222 let preimage_bytes = preimage_sk.secret_bytes();
223 let payment_hash = sha256::digest(&preimage_bytes);
224 let payment_hash_bytes = hex::decode(&payment_hash)
225 .map_err(|err| SparkSdkError::from(IoError::Decoding(err)))?;
226
227 // create the invoice by making a request to the SSP
228 // TODO: we'll need to use fees
229 let (invoice, _fees) = self
230 .create_invoice_with_ssp(
231 amount_sats,
232 payment_hash,
233 expiry_seconds,
234 memo,
235 self.config.spark_config.network,
236 )
237 .await?;
238
239 // distribute the preimage shares to the operators
240 // TODO: parallelize this
241 let t = self.config.spark_config.threshold as usize;
242 let n = self.config.spark_config.operator_pool.operators.len();
243 let vss = VSS::new(t, n).unwrap();
244 let shares = vss.split_from_secret_key(&preimage_sk).map_err(|e| {
245 SparkSdkError::from(CryptoError::InvalidInput {
246 field: format!("Failed to split preimage: {}", e),
247 })
248 })?;
249
250 // verify the shares
251 let verification_shares = vss.reconstruct(&shares[..t]).unwrap();
252 if verification_shares.to_bytes().to_vec() != preimage_sk.secret_bytes().to_vec() {
253 return Err(SparkSdkError::from(ValidationError::InvalidInput {
254 field: "Verification failed: shares do not reconstruct to the preimage".to_string(),
255 }));
256 }
257
258 let signing_operators = self.config.spark_config.operator_pool.operators.clone();
259 let identity_pubkey = self.get_spark_address()?;
260
261 let futures = signing_operators.iter().map(|operator| {
262 let operator_id = operator.id;
263 let share = &shares[operator_id as usize];
264 let payment_hash = payment_hash_bytes.clone();
265 let invoice_str = invoice.clone();
266 let threshold = self.config.spark_config.threshold;
267 let config = self.config.clone();
268
269 async move {
270 let request_data = StorePreimageShareRequest {
271 payment_hash,
272 preimage_share: Some(share.marshal_proto()),
273 threshold,
274 invoice_string: invoice_str,
275 user_identity_public_key: identity_pubkey.serialize().to_vec(),
276 };
277
278 config
279 .spark_config
280 .call_with_retry(
281 request_data,
282 |mut client, req| {
283 Box::pin(async move { client.store_preimage_share(req).await })
284 },
285 Some(operator_id),
286 )
287 .await
288 .map_err(|e| tonic::Status::internal(format!("RPC error: {}", e)))?;
289
290 Ok(())
291 }
292 });
293
294 futures::future::try_join_all(futures)
295 .await
296 .map_err(|e| SparkSdkError::from(NetworkError::Status(e)))?;
297
298 Bolt11Invoice::from_str(&invoice).map_err(|err| {
299 SparkSdkError::from(ValidationError::InvalidBolt11Invoice(err.to_string()))
300 })
301 }
302}
303
304fn get_invoice_amount_sats(invoice: &Bolt11Invoice) -> Result<u64, SparkSdkError> {
305 let invoice_amount_msats = invoice
306 .amount_milli_satoshis()
307 .ok_or(SparkSdkError::Validation(
308 ValidationError::InvalidBolt11Invoice(invoice.to_string()),
309 ))?;
310
311 Ok(invoice_amount_msats / 1000)
312}