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;
8use uuid::Uuid;
9
10use crate::{
11 error::{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 request_data = CooperativeExitRequest {
152 transfer: Some(StartSendTransferRequest {
153 transfer_id: transfer_id.clone(),
154 leaves_to_send: signing_jobs,
155 owner_identity_public_key: self.get_spark_address()?.serialize().to_vec(),
156 receiver_identity_public_key: receiver_pubkey.serialize().to_vec(),
157 expiry_time: Some(prost_types::Timestamp {
158 seconds: expiry_time as i64,
159 nanos: 0,
160 }),
161 key_tweak_proofs: Default::default(),
162 }),
163 exit_id: exit_id.to_string(),
164 exit_txid: exit_txid.clone(),
165 };
166
167 let exit_response = self
168 .config
169 .spark_config
170 .call_with_retry(
171 request_data,
172 |mut client, req| Box::pin(async move { client.cooperative_exit(req).await }),
173 None,
174 )
175 .await?;
176
177 let transfer = exit_response.transfer.ok_or_else(|| {
178 SparkSdkError::from(ValidationError::InvalidInput {
179 field: "Transfer not found in cooperative exit response".to_string(),
180 })
181 })?; let signatures = self.signer.sign_transfer_refunds(
184 &leaf_data_map,
185 &exit_response.signing_results,
186 vec![],
187 )?;
188
189 let mut signature_map = HashMap::new();
190 for leaf_signature in signatures {
191 signature_map.insert(leaf_signature.node_id, leaf_signature.refund_tx_signature);
192 }
193
194 Ok((transfer, signature_map))
195 }
196}