openzeppelin_relayer/models/relayer/
repository.rs

1use serde::{Deserialize, Serialize};
2use strum::Display;
3use utoipa::ToSchema;
4
5use crate::{
6    constants::{
7        DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE, DEFAULT_EVM_MIN_BALANCE,
8        DEFAULT_SOLANA_MIN_BALANCE, DEFAULT_STELLAR_MIN_BALANCE, MAX_SOLANA_TX_DATA_SIZE,
9    },
10    models::RelayerError,
11};
12
13use super::RpcConfig;
14
15#[derive(Debug, Clone, Serialize, PartialEq, Display, Deserialize, Copy, ToSchema)]
16#[serde(rename_all = "lowercase")]
17pub enum NetworkType {
18    Evm,
19    Stellar,
20    Solana,
21}
22
23#[derive(Debug, Serialize, Clone)]
24pub enum RelayerNetworkPolicy {
25    Evm(RelayerEvmPolicy),
26    Solana(RelayerSolanaPolicy),
27    Stellar(RelayerStellarPolicy),
28}
29
30impl RelayerNetworkPolicy {
31    pub fn get_evm_policy(&self) -> RelayerEvmPolicy {
32        match self {
33            Self::Evm(policy) => policy.clone(),
34            _ => RelayerEvmPolicy::default(),
35        }
36    }
37
38    pub fn get_solana_policy(&self) -> RelayerSolanaPolicy {
39        match self {
40            Self::Solana(policy) => policy.clone(),
41            _ => RelayerSolanaPolicy::default(),
42        }
43    }
44
45    pub fn get_stellar_policy(&self) -> RelayerStellarPolicy {
46        match self {
47            Self::Stellar(policy) => policy.clone(),
48            _ => RelayerStellarPolicy::default(),
49        }
50    }
51}
52
53#[derive(Debug, Serialize, Clone)]
54pub struct RelayerEvmPolicy {
55    pub gas_price_cap: Option<u128>,
56    pub whitelist_receivers: Option<Vec<String>>,
57    pub eip1559_pricing: Option<bool>,
58    pub private_transactions: bool,
59    pub min_balance: u128,
60}
61
62impl Default for RelayerEvmPolicy {
63    fn default() -> Self {
64        Self {
65            gas_price_cap: None,
66            whitelist_receivers: None,
67            eip1559_pricing: None,
68            private_transactions: false,
69            min_balance: DEFAULT_EVM_MIN_BALANCE,
70        }
71    }
72}
73
74#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, PartialEq, Default)]
75pub struct SolanaAllowedTokensSwapConfig {
76    #[schema(nullable = false)]
77    pub slippage_percentage: Option<f32>,
78    #[schema(nullable = false)]
79    pub min_amount: Option<u64>,
80    #[schema(nullable = false)]
81    pub max_amount: Option<u64>,
82    #[schema(nullable = false)]
83    pub retain_min_amount: Option<u64>,
84}
85
86#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
87pub struct SolanaAllowedTokensPolicy {
88    pub mint: String,
89    #[schema(nullable = false)]
90    pub decimals: Option<u8>,
91    #[schema(nullable = false)]
92    pub symbol: Option<String>,
93    #[schema(nullable = false)]
94    pub max_allowed_fee: Option<u64>,
95    #[schema(nullable = false)]
96    pub swap_config: Option<SolanaAllowedTokensSwapConfig>,
97}
98
99impl SolanaAllowedTokensPolicy {
100    pub fn new(
101        mint: String,
102        decimals: Option<u8>,
103        symbol: Option<String>,
104        max_allowed_fee: Option<u64>,
105        swap_config: Option<SolanaAllowedTokensSwapConfig>,
106    ) -> Self {
107        Self {
108            mint,
109            decimals,
110            symbol,
111            max_allowed_fee,
112            swap_config,
113        }
114    }
115
116    // Create a new SolanaAllowedTokensPolicy with only the mint field
117    // We are creating partial entry while processing config file and later
118    // we will fill the rest of the fields
119    pub fn new_partial(
120        mint: String,
121        max_allowed_fee: Option<u64>,
122        swap_config: Option<SolanaAllowedTokensSwapConfig>,
123    ) -> Self {
124        Self {
125            mint,
126            decimals: None,
127            symbol: None,
128            max_allowed_fee,
129            swap_config,
130        }
131    }
132}
133
134#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
135#[serde(rename_all = "lowercase")]
136pub enum SolanaFeePaymentStrategy {
137    User,
138    Relayer,
139}
140
141#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
142#[serde(rename_all = "kebab-case")]
143pub enum SolanaSwapStrategy {
144    JupiterSwap,
145    JupiterUltra,
146    Noop,
147}
148
149#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
150#[serde(deny_unknown_fields)]
151pub struct JupiterSwapOptions {
152    pub priority_fee_max_lamports: Option<u64>,
153    pub priority_level: Option<String>,
154    pub dynamic_compute_unit_limit: Option<bool>,
155}
156
157#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
158#[serde(deny_unknown_fields)]
159pub struct RelayerSolanaSwapConfig {
160    pub strategy: Option<SolanaSwapStrategy>,
161    pub cron_schedule: Option<String>,
162    pub min_balance_threshold: Option<u64>,
163    pub jupiter_swap_options: Option<JupiterSwapOptions>,
164}
165
166#[derive(Debug, Serialize, Clone)]
167#[serde(deny_unknown_fields)]
168pub struct RelayerSolanaPolicy {
169    pub fee_payment_strategy: SolanaFeePaymentStrategy,
170    pub fee_margin_percentage: Option<f32>,
171    pub min_balance: u64,
172    pub allowed_tokens: Option<Vec<SolanaAllowedTokensPolicy>>,
173    pub allowed_programs: Option<Vec<String>>,
174    pub allowed_accounts: Option<Vec<String>>,
175    pub disallowed_accounts: Option<Vec<String>>,
176    pub max_signatures: Option<u8>,
177    pub max_tx_data_size: u16,
178    pub max_allowed_fee_lamports: Option<u64>,
179    pub swap_config: Option<RelayerSolanaSwapConfig>,
180}
181
182impl RelayerSolanaPolicy {
183    pub fn get_allowed_tokens(&self) -> Vec<SolanaAllowedTokensPolicy> {
184        self.allowed_tokens.clone().unwrap_or_default()
185    }
186
187    pub fn get_allowed_token_entry(&self, mint: &str) -> Option<SolanaAllowedTokensPolicy> {
188        self.allowed_tokens
189            .clone()
190            .unwrap_or_default()
191            .into_iter()
192            .find(|entry| entry.mint == mint)
193    }
194
195    pub fn get_allowed_token_decimals(&self, mint: &str) -> Option<u8> {
196        self.get_allowed_token_entry(mint)
197            .and_then(|entry| entry.decimals)
198    }
199
200    pub fn get_swap_config(&self) -> Option<RelayerSolanaSwapConfig> {
201        self.swap_config.clone()
202    }
203
204    pub fn get_allowed_token_slippage(&self, mint: &str) -> f32 {
205        self.get_allowed_token_entry(mint)
206            .and_then(|entry| {
207                entry
208                    .swap_config
209                    .and_then(|config| config.slippage_percentage)
210            })
211            .unwrap_or(DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE)
212    }
213
214    pub fn get_allowed_programs(&self) -> Vec<String> {
215        self.allowed_programs.clone().unwrap_or_default()
216    }
217
218    pub fn get_allowed_accounts(&self) -> Vec<String> {
219        self.allowed_accounts.clone().unwrap_or_default()
220    }
221
222    pub fn get_disallowed_accounts(&self) -> Vec<String> {
223        self.disallowed_accounts.clone().unwrap_or_default()
224    }
225
226    pub fn get_max_signatures(&self) -> u8 {
227        self.max_signatures.unwrap_or(1)
228    }
229
230    pub fn get_max_allowed_fee_lamports(&self) -> u64 {
231        self.max_allowed_fee_lamports.unwrap_or(u64::MAX)
232    }
233
234    pub fn get_max_tx_data_size(&self) -> u16 {
235        self.max_tx_data_size
236    }
237
238    pub fn get_fee_margin_percentage(&self) -> f32 {
239        self.fee_margin_percentage.unwrap_or(0.0)
240    }
241
242    pub fn get_fee_payment_strategy(&self) -> SolanaFeePaymentStrategy {
243        self.fee_payment_strategy.clone()
244    }
245}
246
247impl Default for RelayerSolanaPolicy {
248    fn default() -> Self {
249        Self {
250            fee_payment_strategy: SolanaFeePaymentStrategy::User,
251            fee_margin_percentage: None,
252            min_balance: DEFAULT_SOLANA_MIN_BALANCE,
253            allowed_tokens: None,
254            allowed_programs: None,
255            allowed_accounts: None,
256            disallowed_accounts: None,
257            max_signatures: None,
258            max_tx_data_size: MAX_SOLANA_TX_DATA_SIZE,
259            max_allowed_fee_lamports: None,
260            swap_config: None,
261        }
262    }
263}
264
265#[derive(Debug, Serialize, Clone)]
266#[serde(deny_unknown_fields)]
267pub struct RelayerStellarPolicy {
268    pub max_fee: Option<u32>,
269    pub timeout_seconds: Option<u64>,
270    pub min_balance: u64,
271}
272
273impl Default for RelayerStellarPolicy {
274    fn default() -> Self {
275        Self {
276            max_fee: None,
277            timeout_seconds: None,
278            min_balance: DEFAULT_STELLAR_MIN_BALANCE,
279        }
280    }
281}
282
283#[derive(Debug, Clone, Serialize)]
284pub struct RelayerRepoModel {
285    pub id: String,
286    pub name: String,
287    pub network: String,
288    pub paused: bool,
289    pub network_type: NetworkType,
290    pub signer_id: String,
291    pub policies: RelayerNetworkPolicy,
292    pub address: String,
293    pub notification_id: Option<String>,
294    pub system_disabled: bool,
295    pub custom_rpc_urls: Option<Vec<RpcConfig>>,
296}
297
298impl RelayerRepoModel {
299    pub fn validate_active_state(&self) -> Result<(), RelayerError> {
300        if self.paused {
301            return Err(RelayerError::RelayerPaused);
302        }
303
304        if self.system_disabled {
305            return Err(RelayerError::RelayerDisabled);
306        }
307
308        Ok(())
309    }
310}
311
312impl Default for RelayerRepoModel {
313    fn default() -> Self {
314        Self {
315            id: "".to_string(),
316            name: "".to_string(),
317            network: "".to_string(),
318            paused: false,
319            network_type: NetworkType::Evm,
320            signer_id: "".to_string(),
321            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
322            address: "0x".to_string(),
323            notification_id: None,
324            system_disabled: false,
325            custom_rpc_urls: None,
326        }
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    fn create_test_relayer(paused: bool, system_disabled: bool) -> RelayerRepoModel {
335        RelayerRepoModel {
336            id: "test_relayer".to_string(),
337            name: "Test Relayer".to_string(),
338            paused,
339            system_disabled,
340            network: "test_network".to_string(),
341            network_type: NetworkType::Evm,
342            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
343            signer_id: "test_signer".to_string(),
344            address: "0x".to_string(),
345            notification_id: None,
346            custom_rpc_urls: Some(vec![RpcConfig::new(
347                "https://test-rpc.example.com".to_string(),
348            )]),
349        }
350    }
351
352    #[test]
353    fn test_validate_active_state_active() {
354        let relayer = create_test_relayer(false, false);
355        assert!(relayer.validate_active_state().is_ok());
356    }
357
358    #[test]
359    fn test_validate_active_state_paused() {
360        let relayer = create_test_relayer(true, false);
361        let result = relayer.validate_active_state();
362        assert!(matches!(result, Err(RelayerError::RelayerPaused)));
363    }
364
365    #[test]
366    fn test_validate_active_state_disabled() {
367        let relayer = create_test_relayer(false, true);
368        let result = relayer.validate_active_state();
369        assert!(matches!(result, Err(RelayerError::RelayerDisabled)));
370    }
371
372    #[test]
373    fn test_validate_active_state_paused_and_disabled() {
374        let relayer = create_test_relayer(true, true);
375        let result = relayer.validate_active_state();
376        assert!(matches!(result, Err(RelayerError::RelayerPaused)));
377    }
378}