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