spark_rust/wallet/internal_handlers/implementations/
cooperative_exit.rs1use bitcoin::{key::Secp256k1, secp256k1::PublicKey, OutPoint, Transaction};
2use frost_secp256k1_tr_unofficial::round1::SigningCommitments;
3use spark_protos::spark::{
4 CooperativeExitRequest, LeafRefundTxSigningJob, SigningJob, StartSendTransferRequest, Transfer,
5};
6use std::collections::HashMap;
7use tonic::{async_trait, Request};
8use uuid::Uuid;
9
10use crate::{
11 error::{network::NetworkError, validation::ValidationError, SparkSdkError},
12 signer::{default_signer::marshal_frost_commitments, traits::SparkSigner},
13 wallet::{
14 internal_handlers::traits::{
15 cooperative_exit::CooperativeExitInternalHandlers,
16 transfer::{LeafKeyTweak, LeafRefundSigningData, TransferInternalHandlers},
17 },
18 utils::{
19 bitcoin::{bitcoin_tx_from_bytes, serialize_bitcoin_transaction},
20 sequence::next_sequence,
21 transaction::create_connector_refund_tx,
22 },
23 },
24 SparkSdk,
25};
26
27#[async_trait]
28impl<S: SparkSigner + Send + Sync + Clone + 'static> CooperativeExitInternalHandlers<S>
29 for SparkSdk<S>
30{
31 #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
32 async fn get_connector_refund_signatures(
33 &self,
34 leaves: &Vec<LeafKeyTweak>,
35 exit_txid: &Vec<u8>,
36 connector_outputs: &Vec<OutPoint>,
37 receiver_pubkey: &PublicKey,
38 expiry_time: u64,
39 ) -> Result<(Transfer, HashMap<String, Vec<u8>>), SparkSdkError> {
40 let (transfer, refund_signature_map) = self
41 .sign_coop_exit_refunds(
42 leaves,
43 exit_txid,
44 connector_outputs,
45 receiver_pubkey,
46 expiry_time,
47 )
48 .await?;
49
50 let transfer = self
51 .send_transfer_tweak_key(transfer, leaves, &refund_signature_map)
52 .await?;
53
54 Ok((transfer, refund_signature_map))
55 }
56
57 #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
58 fn create_connector_refund_transaction_signing_job(
59 &self,
60 leaf_id: &str,
61 signing_public_key: &Vec<u8>,
62 commitment: &SigningCommitments,
63 refund_tx: &Transaction,
64 ) -> Result<LeafRefundTxSigningJob, SparkSdkError> {
65 let raw_tx = serialize_bitcoin_transaction(refund_tx)?;
66 let signing_nonce_commitment = Some(marshal_frost_commitments(commitment)?);
67
68 Ok(LeafRefundTxSigningJob {
69 leaf_id: leaf_id.to_string(),
70 refund_tx_signing_job: Some(SigningJob {
71 signing_public_key: signing_public_key.clone(),
72 raw_tx,
73 signing_nonce_commitment,
74 }),
75 })
76 }
77
78 #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
79 async fn sign_coop_exit_refunds(
80 &self,
81 leaves: &Vec<LeafKeyTweak>,
82 exit_txid: &Vec<u8>,
83 connector_outputs: &Vec<OutPoint>,
84 receiver_pubkey: &PublicKey,
85 expiry_time: u64,
86 ) -> Result<(Transfer, HashMap<String, Vec<u8>>), SparkSdkError> {
87 let expiry_time = if expiry_time > i64::MAX as u64 {
88 #[cfg(feature = "telemetry")]
89 tracing::error!("Expiry time exceeds i64::MAX, setting to i64::MAX");
90
91 i64::MAX as u64
92 } else {
93 expiry_time
94 };
95
96 if leaves.len() != connector_outputs.len() {
97 return Err(SparkSdkError::from(ValidationError::InvalidInput {
98 field: "Number of leaves and number of connector outputs must match".to_string(),
99 }));
100 }
101
102 let mut signing_jobs = Vec::new();
103 let mut leaf_data_map = HashMap::new();
104
105 for (i, leaf) in leaves.iter().enumerate() {
106 let connector_output = connector_outputs[i];
107 let current_refund_tx = bitcoin_tx_from_bytes(&leaf.leaf.refund_tx)?;
108
109 let sequence = next_sequence(current_refund_tx.input[0].sequence.0);
110
111 let network = self.config.spark_config.network.to_bitcoin_network();
112 let refund_tx = create_connector_refund_tx(
113 sequence,
114 current_refund_tx.input[0].previous_output,
115 connector_output,
116 leaf.leaf.value,
117 receiver_pubkey,
118 network,
119 );
120
121 let signing_commitment = self.signer.new_frost_signing_noncepair()?;
122 let signing_pubkey = leaf.old_signing_private_key.public_key(&Secp256k1::new());
123
124 let signing_job = self.create_connector_refund_transaction_signing_job(
125 &leaf.leaf.id,
126 &signing_pubkey.serialize().to_vec(),
127 &signing_commitment,
128 &refund_tx,
129 )?;
130
131 signing_jobs.push(signing_job);
132
133 let node_tx = bitcoin_tx_from_bytes(&leaf.leaf.node_tx)?;
134 leaf_data_map.insert(
135 leaf.leaf.id.clone(),
136 LeafRefundSigningData {
137 signing_public_key: signing_pubkey,
138 refund_tx: Some(refund_tx),
139 commitment: marshal_frost_commitments(&signing_commitment)?,
140 tx: node_tx,
141 vout: leaf.leaf.vout,
142 receiving_pubkey: *receiver_pubkey,
143 },
144 );
145 }
146
147 let transfer_id = Uuid::now_v7().to_string();
148 let exit_id = Uuid::now_v7().to_string();
149
150 let mut spark_client = self.config.spark_config.get_spark_connection(None).await?;
152 let mut request = Request::new(CooperativeExitRequest {
153 transfer: Some(StartSendTransferRequest {
154 transfer_id: transfer_id.clone(),
155 leaves_to_send: signing_jobs,
156 owner_identity_public_key: self.get_spark_address()?.serialize().to_vec(),
157 receiver_identity_public_key: receiver_pubkey.serialize().to_vec(),
158 expiry_time: Some(prost_types::Timestamp {
159 seconds: expiry_time as i64,
160 nanos: 0,
161 }),
162 key_tweak_proofs: Default::default(),
163 }),
164 exit_id: exit_id.to_string(),
165 exit_txid: exit_txid.clone(),
166 });
167
168 self.add_authorization_header_to_request(&mut request, None);
169 let exit_response = spark_client
170 .cooperative_exit(request)
171 .await
172 .map_err(|status| SparkSdkError::from(NetworkError::Status(status)))?
173 .into_inner();
174
175 let transfer = exit_response.transfer.ok_or_else(|| {
176 SparkSdkError::from(ValidationError::InvalidInput {
177 field: "Transfer not found in cooperative exit response".to_string(),
178 })
179 })?; let signatures = self.signer.sign_transfer_refunds(
182 &leaf_data_map,
183 &exit_response.signing_results,
184 vec![],
185 )?;
186
187 let mut signature_map = HashMap::new();
188 for leaf_signature in signatures {
189 signature_map.insert(leaf_signature.node_id, leaf_signature.refund_tx_signature);
190 }
191
192 Ok((transfer, signature_map))
193 }
194}