spark_rust/wallet/handlers/
cooperative_exit.rs1use crate::{
2 constants::spark::DEFAULT_COOPERATIVE_EXIT_EXPIRY,
3 error::{IoError, SparkSdkError, ValidationError},
4 signer::traits::{derivation_path::SparkKeyType, secp256k1::SparkSignerSecp256k1},
5 wallet::{
6 internal_handlers::traits::{
7 cooperative_exit::CooperativeExitInternalHandlers,
8 leaves::LeavesInternalHandlers,
9 ssp::SspInternalHandlers,
10 transfer::{LeafKeyTweak, TransferInternalHandlers},
11 },
12 leaf_manager::SparkNodeStatus,
13 utils::transaction::create_connector_refund_tx,
14 },
15 SparkSdk,
16};
17use bitcoin::{secp256k1::PublicKey, Address, OutPoint, Transaction, Txid};
18use serde::{Deserialize, Serialize};
19use uuid::Uuid;
20
21#[derive(Serialize, Deserialize, Debug, Clone)]
23pub struct CoopExitRequestId(pub String);
24
25#[derive(Serialize, Deserialize, Debug, Clone)]
26pub struct CoopExitResponse {
27 pub request_id: CoopExitRequestId,
28 pub exit_txid: Txid,
29}
30
31#[derive(Serialize, Deserialize, Debug, Clone)]
32pub enum CoopExitStatus {
33 Pending,
34 Completed,
35}
36
37pub struct RequestCoopExitInput {
39 pub leaf_external_ids: Uuid,
41 pub withdrawal_address: Address,
43}
44
45#[derive(Serialize, Deserialize)]
47pub struct CompleteCoopExitInput {
48 pub user_outbound_transfer_external_id: String,
50 pub coop_exit_request_id: CoopExitRequestId,
52}
53
54#[derive(Serialize, Deserialize, Debug, Clone)]
56pub struct CoopExitRequest {
57 pub id: CoopExitRequestId,
59 pub raw_connector_transaction: Option<Transaction>,
61 pub status: CoopExitStatus,
63 pub created_at: u64,
65 pub updated_at: u64,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct SigningNonce {
72 pub binding: Vec<u8>,
74 pub hiding: Vec<u8>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct SigningCommitment {
81 pub binding: Vec<u8>,
83 pub hiding: Vec<u8>,
85}
86
87impl SparkSdk {
90 #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
101 pub async fn withdraw(
102 &self,
103 onchain_address: &Address,
104 target_amount_sats: Option<u64>,
105 ) -> Result<CoopExitResponse, SparkSdkError> {
106 self.cooperative_exit(onchain_address, target_amount_sats)
108 .await
109 }
110
111 #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
126 async fn cooperative_exit(
127 &self,
128 onchain_address: &Address,
129 target_amount_sats: Option<u64>,
130 ) -> Result<CoopExitResponse, SparkSdkError> {
131 let leaves_to_send = match target_amount_sats {
132 Some(amount) => {
133 self.prepare_leaves_for_amount(amount).await?.leaves
135 }
136 None => {
137 self.leaf_manager
139 .get_available_bitcoin_leaves(None, SparkNodeStatus::CooperativeExit)
140 }
141 };
142
143 if leaves_to_send.is_empty() {
145 return Err(SparkSdkError::from(ValidationError::InvalidInput {
146 field: "No available leaves found".to_string(),
147 }));
148 }
149
150 let mut leaf_key_tweaks = Vec::new();
152 let network = self.config.spark_config.network.to_bitcoin_network();
153 for leaf in &leaves_to_send {
154 let old_signing_private_key = self.signer.expose_leaf_secret_key_for_transfer(
156 leaf.get_id().clone(),
157 SparkKeyType::BaseSigning,
158 0,
159 network,
160 )?;
161 let new_signing_public_key = self.signer.new_ephemeral_keypair()?;
163
164 leaf_key_tweaks.push(LeafKeyTweak {
165 leaf: leaf.get_tree_node()?,
166 old_signing_private_key,
167 new_signing_public_key,
168 });
169 }
170
171 let leaf_ids = leaves_to_send
173 .iter()
174 .map(|leaf| leaf.get_id().clone())
175 .collect();
176
177 let initiate_response = self
178 .initiate_cooperative_exit_with_ssp(leaf_ids, onchain_address)
179 .await?;
180
181 let connector_txid = initiate_response.connector_tx.compute_txid();
183 let coop_exit_txid = initiate_response.connector_tx.input[0].previous_output.txid;
184
185 let mut connector_outputs = Vec::new();
187 let output_count = initiate_response.connector_tx.output.len();
190 for i in 0..output_count.saturating_sub(1) {
191 connector_outputs.push(OutPoint {
192 txid: connector_txid,
193 vout: i as u32,
194 });
195 }
196
197 let coop_exit_txid_bytes = hex::decode(coop_exit_txid.to_string())
199 .map_err(|err| SparkSdkError::from(IoError::Decoding(err)))?;
200
201 let expiry = std::time::SystemTime::now()
202 .duration_since(std::time::UNIX_EPOCH)
203 .unwrap()
204 .as_secs()
205 + DEFAULT_COOPERATIVE_EXIT_EXPIRY;
206 let (transfer, _) = self
207 .get_connector_refund_signatures(
208 &leaf_key_tweaks,
209 &coop_exit_txid_bytes,
210 &connector_outputs,
211 &self.config.spark_config.ssp_identity_public_key,
212 expiry,
213 )
214 .await?;
215
216 let complete_response = match self
218 .complete_cooperative_exit_with_ssp(transfer.id.clone(), initiate_response.request_id)
219 .await
220 {
221 Ok(response) => CoopExitResponse {
222 request_id: response,
223 exit_txid: coop_exit_txid,
224 },
225 Err(status) => {
226 if let Err(cancel_err) = self.cancel_send_transfer(transfer.id.clone()).await {
228 #[cfg(feature = "telemetry")]
230 tracing::error!(
231 "Failed to cancel transfer after complete_cooperative_exit error: {}",
232 cancel_err
233 );
234 }
235 return Err(status);
236 }
237 };
238
239 Ok(complete_response)
240 }
241
242 pub fn create_connector_refund_transaction(
259 &self,
260 sequence: u32,
261 node_outpoint: OutPoint,
262 connector_output: OutPoint,
263 amount_sats: u64,
264 receiver_pubkey: &PublicKey,
265 ) -> Transaction {
266 create_connector_refund_tx(
268 sequence,
269 node_outpoint,
270 connector_output,
271 amount_sats,
272 receiver_pubkey,
273 self.config.spark_config.network.to_bitcoin_network(),
274 )
275 }
276}