Skip to main content

miden_client/rpc/domain/
limits.rs

1// RPC LIMITS
2// ================================================================================================
3
4use alloc::format;
5use core::convert::TryFrom;
6
7use miden_tx::utils::serde::{
8    ByteReader,
9    ByteWriter,
10    Deserializable,
11    DeserializationError,
12    Serializable,
13};
14
15use crate::rpc::RpcEndpoint;
16use crate::rpc::errors::RpcConversionError;
17use crate::rpc::generated::rpc as proto;
18
19/// Key used to store RPC limits in the settings table.
20pub(crate) const RPC_LIMITS_STORE_SETTING: &str = "rpc_limits";
21
22const DEFAULT_NOTE_IDS_LIMIT: u32 = 100;
23const DEFAULT_NULLIFIERS_LIMIT: u32 = 1000;
24const DEFAULT_ACCOUNT_IDS_LIMIT: u32 = 1000;
25const DEFAULT_NOTE_TAGS_LIMIT: u32 = 1000;
26const DEFAULT_VAULT_ASSETS_LIMIT: u32 = 64;
27
28/// Domain type representing RPC endpoint limits.
29///
30/// These limits define the maximum number of items that can be sent in a single RPC request.
31/// Exceeding these limits will result in the request being rejected by the node.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct RpcLimits {
34    /// Maximum number of note IDs that can be sent in a single `GetNotesById` request.
35    pub note_ids_limit: u32,
36    /// Maximum number of nullifier prefixes that can be sent in a single `SyncNullifiers` request.
37    pub nullifiers_limit: u32,
38    /// Maximum number of account IDs that can be sent in a single `SyncTransactions` request.
39    pub account_ids_limit: u32,
40    /// Maximum number of note tags that can be sent in a single `SyncNotes` request.
41    pub note_tags_limit: u32,
42    /// Maximum number of asset IDs that can be sent in a single `GetAccount` vault request.
43    pub vault_assets_limit: u32,
44}
45
46impl Default for RpcLimits {
47    fn default() -> Self {
48        Self {
49            note_ids_limit: DEFAULT_NOTE_IDS_LIMIT,
50            nullifiers_limit: DEFAULT_NULLIFIERS_LIMIT,
51            account_ids_limit: DEFAULT_ACCOUNT_IDS_LIMIT,
52            note_tags_limit: DEFAULT_NOTE_TAGS_LIMIT,
53            vault_assets_limit: DEFAULT_VAULT_ASSETS_LIMIT,
54        }
55    }
56}
57
58impl Serializable for RpcLimits {
59    fn write_into<W: ByteWriter>(&self, target: &mut W) {
60        self.note_ids_limit.write_into(target);
61        self.nullifiers_limit.write_into(target);
62        self.account_ids_limit.write_into(target);
63        self.note_tags_limit.write_into(target);
64        self.vault_assets_limit.write_into(target);
65    }
66}
67
68impl Deserializable for RpcLimits {
69    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
70        Ok(Self {
71            note_ids_limit: u32::read_from(source)?,
72            nullifiers_limit: u32::read_from(source)?,
73            account_ids_limit: u32::read_from(source)?,
74            note_tags_limit: u32::read_from(source)?,
75            vault_assets_limit: u32::read_from(source)?,
76        })
77    }
78}
79
80/// Extracts a parameter limit from the proto response for a given endpoint and parameter name.
81fn get_param(
82    proto: &proto::GetLimitsResponse,
83    endpoint: RpcEndpoint,
84    param: &'static str,
85) -> Result<u32, RpcConversionError> {
86    let ep = proto.endpoints.get(endpoint.proto_name()).ok_or(
87        RpcConversionError::MissingFieldInProtobufRepresentation {
88            entity: "RpcLimits",
89            field_name: param,
90        },
91    )?;
92    let limit = ep.parameters.get(param).ok_or(
93        RpcConversionError::MissingFieldInProtobufRepresentation {
94            entity: "RpcLimits",
95            field_name: param,
96        },
97    )?;
98    if *limit == 0 {
99        return Err(RpcConversionError::InvalidField(format!(
100            "{}.{} must be greater than zero",
101            endpoint.proto_name(),
102            param
103        )));
104    }
105
106    Ok(*limit)
107}
108
109impl TryFrom<proto::GetLimitsResponse> for RpcLimits {
110    type Error = RpcConversionError;
111
112    fn try_from(proto: proto::GetLimitsResponse) -> Result<Self, Self::Error> {
113        Ok(Self {
114            note_ids_limit: get_param(&proto, RpcEndpoint::GetNotesById, "note_id")?,
115            nullifiers_limit: get_param(&proto, RpcEndpoint::SyncNullifiers, "nullifier_prefix")?,
116            account_ids_limit: get_param(&proto, RpcEndpoint::SyncTransactions, "account_id")?,
117            note_tags_limit: get_param(&proto, RpcEndpoint::SyncNotes, "note_tag")?,
118            vault_assets_limit: get_param(&proto, RpcEndpoint::GetAccount, "vault_asset")?,
119        })
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use alloc::string::String;
126
127    use super::*;
128
129    #[test]
130    fn rpc_limits_serialization_roundtrip() {
131        let original = RpcLimits {
132            note_ids_limit: 100,
133            nullifiers_limit: 1000,
134            account_ids_limit: 1000,
135            note_tags_limit: 1000,
136            vault_assets_limit: 64,
137        };
138
139        let bytes = original.to_bytes();
140        let deserialized = RpcLimits::read_from_bytes(&bytes).expect("deserialization failed");
141
142        assert_eq!(original, deserialized);
143    }
144
145    #[test]
146    fn rejects_zero_limits_from_rpc_response() {
147        let mut proto = proto::GetLimitsResponse::default();
148
149        proto.endpoints.insert(
150            RpcEndpoint::GetNotesById.proto_name().into(),
151            proto::EndpointLimits {
152                parameters: [(String::from("note_id"), 0)].into(),
153            },
154        );
155        proto.endpoints.insert(
156            RpcEndpoint::SyncNullifiers.proto_name().into(),
157            proto::EndpointLimits {
158                parameters: [(String::from("nullifier_prefix"), 1000)].into(),
159            },
160        );
161        proto.endpoints.insert(
162            RpcEndpoint::SyncTransactions.proto_name().into(),
163            proto::EndpointLimits {
164                parameters: [(String::from("account_id"), 1000)].into(),
165            },
166        );
167        proto.endpoints.insert(
168            RpcEndpoint::SyncNotes.proto_name().into(),
169            proto::EndpointLimits {
170                parameters: [(String::from("note_tag"), 1000)].into(),
171            },
172        );
173        proto.endpoints.insert(
174            RpcEndpoint::GetAccount.proto_name().into(),
175            proto::EndpointLimits {
176                parameters: [(String::from("vault_asset"), 64)].into(),
177            },
178        );
179
180        let err = RpcLimits::try_from(proto).expect_err("zero limit should be rejected");
181
182        assert!(matches!(err, RpcConversionError::InvalidField(_)), "got {err:?}");
183    }
184}