Skip to main content

miden_client/rpc/domain/
account_vault.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use miden_objects::DecodeMessageExt;
5use miden_protocol::Word;
6use miden_protocol::account::AccountVaultPatch;
7use miden_protocol::asset::{Asset, AssetId};
8use miden_protocol::block::BlockNumber;
9
10use crate::rpc::domain::MissingFieldHelper;
11use crate::rpc::{RpcError, generated as proto};
12
13// ACCOUNT VAULT INFO
14// ================================================================================================
15
16/// The merged result of syncing an account's vault over a block range.
17///
18/// The node reports per-block asset updates that may repeat a vault key across blocks; these are
19/// merged into a single absolute [`AccountVaultPatch`] (latest block wins per key). Also provides
20/// the current chain tip observed while processing the request.
21pub struct AccountVaultInfo {
22    /// Current chain tip.
23    pub chain_tip: BlockNumber,
24    /// The block number of the last check included in this response.
25    pub block_number: BlockNumber,
26    /// The absolute vault patch merged from the per-block updates.
27    pub vault_patch: AccountVaultPatch,
28}
29
30// ACCOUNT VAULT CONVERSION
31// ================================================================================================
32
33impl TryFrom<proto::rpc::SyncAccountVaultResponse> for AccountVaultInfo {
34    type Error = RpcError;
35
36    fn try_from(value: proto::rpc::SyncAccountVaultResponse) -> Result<Self, Self::Error> {
37        let pagination_info =
38            value
39                .pagination_info
40                .ok_or(proto::rpc::SyncAccountVaultResponse::missing_field(stringify!(
41                    pagination_info
42                )))?;
43
44        let mut updates = value
45            .updates
46            .into_iter()
47            .map(vault_update_from_proto)
48            .collect::<Result<Vec<_>, _>>()?;
49
50        // The node may report the same asset ID in more than one block, folding the updates in
51        // ascending block order lets the latest block win, with an absent asset (`None`) encoding a
52        // removal.
53        updates.sort_by_key(|(block_num, ..)| *block_num);
54        let mut vault_patch = AccountVaultPatch::default();
55        for (_, asset_id, asset) in updates {
56            match asset {
57                Some(asset) => vault_patch.insert_asset(asset),
58                None => vault_patch.remove_asset(asset_id),
59            }
60        }
61
62        Ok(Self {
63            chain_tip: pagination_info.chain_tip.into(),
64            block_number: pagination_info.block_num.into(),
65            vault_patch,
66        })
67    }
68}
69
70// ACCOUNT VAULT UPDATE
71// ================================================================================================
72
73/// Converts a single proto vault update into its block number, asset ID, and new asset (`None` if
74/// the asset was removed at that block), validating that a present asset matches the reported key.
75fn vault_update_from_proto(
76    value: proto::rpc::AccountVaultUpdate,
77) -> Result<(BlockNumber, AssetId, Option<Asset>), RpcError> {
78    let block_num = BlockNumber::from(value.block_num);
79
80    let asset_id: Word = value
81        .vault_key
82        .ok_or(proto::rpc::SyncAccountVaultResponse::missing_field(stringify!(asset_id)))?
83        .try_into()?;
84    let asset_id =
85        AssetId::try_from(asset_id).map_err(|e| RpcError::InvalidResponse(e.to_string()))?;
86
87    let asset: Option<Asset> = value.asset.map(DecodeMessageExt::decode_and_verify).transpose()?;
88
89    if let Some(ref asset) = asset
90        && asset.id() != asset_id
91    {
92        return Err(RpcError::InvalidResponse(
93            "account vault update returned mismatched asset key".to_string(),
94        ));
95    }
96
97    Ok((block_num, asset_id, asset))
98}