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 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 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 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 let secp = bitcoin::secp256k1::Secp256k1::new();
137 let private_as_pubkey = &leaf.old_signing_private_key.public_key(&secp);
138 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}