1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! Vault/asset-related database operations for accounts.
use std::rc::Rc;
use std::vec::Vec;
use miden_client::Serializable;
use miden_client::account::{AccountHeader, AccountId, AccountVaultPatch};
use miden_client::asset::Asset;
use miden_client::store::StoreError;
use miden_protocol::asset::AssetId;
use rusqlite::types::Value;
use rusqlite::{OptionalExtension, Transaction, params};
use crate::sql_error::SqlResultExt;
use crate::{SqliteStore, insert_sql, subst, u64_to_value};
impl SqliteStore {
// READER METHODS
// --------------------------------------------------------------------------------------------
// MUTATOR/WRITER METHODS
// --------------------------------------------------------------------------------------------
/// Inserts assets into the latest tables only.
///
/// Historical archival is handled separately by the caller when needed.
pub(crate) fn insert_assets(
tx: &Transaction<'_>,
account_id: AccountId,
assets: impl Iterator<Item = Asset>,
) -> Result<(), StoreError> {
const LATEST_QUERY: &str =
insert_sql!(latest_account_assets { account_id, asset_id, asset } | REPLACE);
let mut latest_stmt = tx.prepare_cached(LATEST_QUERY).into_store_error()?;
let account_id_bytes = account_id.to_bytes();
for asset in assets {
let asset_id_bytes = asset.id().to_bytes();
let asset_bytes = asset.to_value_word().to_bytes();
latest_stmt
.execute(params![&account_id_bytes, &asset_id_bytes, &asset_bytes])
.into_store_error()?;
}
Ok(())
}
/// Persists vault patch changes to the asset tables, updating fungible and non-fungible
/// assets. It archives old values from latest to historical, deletes removed assets from
/// latest, then inserts updated assets.
///
/// The corresponding forest update (and the verification that the resulting vault root
/// matches the final header) happens in `apply_account_patch`, which applies all of an
/// account's tree changes in one batch.
pub(crate) fn apply_account_vault_patch(
tx: &Transaction<'_>,
account_id: AccountId,
final_account_state: &AccountHeader,
vault_patch: &AccountVaultPatch,
) -> Result<(), StoreError> {
let nonce = final_account_state.nonce().as_canonical_u64();
let account_id_bytes = account_id.to_bytes();
let nonce_val = u64_to_value(nonce);
// The patch carries the absolute final value of every changed entry, so updated assets are
// inserted verbatim and removed entries (empty value) are deleted. No prior balance lookup
// or signed-amount arithmetic is needed, and the asset value word already encodes the
// callback flag for both fungible and non-fungible assets.
let updated_assets_values: Vec<Asset> = vault_patch.updated_assets().collect();
let removed_asset_ids: Vec<AssetId> = vault_patch.removed_asset_ids().copied().collect();
Self::persist_vault_delta(
tx,
&account_id_bytes,
&nonce_val,
&removed_asset_ids,
&updated_assets_values,
)?;
Ok(())
}
/// Persists vault delta changes: archives old values from latest to historical,
/// then updates latest (deletes removed assets, inserts/updates changed assets).
fn persist_vault_delta(
tx: &Transaction<'_>,
account_id_bytes: &[u8],
nonce_val: &rusqlite::types::Value,
removed_asset_ids: &[AssetId],
updated_assets: &[Asset],
) -> Result<(), StoreError> {
const READ_OLD_ASSET: &str =
"SELECT asset FROM latest_account_assets WHERE account_id = ? AND asset_id = ?";
const HISTORICAL_INSERT: &str = insert_sql!(
historical_account_assets {
account_id,
replaced_at_nonce,
asset_id,
old_asset
} | REPLACE
);
const LATEST_INSERT: &str =
insert_sql!(latest_account_assets { account_id, asset_id, asset } | REPLACE);
let mut hist_stmt = tx.prepare_cached(HISTORICAL_INSERT).into_store_error()?;
let mut latest_stmt = tx.prepare_cached(LATEST_INSERT).into_store_error()?;
// Archive and delete removed assets
for asset_id in removed_asset_ids {
let asset_id_bytes = asset_id.to_bytes();
// Read old asset value from latest (should exist since we're removing it)
let old_asset: Option<Vec<u8>> = tx
.query_row(READ_OLD_ASSET, params![account_id_bytes, &asset_id_bytes], |row| {
row.get(0)
})
.optional()
.into_store_error()?
.flatten();
// Archive old value to historical
hist_stmt
.execute(params![account_id_bytes, nonce_val, &asset_id_bytes, old_asset,])
.into_store_error()?;
}
// Batch delete removed assets from latest
if !removed_asset_ids.is_empty() {
const DELETE_LATEST_QUERY: &str =
"DELETE FROM latest_account_assets WHERE account_id = ? AND asset_id IN rarray(?)";
tx.execute(
DELETE_LATEST_QUERY,
params![
account_id_bytes,
Rc::new(
removed_asset_ids
.iter()
.map(|id| Value::Blob(id.to_bytes()))
.collect::<Vec<Value>>(),
),
],
)
.into_store_error()?;
}
// Archive old values and insert updated assets
for asset in updated_assets {
let asset_id_bytes = asset.id().to_bytes();
let asset_bytes = asset.to_value_word().to_bytes();
// Read old asset value from latest (NULL if asset is new)
let old_asset: Option<Vec<u8>> = tx
.query_row(READ_OLD_ASSET, params![account_id_bytes, &asset_id_bytes], |row| {
row.get(0)
})
.optional()
.into_store_error()?
.flatten();
// Archive old value to historical (NULL old_asset = asset was new)
hist_stmt
.execute(params![account_id_bytes, nonce_val, &asset_id_bytes, old_asset,])
.into_store_error()?;
// Insert/update in latest
latest_stmt
.execute(params![account_id_bytes, &asset_id_bytes, &asset_bytes])
.into_store_error()?;
}
Ok(())
}
}