1use std::collections::BTreeMap;
4use std::rc::Rc;
5use std::string::{String, ToString};
6use std::sync::{Arc, RwLock};
7use std::vec::Vec;
8
9use miden_client::account::{
10 Account,
11 AccountCode,
12 AccountDelta,
13 AccountHeader,
14 AccountId,
15 AccountIdPrefix,
16 AccountStorage,
17 Address,
18 PartialAccount,
19 PartialStorage,
20 PartialStorageMap,
21 StorageMap,
22 StorageSlotName,
23 StorageSlotType,
24};
25use miden_client::asset::{Asset, AssetVault, AssetWitness, FungibleAsset};
26use miden_client::store::{
27 AccountRecord,
28 AccountRecordData,
29 AccountStatus,
30 AccountStorageFilter,
31 StoreError,
32};
33use miden_client::sync::NoteTagRecord;
34use miden_client::utils::Serializable;
35use miden_client::{AccountError, Word};
36use miden_protocol::account::{AccountStorageHeader, StorageMapWitness, StorageSlotHeader};
37use miden_protocol::asset::{AssetVaultKey, PartialVault};
38use miden_protocol::crypto::merkle::MerkleError;
39use rusqlite::types::Value;
40use rusqlite::{Connection, Transaction, named_params, params};
41
42use crate::account::helpers::{
43 SerializedHeaderData,
44 parse_accounts,
45 query_account_addresses,
46 query_account_code,
47 query_account_headers,
48 query_storage_slots,
49 query_storage_values,
50 query_vault_assets,
51};
52use crate::smt_forest::AccountSmtForest;
53use crate::sql_error::SqlResultExt;
54use crate::sync::{add_note_tag_tx, remove_note_tag_tx};
55use crate::{SqliteStore, column_value_as_u64, insert_sql, subst, u64_to_value};
56
57impl SqliteStore {
58 pub(crate) fn get_account_ids(conn: &mut Connection) -> Result<Vec<AccountId>, StoreError> {
62 const QUERY: &str = "SELECT id FROM tracked_accounts";
63
64 conn.prepare_cached(QUERY)
65 .into_store_error()?
66 .query_map([], |row| row.get(0))
67 .expect("no binding parameters used in query")
68 .map(|result| {
69 let id: String = result.map_err(|e| StoreError::ParsingError(e.to_string()))?;
70 Ok(AccountId::from_hex(&id).expect("account id is valid"))
71 })
72 .collect::<Result<Vec<AccountId>, StoreError>>()
73 }
74
75 pub(crate) fn get_account_headers(
76 conn: &mut Connection,
77 ) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError> {
78 const QUERY: &str = "
79 SELECT
80 a.id,
81 a.nonce,
82 a.vault_root,
83 a.storage_commitment,
84 a.code_commitment,
85 a.account_seed,
86 a.locked
87 FROM accounts AS a
88 JOIN (
89 SELECT id, MAX(nonce) AS nonce
90 FROM accounts
91 GROUP BY id
92 ) AS latest
93 ON a.id = latest.id
94 AND a.nonce = latest.nonce
95 ORDER BY a.id;
96 ";
97
98 conn.prepare_cached(QUERY)
99 .into_store_error()?
100 .query_map(params![], |row| {
101 let id: String = row.get(0)?;
102 let nonce: u64 = column_value_as_u64(row, 1)?;
103 let vault_root: String = row.get(2)?;
104 let storage_commitment: String = row.get(3)?;
105 let code_commitment: String = row.get(4)?;
106 let account_seed: Option<Vec<u8>> = row.get(5)?;
107 let locked: bool = row.get(6)?;
108
109 Ok(SerializedHeaderData {
110 id,
111 nonce,
112 vault_root,
113 storage_commitment,
114 code_commitment,
115 account_seed,
116 locked,
117 })
118 })
119 .into_store_error()?
120 .map(|result| parse_accounts(result.into_store_error()?))
121 .collect::<Result<Vec<(AccountHeader, AccountStatus)>, StoreError>>()
122 }
123
124 pub(crate) fn get_account_header(
125 conn: &mut Connection,
126 account_id: AccountId,
127 ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError> {
128 Ok(query_account_headers(
129 conn,
130 "id = ? ORDER BY nonce DESC LIMIT 1",
131 params![account_id.to_hex()],
132 )?
133 .pop())
134 }
135
136 pub(crate) fn get_account_header_by_commitment(
137 conn: &mut Connection,
138 account_commitment: Word,
139 ) -> Result<Option<AccountHeader>, StoreError> {
140 let account_commitment_str: String = account_commitment.to_string();
141 Ok(
142 query_account_headers(conn, "account_commitment = ?", params![account_commitment_str])?
143 .pop()
144 .map(|(header, _)| header),
145 )
146 }
147
148 pub(crate) fn get_account(
150 conn: &mut Connection,
151 account_id: AccountId,
152 ) -> Result<Option<AccountRecord>, StoreError> {
153 let Some((header, status)) = Self::get_account_header(conn, account_id)? else {
154 return Ok(None);
155 };
156
157 let assets = query_vault_assets(conn, "root = ?", params![header.vault_root().to_hex()])?;
158 let vault = AssetVault::new(&assets)?;
159
160 let slots = query_storage_slots(
161 conn,
162 "commitment = ?",
163 params![header.storage_commitment().to_hex()],
164 )?;
165
166 let storage = AccountStorage::new(slots)?;
167
168 let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
169 return Ok(None);
170 };
171
172 let account = Account::new_unchecked(
173 header.id(),
174 vault,
175 storage,
176 account_code,
177 header.nonce(),
178 status.seed().copied(),
179 );
180
181 let addresses = query_account_addresses(conn, header.id())?;
182 let account_data = AccountRecordData::Full(account);
183 Ok(Some(AccountRecord::new(account_data, status, addresses)))
184 }
185
186 pub(crate) fn get_minimal_partial_account(
188 conn: &mut Connection,
189 account_id: AccountId,
190 ) -> Result<Option<AccountRecord>, StoreError> {
191 let Some((header, status)) = Self::get_account_header(conn, account_id)? else {
192 return Ok(None);
193 };
194
195 let partial_vault = PartialVault::new(header.vault_root());
197
198 let mut storage_header = Vec::new();
200 let mut maps = vec![];
201
202 let storage_values = query_storage_values(
203 conn,
204 "commitment = ?",
205 params![header.storage_commitment().to_hex()],
206 )?;
207
208 for (slot_name, (slot_type, value)) in storage_values {
212 storage_header.push(StorageSlotHeader::new(slot_name.clone(), slot_type, value));
213 if slot_type == StorageSlotType::Map {
214 maps.push(PartialStorageMap::new(value));
215 }
216 }
217 storage_header.sort_by_key(StorageSlotHeader::id);
218 let storage_header =
219 AccountStorageHeader::new(storage_header).map_err(StoreError::AccountError)?;
220 let partial_storage =
221 PartialStorage::new(storage_header, maps).map_err(StoreError::AccountError)?;
222
223 let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
224 return Ok(None);
225 };
226
227 let partial_account = PartialAccount::new(
228 header.id(),
229 header.nonce(),
230 account_code,
231 partial_storage,
232 partial_vault,
233 status.seed().copied(),
234 )?;
235 let account_record_data = AccountRecordData::Partial(partial_account);
236 let addresses = query_account_addresses(conn, header.id())?;
237 Ok(Some(AccountRecord::new(account_record_data, status, addresses)))
238 }
239
240 pub fn get_foreign_account_code(
241 conn: &mut Connection,
242 account_ids: Vec<AccountId>,
243 ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
244 let params: Vec<Value> =
245 account_ids.into_iter().map(|id| Value::from(id.to_hex())).collect();
246 const QUERY: &str = "
247 SELECT account_id, code
248 FROM foreign_account_code JOIN account_code ON foreign_account_code.code_commitment = account_code.commitment
249 WHERE account_id IN rarray(?)";
250
251 conn.prepare_cached(QUERY)
252 .into_store_error()?
253 .query_map([Rc::new(params)], |row| Ok((row.get(0)?, row.get(1)?)))
254 .expect("no binding parameters used in query")
255 .map(|result| {
256 result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
257 |(id, code): (String, Vec<u8>)| {
258 Ok((
259 AccountId::from_hex(&id).map_err(|err| {
260 StoreError::AccountError(
261 AccountError::FinalAccountHeaderIdParsingFailed(err),
262 )
263 })?,
264 AccountCode::from_bytes(&code).map_err(StoreError::AccountError)?,
265 ))
266 },
267 )
268 })
269 .collect::<Result<BTreeMap<AccountId, AccountCode>, _>>()
270 }
271
272 pub fn get_account_vault(
274 conn: &Connection,
275 account_id: AccountId,
276 ) -> Result<AssetVault, StoreError> {
277 let assets = query_vault_assets(
278 conn,
279 "root = (SELECT vault_root FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1)",
280 params![account_id.to_hex()],
281 )?;
282
283 Ok(AssetVault::new(&assets)?)
284 }
285
286 pub fn get_account_storage(
288 conn: &Connection,
289 account_id: AccountId,
290 filter: &AccountStorageFilter,
291 ) -> Result<AccountStorage, StoreError> {
292 let (where_clause, params) = match filter {
293 AccountStorageFilter::All => (
294 "commitment = (SELECT storage_commitment FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1)",
295 params![account_id.to_hex()],
296 ),
297 AccountStorageFilter::Root(root) => (
298 "commitment = (SELECT storage_commitment FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1) AND slot_value = ?",
299 params![account_id.to_hex(), root.to_hex()],
300 ),
301 AccountStorageFilter::SlotName(slot_name) => (
302 "commitment = (SELECT storage_commitment FROM accounts WHERE id = ? ORDER BY nonce DESC LIMIT 1) AND slot_name = ?",
303 params![account_id.to_hex(), slot_name.to_string()],
304 ),
305 };
306
307 let slots = query_storage_slots(conn, where_clause, params)?;
308
309 Ok(AccountStorage::new(slots)?)
310 }
311
312 pub(crate) fn get_account_asset(
315 conn: &mut Connection,
316 smt_forest: &Arc<RwLock<AccountSmtForest>>,
317 account_id: AccountId,
318 vault_key: AssetVaultKey,
319 ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
320 let header = Self::get_account_header(conn, account_id)?
321 .ok_or(StoreError::AccountDataNotFound(account_id))?
322 .0;
323
324 let smt_forest = smt_forest.read().expect("smt_forest read lock not poisoned");
325 match smt_forest.get_asset_and_witness(header.vault_root(), vault_key) {
326 Ok((asset, witness)) => Ok(Some((asset, witness))),
327 Err(StoreError::MerkleStoreError(MerkleError::UntrackedKey(_))) => Ok(None),
328 Err(err) => Err(err),
329 }
330 }
331
332 pub(crate) fn get_account_map_item(
335 conn: &mut Connection,
336 smt_forest: &Arc<RwLock<AccountSmtForest>>,
337 account_id: AccountId,
338 slot_name: StorageSlotName,
339 key: Word,
340 ) -> Result<(Word, StorageMapWitness), StoreError> {
341 let header = Self::get_account_header(conn, account_id)?
342 .ok_or(StoreError::AccountDataNotFound(account_id))?
343 .0;
344
345 let mut storage_values = query_storage_values(
346 conn,
347 "commitment = ? AND slot_name = ?",
348 params![header.storage_commitment().to_hex(), slot_name.to_string()],
349 )?;
350 let (slot_type, map_root) = storage_values
351 .remove(&slot_name)
352 .ok_or(StoreError::AccountStorageRootNotFound(header.storage_commitment()))?;
353 if slot_type != StorageSlotType::Map {
354 return Err(StoreError::AccountError(AccountError::StorageSlotNotMap(slot_name)));
355 }
356
357 let smt_forest = smt_forest.read().expect("smt_forest read lock not poisoned");
358 let witness = smt_forest.get_storage_map_item_witness(map_root, key)?;
359 let item = witness.get(&key).unwrap_or(miden_client::EMPTY_WORD);
360
361 Ok((item, witness))
362 }
363
364 pub(crate) fn get_account_addresses(
365 conn: &mut Connection,
366 account_id: AccountId,
367 ) -> Result<Vec<Address>, StoreError> {
368 query_account_addresses(conn, account_id)
369 }
370
371 pub(crate) fn insert_account(
375 conn: &mut Connection,
376 smt_forest: &Arc<RwLock<AccountSmtForest>>,
377 account: &Account,
378 initial_address: &Address,
379 ) -> Result<(), StoreError> {
380 let tx = conn.transaction().into_store_error()?;
381
382 Self::insert_account_code(&tx, account.code())?;
383
384 Self::insert_storage_slots(
385 &tx,
386 account.storage().to_commitment(),
387 account.storage().slots().iter(),
388 )?;
389
390 Self::insert_assets(&tx, account.vault().root(), account.vault().assets())?;
391 Self::insert_account_header(&tx, &account.into(), account.seed())?;
392
393 Self::insert_address(&tx, initial_address, account.id())?;
394
395 tx.commit().into_store_error()?;
396
397 let mut smt_forest = smt_forest.write().expect("smt_forest write lock not poisoned");
398 smt_forest.insert_and_register_account_state(
399 account.id(),
400 account.vault(),
401 account.storage(),
402 )?;
403
404 Ok(())
405 }
406
407 pub(crate) fn update_account(
408 conn: &mut Connection,
409 smt_forest: &Arc<RwLock<AccountSmtForest>>,
410 new_account_state: &Account,
411 ) -> Result<(), StoreError> {
412 const QUERY: &str = "SELECT id FROM accounts WHERE id = ?";
413 if conn
414 .prepare(QUERY)
415 .into_store_error()?
416 .query_map(params![new_account_state.id().to_hex()], |row| row.get(0))
417 .into_store_error()?
418 .map(|result| {
419 result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
420 |id: String| {
421 AccountId::from_hex(&id).map_err(|err| {
422 StoreError::AccountError(
423 AccountError::FinalAccountHeaderIdParsingFailed(err),
424 )
425 })
426 },
427 )
428 })
429 .next()
430 .is_none()
431 {
432 return Err(StoreError::AccountDataNotFound(new_account_state.id()));
433 }
434
435 let mut smt_forest = smt_forest.write().expect("smt_forest write lock not poisoned");
436 let tx = conn.transaction().into_store_error()?;
437 Self::update_account_state(&tx, &mut smt_forest, new_account_state)?;
438 tx.commit().into_store_error()
439 }
440
441 pub fn upsert_foreign_account_code(
442 conn: &mut Connection,
443 account_id: AccountId,
444 code: &AccountCode,
445 ) -> Result<(), StoreError> {
446 let tx = conn.transaction().into_store_error()?;
447
448 Self::insert_account_code(&tx, code)?;
449
450 const QUERY: &str =
451 insert_sql!(foreign_account_code { account_id, code_commitment } | REPLACE);
452
453 tx.execute(QUERY, params![account_id.to_hex(), code.commitment().to_string()])
454 .into_store_error()?;
455
456 Self::insert_account_code(&tx, code)?;
457 tx.commit().into_store_error()
458 }
459
460 pub(crate) fn insert_address(
461 tx: &Transaction<'_>,
462 address: &Address,
463 account_id: AccountId,
464 ) -> Result<(), StoreError> {
465 let derived_note_tag = address.to_note_tag();
466 let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
467
468 add_note_tag_tx(tx, ¬e_tag_record)?;
469 Self::insert_address_internal(tx, address, account_id)?;
470
471 Ok(())
472 }
473
474 pub(crate) fn remove_address(
475 conn: &mut Connection,
476 address: &Address,
477 account_id: AccountId,
478 ) -> Result<(), StoreError> {
479 let derived_note_tag = address.to_note_tag();
480 let note_tag_record = NoteTagRecord::with_account_source(derived_note_tag, account_id);
481
482 let tx = conn.transaction().into_store_error()?;
483 remove_note_tag_tx(&tx, note_tag_record)?;
484 Self::remove_address_internal(&tx, address)?;
485
486 tx.commit().into_store_error()
487 }
488
489 pub(crate) fn insert_account_code(
491 tx: &Transaction<'_>,
492 account_code: &AccountCode,
493 ) -> Result<(), StoreError> {
494 const QUERY: &str = insert_sql!(account_code { commitment, code } | IGNORE);
495 tx.execute(QUERY, params![account_code.commitment().to_hex(), account_code.to_bytes()])
496 .into_store_error()?;
497 Ok(())
498 }
499
500 pub(crate) fn apply_account_delta(
508 tx: &Transaction<'_>,
509 smt_forest: &mut AccountSmtForest,
510 init_account_state: &AccountHeader,
511 final_account_state: &AccountHeader,
512 updated_fungible_assets: BTreeMap<AccountIdPrefix, FungibleAsset>,
513 updated_storage_maps: BTreeMap<StorageSlotName, StorageMap>,
514 delta: &AccountDelta,
515 ) -> Result<(), StoreError> {
516 Self::copy_account_state(tx, init_account_state, final_account_state)?;
519
520 Self::apply_account_vault_delta(
521 tx,
522 smt_forest,
523 init_account_state,
524 final_account_state,
525 updated_fungible_assets,
526 delta,
527 )?;
528
529 let mut final_roots = smt_forest
534 .get_roots(&init_account_state.id())
535 .cloned()
536 .ok_or(StoreError::AccountDataNotFound(init_account_state.id()))?;
537
538 if let Some(vault_root) = final_roots.first_mut() {
540 *vault_root = final_account_state.vault_root();
541 }
542
543 let updated_storage_slots = Self::apply_account_storage_delta(
544 smt_forest,
545 &mut final_roots,
546 updated_storage_maps,
547 delta,
548 )?;
549
550 Self::insert_storage_slots(
551 tx,
552 final_account_state.storage_commitment(),
553 updated_storage_slots.iter(),
554 )?;
555
556 smt_forest.stage_roots(final_account_state.id(), final_roots);
557
558 Ok(())
559 }
560
561 pub(crate) fn undo_account_state(
564 tx: &Transaction<'_>,
565 smt_forest: &mut AccountSmtForest,
566 discarded_states: &[(AccountId, Word)],
567 ) -> Result<(), StoreError> {
568 if discarded_states.is_empty() {
569 return Ok(());
570 }
571
572 let commitment_params = Rc::new(
573 discarded_states
574 .iter()
575 .map(|(_, commitment)| Value::from(commitment.to_hex()))
576 .collect::<Vec<_>>(),
577 );
578
579 const DELETE_QUERY: &str = "DELETE FROM accounts WHERE account_commitment IN rarray(?)";
580 tx.execute(DELETE_QUERY, params![commitment_params]).into_store_error()?;
581
582 for (account_id, _) in discarded_states {
585 smt_forest.discard_roots(*account_id);
586 }
587
588 Ok(())
589 }
590
591 pub(crate) fn update_account_state(
595 tx: &Transaction<'_>,
596 smt_forest: &mut AccountSmtForest,
597 new_account_state: &Account,
598 ) -> Result<(), StoreError> {
599 smt_forest.insert_and_register_account_state(
600 new_account_state.id(),
601 new_account_state.vault(),
602 new_account_state.storage(),
603 )?;
604 Self::insert_storage_slots(
605 tx,
606 new_account_state.storage().to_commitment(),
607 new_account_state.storage().slots().iter(),
608 )?;
609 Self::insert_assets(
610 tx,
611 new_account_state.vault().root(),
612 new_account_state.vault().assets(),
613 )?;
614 Self::insert_account_header(tx, &new_account_state.into(), None)?;
615
616 Ok(())
617 }
618
619 pub(crate) fn lock_account_on_unexpected_commitment(
622 tx: &Transaction<'_>,
623 account_id: &AccountId,
624 mismatched_digest: &Word,
625 ) -> Result<(), StoreError> {
626 const QUERY: &str = "UPDATE accounts SET locked = true WHERE id = :account_id AND NOT EXISTS (SELECT 1 FROM accounts WHERE id = :account_id AND account_commitment = :digest)";
630 tx.execute(
631 QUERY,
632 named_params! {
633 ":account_id": account_id.to_hex(),
634 ":digest": mismatched_digest.to_string()
635 },
636 )
637 .into_store_error()?;
638 Ok(())
639 }
640
641 fn copy_account_state(
646 tx: &Transaction<'_>,
647 init_account_header: &AccountHeader,
648 final_account_header: &AccountHeader,
649 ) -> Result<(), StoreError> {
650 Self::insert_account_header(tx, final_account_header, None)?;
651
652 if init_account_header.vault_root() != final_account_header.vault_root() {
653 const VAULT_QUERY: &str = "
654 INSERT OR IGNORE INTO account_assets (
655 root,
656 vault_key,
657 faucet_id_prefix,
658 asset
659 )
660 SELECT
661 ?, --new root
662 vault_key,
663 faucet_id_prefix,
664 asset
665 FROM account_assets
666 WHERE root = (SELECT vault_root FROM accounts WHERE account_commitment = ?)
667 ";
668 tx.execute(
669 VAULT_QUERY,
670 params![
671 final_account_header.vault_root().to_hex(),
672 init_account_header.commitment().to_hex()
673 ],
674 )
675 .into_store_error()?;
676 }
677
678 if init_account_header.storage_commitment() != final_account_header.storage_commitment() {
679 const STORAGE_QUERY: &str = "
680 INSERT OR IGNORE INTO account_storage (
681 commitment,
682 slot_name,
683 slot_value,
684 slot_type
685 )
686 SELECT
687 ?, -- new commitment
688 slot_name,
689 slot_value,
690 slot_type
691 FROM account_storage
692 WHERE commitment = (SELECT storage_commitment FROM accounts WHERE account_commitment = ?)
693 ";
694
695 tx.execute(
696 STORAGE_QUERY,
697 params![
698 final_account_header.storage_commitment().to_hex(),
699 init_account_header.commitment().to_hex()
700 ],
701 )
702 .into_store_error()?;
703 }
704
705 Ok(())
706 }
707
708 fn insert_account_header(
710 tx: &Transaction<'_>,
711 account: &AccountHeader,
712 account_seed: Option<Word>,
713 ) -> Result<(), StoreError> {
714 let id: String = account.id().to_hex();
715 let code_commitment = account.code_commitment().to_string();
716 let storage_commitment = account.storage_commitment().to_string();
717 let vault_root = account.vault_root().to_string();
718 let nonce = u64_to_value(account.nonce().as_int());
719 let commitment = account.commitment().to_string();
720
721 let account_seed = account_seed.map(|seed| seed.to_bytes());
722
723 const QUERY: &str = insert_sql!(
724 accounts {
725 id,
726 code_commitment,
727 storage_commitment,
728 vault_root,
729 nonce,
730 account_seed,
731 account_commitment,
732 locked
733 } | REPLACE
734 );
735
736 tx.execute(
737 QUERY,
738 params![
739 id,
740 code_commitment,
741 storage_commitment,
742 vault_root,
743 nonce,
744 account_seed,
745 commitment,
746 false,
747 ],
748 )
749 .into_store_error()?;
750
751 Self::insert_tracked_account_id_tx(tx, account.id())?;
752 Ok(())
753 }
754
755 fn insert_tracked_account_id_tx(
756 tx: &Transaction<'_>,
757 account_id: AccountId,
758 ) -> Result<(), StoreError> {
759 const QUERY: &str = insert_sql!(tracked_accounts { id } | IGNORE);
760 tx.execute(QUERY, params![account_id.to_hex()]).into_store_error()?;
761 Ok(())
762 }
763
764 fn insert_address_internal(
765 tx: &Transaction<'_>,
766 address: &Address,
767 account_id: AccountId,
768 ) -> Result<(), StoreError> {
769 const QUERY: &str = insert_sql!(addresses { address, account_id } | REPLACE);
770 let serialized_address = address.to_bytes();
771 tx.execute(QUERY, params![serialized_address, account_id.to_hex(),])
772 .into_store_error()?;
773
774 Ok(())
775 }
776
777 fn remove_address_internal(tx: &Transaction<'_>, address: &Address) -> Result<(), StoreError> {
778 let serialized_address = address.to_bytes();
779
780 const DELETE_QUERY: &str = "DELETE FROM addresses WHERE address = ?";
781 tx.execute(DELETE_QUERY, params![serialized_address]).into_store_error()?;
782
783 Ok(())
784 }
785}