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