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    SparkSdk,
19};
20
21impl<S: SparkSigner + Send + Sync + Clone + 'static> SparkSdk<S> {
22    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
23    pub async fn pay_lightning_invoice(&self, invoice: &String) -> Result<String, SparkSdkError> {
24        let invoice = Bolt11Invoice::from_str(invoice)
25            .map_err(|err| SparkSdkError::from(IoError::Bolt11InvoiceDecoding(err.to_string())))?;
26
27        // get the invoice amount in sats, then validate the amount
28        let amount_sats = get_invoice_amount_sats(&invoice)?;
29        if amount_sats == 0 {
30            return Err(SparkSdkError::from(ValidationError::InvalidInput {
31                field: "Amount must be greater than 0".to_string(),
32            }));
33        }
34
35        // get the payment hash from the invoice
36        let payment_hash = invoice.payment_hash();
37
38        // select leaves
39        let leaf_selection_response = self.prepare_leaves_for_amount(amount_sats).await?;
40
41        let unlocking_id = leaf_selection_response.unlocking_id.clone().unwrap();
42        let leaves = leaf_selection_response.leaves;
43
44        // self.refresh_timelock_nodes()?.await; // TODO
45
46        // prepare leaf tweaks
47        let network = self.get_network();
48        let mut leaf_tweaks = Vec::with_capacity(leaves.len());
49        for leaf in &leaves {
50            let tree_node = leaf.get_tree_node()?;
51            let old_signing_private_key = self.signer.expose_leaf_secret_key_for_transfer(
52                leaf.get_id().clone(),
53                SparkKeyType::BaseSigning,
54                0,
55                network.to_bitcoin_network(),
56            )?;
57
58            let new_signing_public_key = self.signer.new_ephemeral_keypair()?;
59
60            let leaf_tweak = LeafKeyTweak {
61                leaf: tree_node,
62                old_signing_private_key,
63                new_signing_public_key,
64            };
65            leaf_tweaks.push(leaf_tweak);
66        }
67
68        // swap the leaves for the preimage
69        let ssp_public_key = PublicKey::from_str(LIGHTSPARK_SSP_IDENTITY_PUBLIC_KEY)
70            .map_err(|err| SparkSdkError::from(CryptoError::Secp256k1(err)))?;
71
72        let swap_response = self
73            .swap_nodes_for_preimage(
74                leaf_tweaks.clone(),
75                &ssp_public_key,
76                payment_hash,
77                &invoice,
78                amount_sats,
79                0, // TODO: this must use the estimated fee.
80                false,
81            )
82            .await?;
83
84        #[cfg(feature = "telemetry")]
85        tracing::trace!(swap_response = ?swap_response, "swap_nodes_for_preimage");
86
87        // validate the swap response, check it has the transfer field non-empty
88        if swap_response.transfer.is_none() {
89            return Err(SparkSdkError::from(ValidationError::InvalidInput {
90                field: "Swap response did not contain a transfer".to_string(),
91            }));
92        }
93
94        let transfer = swap_response.transfer.unwrap();
95
96        // start the transfer
97        let transfer = self
98            .send_transfer_tweak_key(transfer, &leaf_tweaks, &HashMap::new())
99            .await?;
100        #[cfg(feature = "telemetry")]
101        tracing::trace!(transfer = ?transfer, "send_transfer_tweak_key");
102
103        // request Lightning send with the SSP
104        let lightning_send_response = self
105            .request_lightning_send_with_ssp(invoice.to_string(), payment_hash.to_string())
106            .await?;
107        #[cfg(feature = "telemetry")]
108        tracing::trace!(lightning_send_response = ?lightning_send_response, "request_lightning_send_with_ssp");
109
110        // delete the leaves after the transfer
111        let leaf_ids_to_remove: Vec<String> = leaves.iter().map(|l| l.get_id().clone()).collect();
112        self.leaf_manager
113            .unlock_leaves(unlocking_id.clone(), &leaf_ids_to_remove, true)?;
114        #[cfg(feature = "telemetry")]
115        tracing::trace!(unlocking_id = ?unlocking_id, leaf_ids_to_remove = ?leaf_ids_to_remove, "unlock_leaves");
116
117        Ok(lightning_send_response)
118    }
119
120    /// Create a Lightning invoice with a preimage and a memo.
121    ///
122    /// The invoice will be valid for 30 days by default.
123    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
124    pub async fn create_lightning_invoice(
125        &self,
126        amount_sats: u64,
127        memo: Option<String>,
128        expiry_seconds: Option<i32>,
129    ) -> Result<Bolt11Invoice, SparkSdkError> {
130        // default expiry to 30 days
131        let expiry_seconds = expiry_seconds.unwrap_or(60 * 60 * 24 * 30);
132
133        // generate the preimage
134        // hash the preimage to get the payment hash
135        let preimage_sk = bitcoin::secp256k1::SecretKey::new(&mut OsRng);
136        let preimage_bytes = preimage_sk.secret_bytes();
137        let payment_hash = sha256::digest(&preimage_bytes);
138        let payment_hash_bytes = hex::decode(&payment_hash)
139            .map_err(|err| SparkSdkError::from(IoError::Decoding(err)))?;
140
141        // create the invoice by making a request to the SSP
142        // TODO: we'll need to use fees
143        let (invoice, _fees) = self
144            .create_invoice_with_ssp(
145                amount_sats,
146                payment_hash,
147                expiry_seconds,
148                memo,
149                self.config.spark_config.network.to_bitcoin_network(),
150            )
151            .await?;
152
153        // distribute the preimage shares to the operators
154        // TODO: parallelize this
155        let t = self.config.spark_config.threshold as usize;
156        let n = self.config.spark_config.spark_operators.len();
157        let vss = VSS::new(t, n).unwrap();
158        let shares = vss.split_from_secret_key(&preimage_sk).map_err(|e| {
159            SparkSdkError::from(CryptoError::InvalidInput {
160                field: format!("Failed to split preimage: {}", e),
161            })
162        })?;
163
164        // verify the shares
165        let verification_shares = vss.reconstruct(&shares[..t]).unwrap();
166        if verification_shares.to_bytes().to_vec() != preimage_sk.secret_bytes().to_vec() {
167            return Err(SparkSdkError::from(ValidationError::InvalidInput {
168                field: "Verification failed: shares do not reconstruct to the preimage".to_string(),
169            }));
170        }
171
172        let signing_operators = self.config.spark_config.spark_operators.clone();
173        let identity_pubkey = self.get_spark_address()?;
174
175        let futures = signing_operators.iter().map(|operator| {
176            let operator_id = operator.id;
177            let share = &shares[operator_id as usize];
178            let payment_hash = payment_hash_bytes.clone();
179            let invoice_str = invoice.clone();
180            let threshold = self.config.spark_config.threshold;
181            let config = self.config.clone();
182
183            async move {
184                let mut spark_client = config
185                    .spark_config
186                    .get_spark_connection(Some(operator_id))
187                    .await
188                    .map_err(|e| tonic::Status::internal(format!("Connection error: {}", e)))?;
189
190                let mut request = tonic::Request::new(StorePreimageShareRequest {
191                    payment_hash,
192                    preimage_share: Some(share.marshal_proto()),
193                    threshold,
194                    invoice_string: invoice_str,
195                    user_identity_public_key: identity_pubkey.serialize().to_vec(),
196                });
197                self.add_authorization_header_to_request(&mut request, Some(operator_id));
198                spark_client.store_preimage_share(request).await
199            }
200        });
201
202        futures::future::try_join_all(futures)
203            .await
204            .map_err(|e| SparkSdkError::from(NetworkError::Status(e)))?;
205
206        Bolt11Invoice::from_str(&invoice).map_err(|err| {
207            SparkSdkError::from(ValidationError::InvalidBolt11Invoice(err.to_string()))
208        })
209    }
210}
211
212fn get_invoice_amount_sats(invoice: &Bolt11Invoice) -> Result<u64, SparkSdkError> {
213    let invoice_amount_msats = invoice
214        .amount_milli_satoshis()
215        .ok_or(SparkSdkError::Validation(
216            ValidationError::InvalidBolt11Invoice(invoice.to_string()),
217        ))?;
218
219    Ok(invoice_amount_msats / 1000)
220}