spark_rust/wallet/handlers/
lightning.rs

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