spark_rust/wallet/handlers/lightning.rs
1use lightning_invoice::Bolt11Invoice;
2use rand::rngs::OsRng;
3use spark_cryptography::derivation_path::SparkKeyType;
4use spark_protos::spark::StorePreimageShareRequest;
5use std::{collections::HashMap, str::FromStr};
6use uuid::Uuid;
7
8use crate::{
9 error::{IoError, NetworkError, SparkSdkError, ValidationError},
10 signer::traits::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 keypair = self.signer.derive_spark_key(
96 leaf.get_id().clone(),
97 0,
98 SparkKeyType::BaseSigning,
99 network.to_bitcoin_network(),
100 )?;
101 let old_signing_private_key = keypair.secret_key();
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 let swap_response = self
114 .swap_nodes_for_preimage(
115 leaf_tweaks.clone(),
116 &self.config.spark_config.ssp_identity_public_key,
117 payment_hash,
118 &invoice,
119 amount_sats,
120 0, // TODO: this must use the estimated fee.
121 false,
122 )
123 .await?;
124
125 #[cfg(feature = "telemetry")]
126 tracing::trace!(swap_response = ?swap_response, "swap_nodes_for_preimage");
127
128 let transfer = swap_response
129 .transfer
130 .ok_or(SparkSdkError::from(ValidationError::InvalidInput {
131 field: "Swap response did not contain a transfer".to_string(),
132 }))?
133 .try_into()?;
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!(
141 transfer_id = transfer.id.to_string(),
142 "send_transfer_tweak_key"
143 );
144
145 // request Lightning send with the SSP
146 let lightning_send_response = self
147 .request_lightning_send_with_ssp(invoice.to_string(), payment_hash.to_string())
148 .await?;
149 #[cfg(feature = "telemetry")]
150 tracing::trace!(lightning_send_response = ?lightning_send_response, "request_lightning_send_with_ssp");
151
152 // delete the leaves after the transfer
153 let leaf_ids_to_remove: Vec<String> = leaves.iter().map(|l| l.get_id().clone()).collect();
154 self.leaf_manager
155 .unlock_leaves(unlocking_id.clone(), &leaf_ids_to_remove, true)?;
156 #[cfg(feature = "telemetry")]
157 tracing::trace!(unlocking_id = ?unlocking_id, leaf_ids_to_remove = ?leaf_ids_to_remove, "unlock_leaves");
158
159 Ok(transfer.id)
160 })
161 .await
162 }
163
164 /// Creates a Lightning Network invoice that others can pay to you.
165 ///
166 /// This function generates a BOLT11 Lightning invoice and distributes the payment preimage
167 /// using a threshold secret sharing scheme among Spark operators. When someone pays this
168 /// invoice via Lightning, the funds will be received by the SSP and then transferred to
169 /// your Spark wallet.
170 ///
171 /// The process involves:
172 /// 1. Generating a secure payment preimage and hash
173 /// 2. Creating the invoice through the SSP
174 /// 3. Distributing preimage shares to Spark operators using threshold secret sharing
175 /// 4. Returning the formatted BOLT11 invoice
176 ///
177 /// # Arguments
178 ///
179 /// * `amount_sats` - The amount in satoshis that you want to receive
180 /// * `memo` - Optional description/memo for the invoice
181 /// * `expiry_seconds` - Optional expiry time in seconds (defaults to 30 days if not specified)
182 ///
183 /// # Returns
184 ///
185 /// * `Ok(Bolt11Invoice)` - The generated Lightning invoice if successful
186 /// * `Err(SparkSdkError)` - If there was an error during invoice creation
187 ///
188 /// # Example
189 ///
190 /// ```
191 /// # use spark_rust::SparkSdk;
192 /// # async fn example(sdk: SparkSdk) -> Result<(), Box<dyn std::error::Error>> {
193 /// // Create an invoice for 50,000 satoshis
194 /// let amount_sats = 50_000;
195 /// let memo = Some("Payment for services".to_string());
196 /// let expiry = Some(3600 * 24); // 24 hours
197 ///
198 /// // Generate the Lightning invoice
199 /// let invoice = sdk.create_lightning_invoice(amount_sats, memo, expiry).await?;
200 ///
201 /// // Get the invoice string to share with the payer
202 /// let invoice_string = invoice.to_string();
203 /// println!("Lightning Invoice: {}", invoice_string);
204 /// # Ok(())
205 /// # }
206 /// ```
207 ///
208 /// # Note
209 ///
210 /// Receiving Lightning payments incurs a service fee charged by the SSP. You can estimate
211 /// this fee before creating an invoice using the `get_lightning_receive_fee_estimate` method.
212 #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
213 pub async fn create_lightning_invoice(
214 &self,
215 amount_sats: u64,
216 memo: Option<String>,
217 expiry_seconds: Option<i32>,
218 ) -> Result<Bolt11Invoice, SparkSdkError> {
219 // default expiry to 30 days
220 let expiry_seconds = expiry_seconds.unwrap_or(60 * 60 * 24 * 30);
221
222 // generate the preimage
223 // hash the preimage to get the payment hash
224 let preimage_sk = bitcoin::secp256k1::SecretKey::new(&mut OsRng);
225 let preimage_bytes = preimage_sk.secret_bytes();
226 let payment_hash = sha256::digest(&preimage_bytes);
227 let payment_hash_bytes = hex::decode(&payment_hash)
228 .map_err(|err| SparkSdkError::from(IoError::Decoding(err)))?;
229
230 // create the invoice by making a request to the SSP
231 // TODO: we'll need to use fees
232 let (invoice, _fees) = self
233 .create_invoice_with_ssp(
234 amount_sats,
235 payment_hash,
236 expiry_seconds,
237 memo,
238 self.config.spark_config.network,
239 )
240 .await?;
241
242 // distribute the preimage shares to the operators
243 // TODO: parallelize this
244 let t = self.config.spark_config.threshold as usize;
245 let n = self.config.spark_config.operator_pool.operators.len();
246 let shares = self.signer.split_with_verifiable_secret_sharing(
247 preimage_sk.secret_bytes().to_vec(),
248 t,
249 n,
250 )?;
251
252 let signing_operators = self.config.spark_config.operator_pool.operators.clone();
253 let identity_pubkey = self.get_spark_address()?;
254
255 let futures = signing_operators.iter().map(|operator| {
256 let operator_id = operator.id;
257 let share = &shares[operator_id as usize];
258 let payment_hash = payment_hash_bytes.clone();
259 let invoice_str = invoice.clone();
260 let threshold = self.config.spark_config.threshold;
261 let config = self.config.clone();
262
263 async move {
264 let request_data = StorePreimageShareRequest {
265 payment_hash,
266 preimage_share: Some(share.marshal_proto()),
267 threshold,
268 invoice_string: invoice_str,
269 user_identity_public_key: identity_pubkey.serialize().to_vec(),
270 };
271
272 config
273 .spark_config
274 .call_with_retry(
275 request_data,
276 |mut client, req| {
277 Box::pin(async move { client.store_preimage_share(req).await })
278 },
279 Some(operator_id),
280 )
281 .await
282 .map_err(|e| tonic::Status::internal(format!("RPC error: {}", e)))?;
283
284 Ok(())
285 }
286 });
287
288 futures::future::try_join_all(futures)
289 .await
290 .map_err(|e| SparkSdkError::from(NetworkError::Status(e)))?;
291
292 Bolt11Invoice::from_str(&invoice).map_err(|err| {
293 SparkSdkError::from(ValidationError::InvalidBolt11Invoice(err.to_string()))
294 })
295 }
296}
297
298fn get_invoice_amount_sats(invoice: &Bolt11Invoice) -> Result<u64, SparkSdkError> {
299 let invoice_amount_msats = invoice
300 .amount_milli_satoshis()
301 .ok_or(SparkSdkError::Validation(
302 ValidationError::InvalidBolt11Invoice(invoice.to_string()),
303 ))?;
304
305 Ok(invoice_amount_msats / 1000)
306}