Skip to main content

miden_client/rpc/domain/
account_vault.rs

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