spark_rust/wallet/handlers/
lightning.rs1use 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 #[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 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 let payment_hash = invoice.payment_hash();
80
81 self.refresh_timelock_nodes(None).await?;
83
84 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 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, 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 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 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 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 #[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 let expiry_seconds = expiry_seconds.unwrap_or(60 * 60 * 24 * 30);
220
221 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 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 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 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}