spark_rust/wallet/handlers/
swap.rs

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