spark_rust/wallet/internal_handlers/implementations/
cooperative_exit.rs

1use 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        // call coop exit on Spark operators
151        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        })?; // `transfer` is always Some.
182
183        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}