breez_sdk_liquid/persist/
cache.rs

1use anyhow::Result;
2use rusqlite::{OptionalExtension, Transaction, TransactionBehavior};
3use std::str::FromStr;
4
5use crate::model::GetInfoResponse;
6use crate::sync::model::{data::LAST_DERIVATION_INDEX_DATA_ID, RecordType};
7
8use super::{BlockchainInfo, Persister, WalletInfo};
9
10const KEY_WALLET_INFO: &str = "wallet_info";
11const KEY_BLOCKCHAIN_INFO: &str = "blockchain_info";
12const KEY_SWAPPER_PROXY_URL: &str = "swapper_proxy_url";
13const KEY_IS_FIRST_SYNC_COMPLETE: &str = "is_first_sync_complete";
14const KEY_WEBHOOK_URL: &str = "webhook_url";
15pub(crate) const KEY_LAST_DERIVATION_INDEX: &str = "last_derivation_index";
16const KEY_LAST_SCANNED_DERIVATION_INDEX: &str = "last_scanned_derivation_index";
17
18impl Persister {
19    fn get_cached_item_inner(tx: &Transaction, key: &str) -> Result<Option<String>> {
20        let res = tx.query_row(
21            "SELECT value FROM cached_items WHERE key = ?1",
22            [key],
23            |row| row.get(0),
24        );
25        Ok(res.ok())
26    }
27
28    pub(crate) fn update_cached_item_inner(
29        tx: &Transaction,
30        key: &str,
31        value: String,
32    ) -> Result<()> {
33        tx.execute(
34            "INSERT OR REPLACE INTO cached_items (key, value) VALUES (?1,?2)",
35            (key, value),
36        )?;
37        Ok(())
38    }
39
40    pub fn delete_cached_item_inner(tx: &Transaction, key: &str) -> Result<()> {
41        tx.execute("DELETE FROM cached_items WHERE key = ?1", [key])?;
42        Ok(())
43    }
44
45    pub fn get_cached_item(&self, key: &str) -> Result<Option<String>> {
46        let mut con = self.get_connection()?;
47        let tx = con.transaction()?;
48        let res = Self::get_cached_item_inner(&tx, key);
49        tx.commit()?;
50        res
51    }
52
53    pub fn update_cached_item(&self, key: &str, value: String) -> Result<()> {
54        let mut con = self.get_connection()?;
55        let tx = con.transaction()?;
56        let res = Self::update_cached_item_inner(&tx, key, value);
57        tx.commit()?;
58        res
59    }
60
61    pub fn delete_cached_item(&self, key: &str) -> Result<()> {
62        let mut con = self.get_connection()?;
63        let tx = con.transaction()?;
64        let res = Self::delete_cached_item_inner(&tx, key);
65        tx.commit()?;
66        res
67    }
68
69    pub fn set_wallet_info(&self, info: &WalletInfo) -> Result<()> {
70        let serialized_info = serde_json::to_string(info)?;
71        self.update_cached_item(KEY_WALLET_INFO, serialized_info)
72    }
73
74    pub fn update_blockchain_info(&self, liquid_tip: u32, bitcoin_tip: Option<u32>) -> Result<()> {
75        let info = match bitcoin_tip {
76            Some(bitcoin_tip) => BlockchainInfo {
77                liquid_tip,
78                bitcoin_tip,
79            },
80            None => {
81                let current_tip = self
82                    .get_cached_item(KEY_BLOCKCHAIN_INFO)?
83                    .and_then(|info| serde_json::from_str::<BlockchainInfo>(&info).ok())
84                    .map(|info| info.bitcoin_tip)
85                    .unwrap_or(0);
86                BlockchainInfo {
87                    liquid_tip,
88                    bitcoin_tip: current_tip,
89                }
90            }
91        };
92
93        let serialized_info = serde_json::to_string(&info)?;
94        self.update_cached_item(KEY_BLOCKCHAIN_INFO, serialized_info)
95    }
96
97    pub fn get_info(&self) -> Result<Option<GetInfoResponse>> {
98        let con = self.get_connection()?;
99
100        let info: Option<(Option<String>, Option<String>)> = con
101            .query_row(
102                &format!(
103                    "
104            SELECT
105                c1.value AS wallet_info,
106                COALESCE(c2.value, NULL) AS blockchain_info
107            FROM (SELECT value FROM cached_items WHERE key = '{KEY_WALLET_INFO}') c1
108            LEFT JOIN (SELECT value FROM cached_items WHERE key = '{KEY_BLOCKCHAIN_INFO}') c2
109        "
110                ),
111                [],
112                |row| Ok((row.get(0)?, row.get(1)?)),
113            )
114            .optional()?;
115
116        match info {
117            Some((Some(wallet_info), blockchain_info)) => {
118                let wallet_info = serde_json::from_str(&wallet_info)?;
119                let blockchain_info = blockchain_info
120                    .and_then(|info| serde_json::from_str(&info).ok())
121                    .unwrap_or_default();
122                Ok(Some(GetInfoResponse {
123                    wallet_info,
124                    blockchain_info,
125                }))
126            }
127            _ => Ok(None),
128        }
129    }
130
131    pub fn set_swapper_proxy_url(&self, swapper_proxy_url: String) -> Result<()> {
132        self.update_cached_item(KEY_SWAPPER_PROXY_URL, swapper_proxy_url)
133    }
134
135    #[allow(dead_code)]
136    pub fn remove_swapper_proxy_url(&self) -> Result<()> {
137        self.delete_cached_item(KEY_SWAPPER_PROXY_URL)
138    }
139
140    pub fn get_swapper_proxy_url(&self) -> Result<Option<String>> {
141        self.get_cached_item(KEY_SWAPPER_PROXY_URL)
142    }
143
144    pub fn set_is_first_sync_complete(&self, complete: bool) -> Result<()> {
145        self.update_cached_item(KEY_IS_FIRST_SYNC_COMPLETE, complete.to_string())
146    }
147
148    pub fn get_is_first_sync_complete(&self) -> Result<Option<bool>> {
149        self.get_cached_item(KEY_IS_FIRST_SYNC_COMPLETE)
150            .map(|maybe_str| maybe_str.and_then(|val_str| bool::from_str(&val_str).ok()))
151    }
152
153    pub fn set_webhook_url(&self, webhook_url: String) -> Result<()> {
154        self.update_cached_item(KEY_WEBHOOK_URL, webhook_url)
155    }
156
157    pub fn remove_webhook_url(&self) -> Result<()> {
158        self.delete_cached_item(KEY_WEBHOOK_URL)
159    }
160
161    pub fn get_webhook_url(&self) -> Result<Option<String>> {
162        self.get_cached_item(KEY_WEBHOOK_URL)
163    }
164
165    pub fn set_last_derivation_index_inner(&self, tx: &Transaction, index: u32) -> Result<()> {
166        Self::update_cached_item_inner(tx, KEY_LAST_DERIVATION_INDEX, index.to_string())?;
167        self.commit_outgoing(
168            tx,
169            LAST_DERIVATION_INDEX_DATA_ID,
170            RecordType::LastDerivationIndex,
171            // insert a mock updated field so that merging with incoming data works as expected
172            Some(vec![LAST_DERIVATION_INDEX_DATA_ID.to_string()]),
173        )
174    }
175
176    pub fn set_last_derivation_index(&self, index: u32) -> Result<()> {
177        let mut con = self.get_connection()?;
178        let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?;
179        self.set_last_derivation_index_inner(&tx, index)?;
180        tx.commit()?;
181        self.trigger_sync();
182        Ok(())
183    }
184
185    pub fn get_last_derivation_index(&self) -> Result<Option<u32>> {
186        self.get_cached_item(KEY_LAST_DERIVATION_INDEX)
187            .map(|maybe_str| maybe_str.and_then(|str| str.as_str().parse::<u32>().ok()))
188    }
189
190    pub fn next_derivation_index(&self) -> Result<Option<u32>> {
191        let mut con = self.get_connection()?;
192        let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?;
193        let res = match Self::get_cached_item_inner(&tx, KEY_LAST_DERIVATION_INDEX)? {
194            Some(last_index_str) => {
195                let next_index = last_index_str
196                    .as_str()
197                    .parse::<u32>()
198                    .map(|index| index + 1)?;
199                self.set_last_derivation_index_inner(&tx, next_index)?;
200                Some(next_index)
201            }
202            None => None,
203        };
204        tx.commit()?;
205        self.trigger_sync();
206        Ok(res)
207    }
208
209    pub fn set_last_scanned_derivation_index(&self, index: u32) -> Result<()> {
210        let mut con = self.get_connection()?;
211        let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?;
212        Self::update_cached_item_inner(&tx, KEY_LAST_SCANNED_DERIVATION_INDEX, index.to_string())?;
213        tx.commit()?;
214        Ok(())
215    }
216
217    pub fn get_last_scanned_derivation_index(&self) -> Result<Option<u32>> {
218        self.get_cached_item(KEY_LAST_SCANNED_DERIVATION_INDEX)
219            .map(|maybe_str| maybe_str.and_then(|str| str.as_str().parse::<u32>().ok()))
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use anyhow::Result;
226
227    use crate::test_utils::persist::create_persister;
228
229    #[cfg(feature = "browser-tests")]
230    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
231
232    #[sdk_macros::test_all]
233    fn test_cached_items() -> Result<()> {
234        create_persister!(persister);
235
236        persister.update_cached_item("key1", "val1".to_string())?;
237        let item_value = persister.get_cached_item("key1")?;
238        assert_eq!(item_value, Some("val1".to_string()));
239
240        persister.delete_cached_item("key1")?;
241        let item_value = persister.get_cached_item("key1")?;
242        assert_eq!(item_value, None);
243
244        Ok(())
245    }
246
247    #[sdk_macros::test_all]
248    fn test_get_last_derivation_index() -> Result<()> {
249        create_persister!(persister);
250
251        let maybe_last_index = persister.get_last_derivation_index()?;
252        assert!(maybe_last_index.is_none());
253
254        persister.set_last_derivation_index(50)?;
255
256        let maybe_last_index = persister.get_last_derivation_index()?;
257        assert!(maybe_last_index.is_some());
258        assert_eq!(maybe_last_index, Some(50));
259
260        persister.set_last_derivation_index(51)?;
261
262        let maybe_last_index = persister.get_last_derivation_index()?;
263        assert!(maybe_last_index.is_some());
264        assert_eq!(maybe_last_index, Some(51));
265
266        Ok(())
267    }
268
269    #[sdk_macros::test_all]
270    fn test_next_derivation_index() -> Result<()> {
271        create_persister!(persister);
272
273        let maybe_next_index = persister.next_derivation_index()?;
274        assert!(maybe_next_index.is_none());
275
276        persister.set_last_derivation_index(50)?;
277
278        let maybe_next_index = persister.next_derivation_index()?;
279        assert!(maybe_next_index.is_some());
280        assert_eq!(maybe_next_index, Some(51));
281
282        let maybe_last_index = persister.get_last_derivation_index()?;
283        assert!(maybe_last_index.is_some());
284        assert_eq!(maybe_last_index, Some(51));
285
286        persister.set_last_derivation_index(52)?;
287
288        let maybe_next_index = persister.next_derivation_index()?;
289        assert!(maybe_next_index.is_some());
290        assert_eq!(maybe_next_index, Some(53));
291
292        let maybe_next_index = persister.next_derivation_index()?;
293        assert!(maybe_next_index.is_some());
294        assert_eq!(maybe_next_index, Some(54));
295
296        let maybe_last_index = persister.get_last_derivation_index()?;
297        assert!(maybe_last_index.is_some());
298        assert_eq!(maybe_last_index, Some(54));
299
300        Ok(())
301    }
302}