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::{
11    bitcoin_tx_from_bytes, parse_public_key, serialize_bitcoin_transaction, sighash_from_tx,
12};
13use crate::wallet::utils::bitcoin::{
14    compute_taproot_key_no_script_from_internal_key, parse_secret_key,
15};
16use crate::with_handler_lock;
17use crate::SparkSdk;
18use bitcoin::key::Secp256k1;
19use spark_cryptography::adaptor_signature::apply_adaptor_to_signature;
20use spark_cryptography::adaptor_signature::generate_adaptor_from_signature;
21use spark_cryptography::adaptor_signature::generate_signature_from_existing_adaptor;
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 old_signing_private_key = self.signer.expose_leaf_secret_key_for_transfer(
112                leaf.get_id().clone(),
113                SparkKeyType::BaseSigning,
114                0,
115                self.config.spark_config.network.to_bitcoin_network(),
116            )?;
117
118            // Generate new private key for the leaf
119            let new_signing_public_key = self.signer.new_ephemeral_keypair()?;
120
121            leaf_key_tweaks.push(LeafKeyTweak {
122                leaf: tree_leaf,
123                old_signing_private_key,
124                new_signing_public_key,
125            });
126        }
127
128        let expiry_time = chrono::Utc::now().timestamp() as u64 + DEFAULT_TRANSFER_EXPIRY;
129
130        let (transfer, refund_signature_map) = self
131            .send_transfer_sign_refunds(
132                &leaf_key_tweaks,
133                &self.config.spark_config.ssp_identity_public_key,
134                expiry_time,
135            )
136            .await?;
137
138        let leaf = transfer.leaves[0].leaf.as_ref().unwrap();
139        let leaf_refund_signature = refund_signature_map[&leaf.id.to_string()].clone();
140
141        let adaptor_signature =
142            generate_adaptor_from_signature(leaf_refund_signature.as_slice().try_into().unwrap())
143                .unwrap();
144
145        let mut user_leaves = Vec::new();
146        user_leaves.push(SwapLeaf {
147            leaf_id: transfer.leaves[0].leaf.as_ref().unwrap().id.to_string(),
148            raw_unsigned_refund_transaction: hex::encode(serialize_bitcoin_transaction(
149                &transfer.leaves[0].intermediate_refund_tx,
150            )?),
151            adaptor_added_signature: hex::encode(adaptor_signature.signature),
152        });
153
154        for leaf in transfer.leaves.iter().skip(1) {
155            let leaf_id = leaf.leaf.as_ref().unwrap().id;
156            let leaf_refund_signature = refund_signature_map[&leaf_id.to_string()].clone();
157
158            let signature = generate_signature_from_existing_adaptor(
159                &leaf_refund_signature,
160                adaptor_signature.adaptor_private_key.as_slice(),
161            )
162            .unwrap();
163
164            user_leaves.push(SwapLeaf {
165                leaf_id: leaf_id.to_string(),
166                raw_unsigned_refund_transaction: hex::encode(serialize_bitcoin_transaction(
167                    &leaf.intermediate_refund_tx,
168                )?),
169                adaptor_added_signature: hex::encode(signature),
170            });
171        }
172
173        // total leaf value
174        let total_amount = leaf_key_tweaks
175            .iter()
176            .map(|leaf| leaf.leaf.value)
177            .sum::<u64>();
178
179        let secp = Secp256k1::new();
180        let adaptor_private_key =
181            parse_secret_key(&adaptor_signature.adaptor_private_key.to_vec())?;
182        let adaptor_public_key = adaptor_private_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                network: network.marshal_proto(),
204            };
205
206            let response = self
207                .config
208                .spark_config
209                .call_with_retry(
210                    request_data,
211                    |mut client, req| Box::pin(async move { client.query_nodes(req).await }),
212                    None,
213                )
214                .await?;
215
216            let node = response
217                .nodes
218                .get(&leaf_id)
219                .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?;
220
221            #[cfg(feature = "telemetry")]
222            tracing::trace!(
223                leaf_id = leaf_id.clone(),
224                "Leaf balance returned for SSP swap: {:?}",
225                node.value
226            );
227
228            let node_tx = bitcoin_tx_from_bytes(&node.node_tx)?;
229            let refund_tx_bytes = hex::decode(&leaf.raw_unsigned_refund_transaction)
230                .map_err(|err| SparkSdkError::from(IoError::Decoding(err)))?;
231            let refund_tx = bitcoin_tx_from_bytes(&refund_tx_bytes)?;
232
233            let sighash = sighash_from_tx(&refund_tx, 0, &node_tx.output[0])?;
234
235            // First, parse the public key from the node
236            let verifying_public_key = parse_public_key(&node.verifying_public_key)?;
237
238            let taproot_key = compute_taproot_key_no_script_from_internal_key(
239                &verifying_public_key.x_only_public_key().0.serialize(),
240            )?;
241
242            let adaptor_signature_bytes = hex::decode(&leaf.adaptor_added_signature)
243                .map_err(|err| SparkSdkError::from(IoError::Decoding(err)))?;
244
245            let _ = apply_adaptor_to_signature(
246                &taproot_key,
247                &sighash,
248                &adaptor_signature_bytes,
249                &adaptor_private_key.secret_bytes(),
250            )
251            .unwrap();
252        }
253
254        let transfer_id = transfer.id;
255        self.send_transfer_tweak_key(&transfer, &leaf_key_tweaks, &refund_signature_map)
256            .await?;
257
258        // TODO: print the UUID in the response
259        let _completion = self
260            .complete_leaves_swap_with_ssp(
261                hex::encode(adaptor_private_key.secret_bytes()),
262                transfer_id.to_string(),
263                request_id.clone(),
264            )
265            .await?;
266
267        self.claim_transfers_internal().await?;
268
269        // Unlock and delete old leaves.
270        let leaf_ids = available_leaves
271            .leaves
272            .iter()
273            .map(|leaf| leaf.get_id().clone())
274            .collect();
275        self.leaf_manager
276            .unlock_leaves(available_leaves.unlocking_id.unwrap(), &leaf_ids, true)?;
277
278        // return the leaf ID with the amount that was swapped
279        let leaf_id = leaves[0].leaf_id.clone();
280
281        Ok(leaf_id)
282    }
283}