spark_rust/wallet/handlers/
timelock.rs

1use hashbrown::HashMap;
2use spark_protos::spark::{query_nodes_request::Source, QueryNodesRequest, TreeNodeIds};
3
4use crate::{
5    error::{SparkSdkError, WalletError},
6    signer::traits::{derivation_path::SparkKeyType, SparkSigner},
7    wallet::{
8        internal_handlers::traits::timelock::TimelockInternalHandlers,
9        leaf_manager::{SparkLeaf, SparkNodeStatus},
10        utils::{bitcoin::bitcoin_tx_from_bytes, sequence::next_sequence},
11    },
12    SparkSdk,
13};
14
15impl<S: SparkSigner + Send + Sync + Clone + 'static> SparkSdk<S> {
16    /// Refreshes timelock nodes in the wallet to handle timelock-related operations.
17    ///
18    /// This internal method updates the timelock state of Bitcoin leaves in the wallet.
19    /// It handles the following timelock-related operations:
20    ///
21    /// 1. If a specific node ID is provided, it refreshes only that node
22    /// 2. If no node ID is provided, it refreshes all Bitcoin leaves that are eligible for
23    ///    timelock updates (based on sequence number progression)
24    /// 3. Queries the Spark network for node information
25    /// 4. Generates new signing keys for refreshed nodes
26    /// 5. Updates the leaf manager with the refreshed nodes
27    ///
28    /// Timelock nodes are part of Spark's security model, where certain operations become
29    /// valid only after a specific time has passed. This method ensures that the wallet
30    /// properly tracks and updates the timelock state of its leaves.
31    ///
32    /// # Arguments
33    ///
34    /// * `node_id` - Optional ID of a specific node to refresh. If None, all eligible nodes are refreshed.
35    ///
36    /// # Returns
37    ///
38    /// * `Ok(())` - If the refresh operation was successful
39    /// * `Err(SparkSdkError)` - If there was an error during the refresh process
40    ///
41    /// # Internal Usage
42    ///
43    /// This method is called by several public methods in the SDK:
44    /// - `sync_wallet()` - To ensure all timelock nodes are up-to-date
45    /// - `transfer()` - Before selecting leaves for transfer
46    /// - `pay_lightning_invoice()` - Before selecting leaves for payment
47    /// - Other operations that require fresh leaf state
48    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
49    pub(crate) async fn refresh_timelock_nodes(
50        &self,
51        node_id: Option<String>,
52    ) -> Result<(), SparkSdkError> {
53        let mut nodes_to_refresh = vec![];
54        let mut node_ids = vec![];
55
56        match node_id {
57            Some(leaf_id) => {
58                let leaf_in_array = self
59                    .leaf_manager
60                    .filter_nodes_by_ids(&vec![leaf_id.clone()]);
61                if leaf_in_array.is_empty() {
62                    return Err(SparkSdkError::from(
63                        WalletError::LeafNotFoundAfterOperation {
64                            leaf_id: leaf_id.clone(),
65                        },
66                    ));
67                }
68                let leaf = leaf_in_array.first().unwrap();
69                if !leaf.is_bitcoin() {
70                    return Err(SparkSdkError::from(WalletError::LeafIsNotBitcoin {
71                        leaf_id: leaf.get_id().clone(),
72                    }));
73                }
74
75                nodes_to_refresh.push(leaf.get_tree_node()?);
76                node_ids.push(leaf_id);
77            }
78            None => {
79                let lock_callback = Some(Box::new(|leaf: &SparkLeaf| -> bool {
80                    if !leaf.is_bitcoin() {
81                        return false;
82                    }
83
84                    let leaf_node = leaf.get_tree_node().unwrap();
85                    let refund_tx = bitcoin_tx_from_bytes(&leaf_node.refund_tx);
86                    if refund_tx.is_err() {
87                        return false;
88                    }
89
90                    let refund_tx = refund_tx.unwrap();
91                    let current_sequence = refund_tx.input[0].sequence.0;
92                    let next_sequence = next_sequence(current_sequence);
93
94                    next_sequence == 0 || next_sequence > current_sequence // If 0 or if overflowed
95                }) as Box<dyn Fn(&SparkLeaf) -> bool>);
96
97                let (leaves, _unlocking_id) = self.leaf_manager.get_all_available_leaves(
98                    lock_callback,
99                    Some(SparkNodeStatus::RefreshTimelock),
100                );
101                node_ids = leaves.iter().map(|n| n.get_id().clone()).collect();
102                nodes_to_refresh = leaves.iter().map(|n| n.get_tree_node().unwrap()).collect();
103            }
104        };
105
106        if nodes_to_refresh.is_empty() {
107            #[cfg(feature = "telemetry")]
108            tracing::debug!("No leaf needs timelock refresh");
109
110            return Ok(());
111        }
112
113        let request = QueryNodesRequest {
114            source: Some(Source::NodeIds(TreeNodeIds { node_ids })),
115            include_parents: true,
116            network: self.config.spark_config.network.marshal_proto(),
117        };
118
119        let node_response = self
120            .config
121            .spark_config
122            .call_with_retry(
123                request,
124                |mut client, req| Box::pin(async move { client.query_nodes(req).await }),
125                None,
126            )
127            .await?;
128
129        let nodes_response = node_response.nodes;
130        let mut nodes_map = HashMap::new();
131        for (id, node) in nodes_response {
132            nodes_map.insert(id, node);
133        }
134
135        for node in nodes_to_refresh {
136            let parent_node_id = node.clone().parent_node_id.ok_or(SparkSdkError::from(
137                WalletError::LeafHasNoParent {
138                    leaf_id: node.id.clone(),
139                },
140            ))?;
141
142            let parent_node = nodes_map.get(&parent_node_id).ok_or(SparkSdkError::from(
143                WalletError::LeafParentNotFound {
144                    leaf_id: node.id.clone(),
145                },
146            ))?;
147
148            let signing_public_key = self.signer.new_secp256k1_keypair(
149                node.id.clone(),
150                SparkKeyType::BaseSigning,
151                0,
152                self.config.spark_config.network.to_bitcoin_network(),
153            )?;
154
155            let nodes = self
156                .refresh_timelock_transfer_nodes(
157                    &vec![node.clone()],
158                    parent_node.clone(),
159                    &signing_public_key,
160                )
161                .await?;
162
163            if nodes.len() != 1 {
164                return Err(SparkSdkError::from(
165                    WalletError::PostRefreshNodeSignatureLengthMismatch,
166                ));
167            }
168
169            let new_node = nodes.first().unwrap();
170
171            // Update the leaves collection by removing the old node
172            let leaf_ids_to_delete = vec![node.id.clone()];
173            let leaves_to_insert = vec![SparkLeaf::Bitcoin(new_node.clone())];
174            self.leaf_manager
175                .delete_and_insert_leaves_atomically(&leaf_ids_to_delete, &leaves_to_insert)?;
176        }
177
178        Ok(())
179    }
180}