spark_rust/wallet/internal_handlers/implementations/
lightning.rs

1use crate::error::SparkSdkError;
2use crate::signer::traits::SparkSigner;
3use crate::wallet::internal_handlers::traits::commitments::StatechainCommitmentsInternalHandlers;
4use crate::wallet::internal_handlers::traits::lightning::LightningInternalHandlers;
5use crate::wallet::internal_handlers::traits::transfer::LeafKeyTweak;
6use crate::wallet::utils::bitcoin::{
7    bitcoin_tx_from_bytes, serialize_bitcoin_transaction, sighash_from_tx,
8};
9use crate::wallet::utils::sequence::next_sequence;
10use crate::wallet::utils::transaction::create_refund_tx;
11use crate::SparkSdk;
12use bitcoin::hashes;
13use bitcoin::hashes::Hash;
14use bitcoin::secp256k1::PublicKey;
15use lightning_invoice::Bolt11Invoice;
16use spark_protos::spark::initiate_preimage_swap_request::Reason;
17use spark_protos::spark::InitiatePreimageSwapRequest;
18use spark_protos::spark::InitiatePreimageSwapResponse;
19use spark_protos::spark::InvoiceAmount;
20use spark_protos::spark::InvoiceAmountProof;
21use spark_protos::spark::RequestedSigningCommitments;
22use spark_protos::spark::SigningCommitments;
23use spark_protos::spark::StartSendTransferRequest;
24use spark_protos::spark::UserSignedRefund;
25use tonic::async_trait;
26use uuid::Uuid;
27
28#[cfg(test)]
29use spark_protos::spark::ProvidePreimageRequest;
30#[cfg(test)]
31use spark_protos::spark::Transfer;
32
33#[async_trait]
34impl<S: SparkSigner + Send + Sync + Clone + 'static> LightningInternalHandlers<S> for SparkSdk<S> {
35    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
36    async fn swap_nodes_for_preimage(
37        &self,
38        leaves: Vec<LeafKeyTweak>,
39        receiver_pubkey: &PublicKey,
40        payment_hash: &hashes::sha256::Hash,
41        invoice: &Bolt11Invoice,
42        invoice_amount_sats: u64,
43        fee_sats: u64,
44        is_inbound_payment: bool,
45    ) -> Result<InitiatePreimageSwapResponse, SparkSdkError> {
46        // get signing commitments
47        let node_ids: Vec<String> = leaves.iter().map(|l| l.leaf.id.clone()).collect();
48        let spark_commitments = self.get_spark_signing_commitments(node_ids).await?;
49
50        // get user signed refunds
51        let user_signed_refunds = self
52            .sign_lightning_refunds(leaves, spark_commitments, receiver_pubkey)
53            .await?;
54
55        let transfer_id = Uuid::now_v7().to_string();
56        let reason = if is_inbound_payment {
57            Reason::Receive
58        } else {
59            Reason::Send
60        };
61
62        let request_data = InitiatePreimageSwapRequest {
63            payment_hash: payment_hash.to_byte_array().to_vec(),
64            user_signed_refunds,
65            reason: reason as i32,
66            invoice_amount: Some(InvoiceAmount {
67                invoice_amount_proof: Some(InvoiceAmountProof {
68                    bolt11_invoice: invoice.to_string(),
69                }),
70                value_sats: invoice_amount_sats,
71            }),
72            transfer: Some(StartSendTransferRequest {
73                transfer_id: transfer_id.clone(),
74                owner_identity_public_key: self.get_spark_address()?.serialize().to_vec(),
75                receiver_identity_public_key: receiver_pubkey.serialize().to_vec(),
76                expiry_time: Default::default(),
77                leaves_to_send: Default::default(),
78                key_tweak_proofs: Default::default(),
79            }),
80            receiver_identity_public_key: receiver_pubkey.serialize().to_vec(),
81            fee_sats,
82        };
83
84        let time_start = std::time::Instant::now();
85        let response = self
86            .config
87            .spark_config
88            .call_with_retry(
89                request_data,
90                |mut client, req| Box::pin(async move { client.initiate_preimage_swap(req).await }),
91                None,
92            )
93            .await?;
94
95        let duration = time_start.elapsed();
96        #[cfg(feature = "telemetry")]
97        tracing::debug!(duration = ?duration, "initiate_preimage_swap");
98
99        Ok(response)
100    }
101
102    async fn sign_lightning_refunds(
103        &self,
104        leaves: Vec<LeafKeyTweak>,
105        spark_commitments: Vec<RequestedSigningCommitments>,
106        receiver_pubkey: &PublicKey,
107    ) -> Result<Vec<UserSignedRefund>, SparkSdkError> {
108        // sign refunds. TODO: In JS SDK, this is the `sign_refunds` function
109        let mut user_signed_refunds = Vec::with_capacity(leaves.len());
110        let bitcoin_network = self.config.spark_config.network.to_bitcoin_network();
111        for (i, leaf) in leaves.iter().enumerate() {
112            let node_tx = bitcoin_tx_from_bytes(&leaf.leaf.node_tx)?;
113            let node_outpoint = bitcoin::OutPoint {
114                txid: node_tx.compute_txid(),
115                vout: 0,
116            };
117
118            let curr_refund_tx = bitcoin_tx_from_bytes(&leaf.leaf.refund_tx)?;
119            let next_sequence = next_sequence(curr_refund_tx.input[0].sequence.0);
120            let amount_sats = curr_refund_tx.output[0].value;
121
122            let new_refund_tx = create_refund_tx(
123                next_sequence,
124                node_outpoint,
125                amount_sats,
126                receiver_pubkey,
127                bitcoin_network,
128            );
129
130            let sighash = sighash_from_tx(&new_refund_tx, 0, &node_tx.output[0])?;
131
132            let self_commitment = self.signer.generate_frost_signing_commitments()?;
133            let spark_commitment = spark_commitments[i].signing_nonce_commitments.clone();
134
135            // todo: clean start
136            let secp = bitcoin::secp256k1::Secp256k1::new();
137            let private_as_pubkey = &leaf.old_signing_private_key.public_key(&secp);
138            // todo: clean end
139
140            let user_signature_share = self.signer.sign_frost_new(
141                sighash.to_vec(),
142                private_as_pubkey.serialize().to_vec(),
143                leaf.leaf.verifying_public_key.clone(),
144                self_commitment,
145                spark_commitment.clone(),
146                None,
147            )?;
148
149            user_signed_refunds.push(UserSignedRefund {
150                node_id: leaf.leaf.id.clone(),
151                refund_tx: serialize_bitcoin_transaction(&new_refund_tx)?,
152                user_signature: user_signature_share,
153                user_signature_commitment: Some(marshal_frost_commitments(&self_commitment)?),
154                signing_commitments: Some(SigningCommitments {
155                    signing_commitments: spark_commitment,
156                }),
157                network: self.config.spark_config.network.marshal_proto(),
158            });
159        }
160
161        Ok(user_signed_refunds)
162    }
163
164    #[cfg(test)]
165    async fn provide_preimage(&self, preimage: Vec<u8>) -> Result<Transfer, SparkSdkError> {
166        use crate::error::{IoError, ValidationError};
167
168        let payment_hash = sha256::digest(&preimage);
169        let payment_hash_bytes = hex::decode(&payment_hash)
170            .map_err(|err| SparkSdkError::from(IoError::Decoding(err)))?;
171
172        let request_data = ProvidePreimageRequest {
173            payment_hash: payment_hash_bytes,
174            preimage,
175            identity_public_key: Default::default(),
176        };
177
178        let response = self
179            .config
180            .spark_config
181            .call_with_retry(
182                request_data,
183                |mut client, req| Box::pin(async move { client.provide_preimage(req).await }),
184                None,
185            )
186            .await?;
187
188        if response.transfer.is_none() {
189            return Err(SparkSdkError::from(ValidationError::InvalidInput {
190                field: "Transfer not found".to_string(),
191            }));
192        }
193
194        Ok(response.transfer.unwrap())
195    }
196}
197
198use crate::common_types::types::frost::FrostSigningCommitments;
199use spark_protos::common::SigningCommitment as ProtoSigningCommitments;
200
201pub(crate) fn marshal_frost_commitments(
202    commitments: &FrostSigningCommitments,
203) -> Result<ProtoSigningCommitments, SparkSdkError> {
204    let hiding = commitments.hiding().serialize().unwrap();
205    let binding = commitments.binding().serialize().unwrap();
206
207    Ok(ProtoSigningCommitments { hiding, binding })
208}