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 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}