spark_rust/wallet/handlers/
swap.rs

1use crate::constants::spark::DEFAULT_TRANSFER_EXPIRY;
2use crate::error::{IoError, NetworkError, SparkSdkError};
3use crate::signer::traits::derivation_path::SparkKeyType;
4use crate::signer::traits::SparkSigner;
5use crate::wallet::internal_handlers::traits::ssp::SspInternalHandlers;
6use crate::wallet::internal_handlers::traits::ssp::SwapLeaf;
7use crate::wallet::internal_handlers::traits::transfer::LeafKeyTweak;
8use crate::wallet::internal_handlers::traits::transfer::TransferInternalHandlers;
9use crate::wallet::leaf_manager::SparkNodeStatus;
10use crate::wallet::utils::bitcoin::{bitcoin_tx_from_bytes, parse_public_key, sighash_from_tx};
11use crate::wallet::utils::bitcoin::{
12    compute_taproot_key_no_script_from_internal_key, parse_secret_key,
13};
14use crate::with_handler_lock;
15use crate::SparkSdk;
16use bitcoin::key::Secp256k1;
17use spark_cryptography::adaptor_signature::apply_adaptor_to_signature;
18use spark_cryptography::adaptor_signature::generate_adaptor_from_signature;
19use spark_cryptography::adaptor_signature::generate_signature_from_existing_adaptor;
20use spark_protos::spark::query_nodes_request::Source;
21use spark_protos::spark::QueryNodesRequest;
22use spark_protos::spark::TreeNodeIds;
23
24impl<S: SparkSigner + Send + Sync + Clone + 'static> SparkSdk<S> {
25    /// Optimizes your wallet's leaf structure by swapping your current leaves with the Spark Service Provider (SSP).
26    ///
27    /// This function allows you to obtain leaves of specific denominations by swapping your existing
28    /// leaves with the SSP. This is particularly useful when you need to transfer a specific amount
29    /// but don't have a leaf of that exact denomination.
30    ///
31    /// For example, if you have a single leaf of 100,000 satoshis but need to send 80,000 satoshis,
32    /// this function will swap with the SSP to get leaves totaling 100,000 satoshis but with
33    /// denominations that include the 80,000 you need. The SSP typically provides leaves in
34    /// power-of-2 denominations for optimal efficiency.
35    ///
36    /// The swap process involves:
37    /// 1. Locking all your available Bitcoin leaves
38    /// 2. Preparing leaf key tweaks for each leaf
39    /// 3. Creating a transfer to the SSP with all your available leaves
40    /// 4. Using cryptographic adaptor signatures for security
41    /// 5. Requesting new leaves from the SSP with your desired target amount
42    /// 6. Verifying the cryptographic integrity of the returned leaves
43    /// 7. Completing the swap process and claiming the new leaves
44    /// 8. Deleting your old leaves
45    ///
46    /// # Arguments
47    ///
48    /// * `target_amount` - The amount (in satoshis) you want to have in a specific leaf after the swap
49    ///
50    /// # Returns
51    ///
52    /// * `Ok(String)` - The ID of the newly created leaf with the target amount if successful
53    /// * `Err(SparkSdkError)` - If there was an error during the swap process
54    ///
55    /// # Example
56    ///
57    /// ```
58    /// # use spark_rust::SparkSdk;
59    /// # use bitcoin::secp256k1::PublicKey;
60    /// # use std::str::FromStr;
61    /// # async fn example(sdk: SparkSdk) -> Result<(), Box<dyn std::error::Error>> {
62    /// // Let's say you have a single leaf of 100,000 satoshis but need to send 80,000
63    /// let target_amount = 80_000;
64    ///
65    /// // Request a swap with the SSP to get optimized leaves
66    /// let new_leaf_id = sdk.request_leaves_swap(target_amount).await?;
67    /// println!("Created new leaf with ID: {}", new_leaf_id);
68    ///
69    /// // Now you can transfer exactly 80,000 satoshis
70    /// let receiver_spark_address = PublicKey::from_str(
71    ///     "02782d7ba8764306bd324e23082f785f7c880b7202cb10c85a2cb96496aedcaba7"
72    /// ).unwrap();
73    /// sdk.transfer(target_amount, &receiver_spark_address).await?;
74    /// # Ok(())
75    /// # }
76    /// ```
77    ///
78    /// # Note
79    ///
80    /// Leaves swaps incur a service fee charged by the SSP. You can estimate this fee before
81    /// initiating a swap using the `get_leaves_swap_fee_estimate` method. The swap operates on
82    /// all available leaves in your wallet, so the total balance will remain the same (minus fees),
83    /// but the denomination structure will change.
84    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
85    pub async fn request_leaves_swap(&self, target_amount: u64) -> Result<String, SparkSdkError> {
86        with_handler_lock!(self, async {
87            self.request_leaves_swap_internal(target_amount).await
88        })
89        .await
90    }
91
92    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
93    pub(crate) async fn request_leaves_swap_internal(
94        &self,
95        target_amount: u64,
96    ) -> Result<String, SparkSdkError> {
97        // Lock all availables leaves to provide to SSP. This allows the SSP to aggregate
98        // and optimize the number of leaves.
99        let available_leaves = self
100            .leaf_manager
101            .lock_available_bitcoin_leaves(SparkNodeStatus::Swap);
102
103        // leaf selection for atomic swap
104        let mut leaf_key_tweaks = Vec::with_capacity(available_leaves.leaves.len());
105
106        for leaf in available_leaves.leaves.iter() {
107            let tree_leaf = leaf.get_tree_node()?;
108
109            let old_signing_private_key = self.signer.expose_leaf_secret_key_for_transfer(
110                leaf.get_id().clone(),
111                SparkKeyType::BaseSigning,
112                0,
113                self.config.spark_config.network.to_bitcoin_network(),
114            )?;
115
116            // Generate new private key for the leaf
117            let new_signing_public_key = self.signer.new_ephemeral_keypair()?;
118
119            leaf_key_tweaks.push(LeafKeyTweak {
120                leaf: tree_leaf,
121                old_signing_private_key,
122                new_signing_public_key,
123            });
124        }
125
126        let expiry_time = chrono::Utc::now().timestamp() as u64 + DEFAULT_TRANSFER_EXPIRY;
127
128        let (transfer, refund_signature_map) = self
129            .send_transfer_sign_refunds(
130                &leaf_key_tweaks,
131                &self.config.spark_config.ssp_identity_public_key,
132                expiry_time,
133            )
134            .await?;
135
136        let leaf = transfer.leaves[0].leaf.as_ref().unwrap();
137        let leaf_refund_signature = refund_signature_map[&leaf.id].clone();
138
139        let adaptor_signature =
140            generate_adaptor_from_signature(leaf_refund_signature.as_slice().try_into().unwrap())
141                .unwrap();
142
143        let mut user_leaves = Vec::new();
144        user_leaves.push(SwapLeaf {
145            leaf_id: transfer.leaves[0].leaf.as_ref().unwrap().id.clone(),
146            raw_unsigned_refund_transaction: hex::encode(
147                &transfer.leaves[0].intermediate_refund_tx,
148            ),
149            adaptor_added_signature: hex::encode(adaptor_signature.signature),
150        });
151
152        for leaf in transfer.leaves.iter().skip(1) {
153            let leaf_id = leaf.leaf.as_ref().unwrap().id.clone();
154            let leaf_refund_signature = refund_signature_map[&leaf_id].clone();
155
156            let signature = generate_signature_from_existing_adaptor(
157                &leaf_refund_signature,
158                adaptor_signature.adaptor_private_key.as_slice(),
159            )
160            .unwrap();
161
162            user_leaves.push(SwapLeaf {
163                leaf_id,
164                raw_unsigned_refund_transaction: hex::encode(&leaf.intermediate_refund_tx),
165                adaptor_added_signature: hex::encode(signature),
166            });
167        }
168
169        // total leaf value
170        let total_amount = leaf_key_tweaks
171            .iter()
172            .map(|leaf| leaf.leaf.value)
173            .sum::<u64>();
174
175        let secp = Secp256k1::new();
176        let adaptor_private_key =
177            parse_secret_key(&adaptor_signature.adaptor_private_key.to_vec())?;
178        let adaptor_public_key = adaptor_private_key.public_key(&secp);
179
180        let (request_id, leaves) = self
181            .request_swap_leaves_with_ssp(
182                hex::encode(adaptor_public_key.serialize()),
183                total_amount,
184                target_amount,
185                0,
186                user_leaves,
187            )
188            .await?;
189
190        let network = self.config.spark_config.network;
191
192        for leaf in &leaves {
193            let leaf_id = leaf.leaf_id.clone();
194            let request_data = QueryNodesRequest {
195                source: Some(Source::NodeIds(TreeNodeIds {
196                    node_ids: vec![leaf_id.clone()],
197                })),
198                include_parents: Default::default(),
199                network: network.marshal_proto(),
200            };
201
202            let response = self
203                .config
204                .spark_config
205                .call_with_retry(
206                    request_data,
207                    |mut client, req| Box::pin(async move { client.query_nodes(req).await }),
208                    None,
209                )
210                .await?;
211
212            let node = response
213                .nodes
214                .get(&leaf_id)
215                .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
216
217            #[cfg(feature = "telemetry")]
218            tracing::trace!(
219                leaf_id = leaf_id.clone(),
220                "Leaf balance returned for SSP swap: {:?}",
221                node.value
222            );
223
224            let node_tx = bitcoin_tx_from_bytes(&node.node_tx)?;
225            let refund_tx_bytes = hex::decode(&leaf.raw_unsigned_refund_transaction)
226                .map_err(|err| SparkSdkError::from(IoError::Decoding(err)))?;
227            let refund_tx = bitcoin_tx_from_bytes(&refund_tx_bytes)?;
228
229            let sighash = sighash_from_tx(&refund_tx, 0, &node_tx.output[0])?;
230
231            // First, parse the public key from the node
232            let verifying_public_key = parse_public_key(&node.verifying_public_key)?;
233
234            let taproot_key = compute_taproot_key_no_script_from_internal_key(
235                &verifying_public_key.x_only_public_key().0.serialize(),
236            )?;
237
238            let adaptor_signature_bytes = hex::decode(&leaf.adaptor_added_signature)
239                .map_err(|err| SparkSdkError::from(IoError::Decoding(err)))?;
240
241            let _ = apply_adaptor_to_signature(
242                &taproot_key,
243                &sighash,
244                &adaptor_signature_bytes,
245                &adaptor_private_key.secret_bytes(),
246            )
247            .unwrap();
248        }
249
250        let transfer_id = transfer.id.clone();
251        self.send_transfer_tweak_key(transfer.clone(), &leaf_key_tweaks, &refund_signature_map)
252            .await?;
253
254        // TODO: print the UUID in the response
255        let _completion = self
256            .complete_leaves_swap_with_ssp(
257                hex::encode(adaptor_private_key.secret_bytes()),
258                transfer_id,
259                request_id.clone(),
260            )
261            .await?;
262
263        self.claim_transfers_internal().await?;
264
265        // Unlock and delete old leaves.
266        let leaf_ids = available_leaves
267            .leaves
268            .iter()
269            .map(|leaf| leaf.get_id().clone())
270            .collect();
271        self.leaf_manager
272            .unlock_leaves(available_leaves.unlocking_id.unwrap(), &leaf_ids, true)?;
273
274        // return the leaf ID with the amount that was swapped
275        let leaf_id = leaves[0].leaf_id.clone();
276
277        Ok(leaf_id)
278    }
279}