miden_client/rpc/domain/
limits.rs1use 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
19pub(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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct RpcLimits {
34 pub note_ids_limit: u32,
36 pub nullifiers_limit: u32,
38 pub account_ids_limit: u32,
40 pub note_tags_limit: u32,
42 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
80fn 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}