spark_sdk/wallet/internal_handlers/implementations/
cooperative_exit.rs

1use 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(); // TODO: check when Bitcoin transaction encoding fail, you may need to handle this.
63
64        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        })?; // `transfer` is always Some.
163
164        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}