1use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
10pub enum AptosNetwork {
11 Mainnet,
13 Testnet,
15 Devnet,
17 Custom {
19 chain_id: u8,
21 name: String,
23 },
24}
25
26impl AptosNetwork {
27 pub fn chain_id(&self) -> u8 {
29 match self {
30 AptosNetwork::Mainnet => 1,
31 AptosNetwork::Testnet => 2,
32 AptosNetwork::Devnet => 4,
33 AptosNetwork::Custom { chain_id, .. } => *chain_id,
34 }
35 }
36
37 pub fn default_rpc_url(&self) -> &'static str {
39 match self {
40 AptosNetwork::Mainnet => "https://fullnode.mainnet.aptoslabs.com/v1",
41 AptosNetwork::Testnet => "https://fullnode.testnet.aptoslabs.com/v1",
42 AptosNetwork::Devnet => "https://fullnode.devnet.aptoslabs.com/v1",
43 AptosNetwork::Custom { .. } => "",
44 }
45 }
46
47 pub fn default_indexer_url(&self) -> &'static str {
49 match self {
50 AptosNetwork::Mainnet => "https://indexer.mainnet.aptoslabs.com/v1/graphql",
51 AptosNetwork::Testnet => "https://indexer.testnet.aptoslabs.com/v1/graphql",
52 AptosNetwork::Devnet => "",
53 AptosNetwork::Custom { .. } => "",
54 }
55 }
56
57 pub fn explorer_url(&self) -> &'static str {
59 match self {
60 AptosNetwork::Mainnet => "https://explorer.aptoslabs.com",
61 AptosNetwork::Testnet => "https://explorer.aptoslabs.com",
62 AptosNetwork::Devnet => "",
63 AptosNetwork::Custom { .. } => "",
64 }
65 }
66
67 pub fn known_validator_count(&self) -> u64 {
69 match self {
70 AptosNetwork::Mainnet => 100, AptosNetwork::Testnet => 10, AptosNetwork::Devnet => 4, AptosNetwork::Custom { .. } => 4,
74 }
75 }
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize)]
80pub struct CheckpointConfig {
81 pub require_certified: bool,
83 pub max_epoch_lookback: u64,
85 pub timeout_ms: u64,
87}
88
89impl Default for CheckpointConfig {
90 fn default() -> Self {
91 Self {
92 require_certified: true,
93 max_epoch_lookback: 5,
94 timeout_ms: 30_000,
95 }
96 }
97}
98
99#[derive(Clone, Debug, Serialize, Deserialize)]
101pub struct TransactionConfig {
102 pub max_gas: u64,
104 pub confirmation_timeout_ms: u64,
106 pub max_retries: u32,
108 pub retry_delay_ms: u64,
110}
111
112impl Default for TransactionConfig {
113 fn default() -> Self {
114 Self {
115 max_gas: 100_000,
116 confirmation_timeout_ms: 30_000,
117 max_retries: 3,
118 retry_delay_ms: 1_000,
119 }
120 }
121}
122
123#[derive(Clone, Debug, Serialize, Deserialize)]
125pub struct SealContractConfig {
126 pub module_address: String,
128 pub module_name: String,
130 pub seal_resource: String,
132}
133
134impl Default for SealContractConfig {
135 fn default() -> Self {
136 Self {
137 module_address: "0x1".to_string(),
138 module_name: "csv_seal".to_string(),
139 seal_resource: "Seal".to_string(),
140 }
141 }
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize)]
146pub struct AptosConfig {
147 pub network: AptosNetwork,
149 pub rpc_url: String,
151 pub indexer_url: Option<String>,
153 pub checkpoint: CheckpointConfig,
155 pub transaction: TransactionConfig,
157 pub seal_contract: SealContractConfig,
159}
160
161impl Default for AptosConfig {
162 fn default() -> Self {
163 let network = AptosNetwork::Devnet;
164 Self {
165 network: network.clone(),
166 rpc_url: network.default_rpc_url().to_string(),
167 indexer_url: None,
168 checkpoint: CheckpointConfig::default(),
169 transaction: TransactionConfig::default(),
170 seal_contract: SealContractConfig::default(),
171 }
172 }
173}
174
175impl AptosConfig {
176 pub fn new(network: AptosNetwork) -> Self {
178 Self {
179 rpc_url: network.default_rpc_url().to_string(),
180 network,
181 ..Self::default()
182 }
183 }
184
185 pub fn with_rpc(network: AptosNetwork, rpc_url: impl Into<String>) -> Self {
187 Self {
188 rpc_url: rpc_url.into(),
189 network,
190 ..Self::default()
191 }
192 }
193
194 pub fn validate(&self) -> Result<(), String> {
196 if self.rpc_url.is_empty() {
197 return Err("RPC URL cannot be empty".to_string());
198 }
199 if self.transaction.max_gas == 0 {
200 return Err("Max gas must be greater than 0".to_string());
201 }
202 if self.transaction.confirmation_timeout_ms == 0 {
203 return Err("Confirmation timeout must be greater than 0".to_string());
204 }
205 if self.checkpoint.max_epoch_lookback == 0 {
206 return Err("Epoch lookback must be greater than 0".to_string());
207 }
208 if self.seal_contract.module_address.is_empty() {
209 return Err("Seal contract address cannot be empty".to_string());
210 }
211 Ok(())
212 }
213
214 pub fn chain_id(&self) -> u8 {
216 self.network.chain_id()
217 }
218
219 pub fn f_plus_one(&self) -> u64 {
222 let n = self.network.known_validator_count();
223 (2 * n) / 3 + 1
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_network_chain_ids() {
235 assert_eq!(AptosNetwork::Mainnet.chain_id(), 1);
236 assert_eq!(AptosNetwork::Testnet.chain_id(), 2);
237 assert_eq!(AptosNetwork::Devnet.chain_id(), 4);
238 assert_eq!(
239 AptosNetwork::Custom {
240 chain_id: 99,
241 name: "local".to_string()
242 }
243 .chain_id(),
244 99
245 );
246 }
247
248 #[test]
249 fn test_default_rpc_urls() {
250 assert!(AptosNetwork::Mainnet.default_rpc_url().contains("mainnet"));
251 assert!(AptosNetwork::Testnet.default_rpc_url().contains("testnet"));
252 }
253
254 #[test]
255 fn test_config_validation() {
256 let config = AptosConfig::default();
257 assert!(config.validate().is_ok());
258 }
259
260 #[test]
261 fn test_config_custom_rpc() {
262 let config = AptosConfig::with_rpc(AptosNetwork::Mainnet, "https://custom.example.com");
263 assert_eq!(config.rpc_url, "https://custom.example.com");
264 assert_eq!(config.network.chain_id(), 1);
265 }
266
267 #[test]
268 fn test_f_plus_one() {
269 let config = AptosConfig::new(AptosNetwork::Devnet);
270 assert!(config.f_plus_one() >= 3);
272 }
273
274 #[test]
275 fn test_invalid_config() {
276 let mut config = AptosConfig::default();
277 config.rpc_url = "".to_string();
278 assert!(config.validate().is_err());
279
280 let mut config = AptosConfig::default();
281 config.transaction.max_gas = 0;
282 assert!(config.validate().is_err());
283 }
284}