spark_rust/wallet/handlers/
lightning.rs1use 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 #[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 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 let payment_hash = invoice.payment_hash();
81
82 self.refresh_timelock_nodes(None).await?;
84
85 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 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 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, false,
126 )
127 .await?;
128
129 #[cfg(feature = "telemetry")]
130 tracing::trace!(swap_response = ?swap_response, "swap_nodes_for_preimage");
131
132 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 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 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 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 #[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 let expiry_seconds = expiry_seconds.unwrap_or(60 * 60 * 24 * 30);
224
225 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 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 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 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}