use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AptosNetwork {
Mainnet,
Testnet,
Devnet,
Custom {
chain_id: u8,
name: String,
},
}
impl AptosNetwork {
pub fn chain_id(&self) -> u8 {
match self {
AptosNetwork::Mainnet => 1,
AptosNetwork::Testnet => 2,
AptosNetwork::Devnet => 4,
AptosNetwork::Custom { chain_id, .. } => *chain_id,
}
}
pub fn default_rpc_url(&self) -> &'static str {
match self {
AptosNetwork::Mainnet => "https://fullnode.mainnet.aptoslabs.com/v1",
AptosNetwork::Testnet => "https://fullnode.testnet.aptoslabs.com/v1",
AptosNetwork::Devnet => "https://fullnode.devnet.aptoslabs.com/v1",
AptosNetwork::Custom { .. } => "",
}
}
pub fn default_indexer_url(&self) -> &'static str {
match self {
AptosNetwork::Mainnet => "https://indexer.mainnet.aptoslabs.com/v1/graphql",
AptosNetwork::Testnet => "https://indexer.testnet.aptoslabs.com/v1/graphql",
AptosNetwork::Devnet => "",
AptosNetwork::Custom { .. } => "",
}
}
pub fn explorer_url(&self) -> &'static str {
match self {
AptosNetwork::Mainnet => "https://explorer.aptoslabs.com",
AptosNetwork::Testnet => "https://explorer.aptoslabs.com",
AptosNetwork::Devnet => "",
AptosNetwork::Custom { .. } => "",
}
}
pub fn known_validator_count(&self) -> u64 {
match self {
AptosNetwork::Mainnet => 100, AptosNetwork::Testnet => 10, AptosNetwork::Devnet => 4, AptosNetwork::Custom { .. } => 4,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CheckpointConfig {
pub require_certified: bool,
pub max_epoch_lookback: u64,
pub timeout_ms: u64,
}
impl Default for CheckpointConfig {
fn default() -> Self {
Self {
require_certified: true,
max_epoch_lookback: 5,
timeout_ms: 30_000,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TransactionConfig {
pub max_gas: u64,
pub confirmation_timeout_ms: u64,
pub max_retries: u32,
pub retry_delay_ms: u64,
}
impl Default for TransactionConfig {
fn default() -> Self {
Self {
max_gas: 100_000,
confirmation_timeout_ms: 30_000,
max_retries: 3,
retry_delay_ms: 1_000,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SealContractConfig {
pub module_address: String,
pub module_name: String,
pub seal_resource: String,
}
impl Default for SealContractConfig {
fn default() -> Self {
Self {
module_address: "0x1".to_string(),
module_name: "csv_seal".to_string(),
seal_resource: "Seal".to_string(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AptosConfig {
pub network: AptosNetwork,
pub rpc_url: String,
pub indexer_url: Option<String>,
pub checkpoint: CheckpointConfig,
pub transaction: TransactionConfig,
pub seal_contract: SealContractConfig,
}
impl Default for AptosConfig {
fn default() -> Self {
let network = AptosNetwork::Devnet;
Self {
network: network.clone(),
rpc_url: network.default_rpc_url().to_string(),
indexer_url: None,
checkpoint: CheckpointConfig::default(),
transaction: TransactionConfig::default(),
seal_contract: SealContractConfig::default(),
}
}
}
impl AptosConfig {
pub fn new(network: AptosNetwork) -> Self {
Self {
rpc_url: network.default_rpc_url().to_string(),
network,
..Self::default()
}
}
pub fn with_rpc(network: AptosNetwork, rpc_url: impl Into<String>) -> Self {
Self {
rpc_url: rpc_url.into(),
network,
..Self::default()
}
}
pub fn validate(&self) -> Result<(), String> {
if self.rpc_url.is_empty() {
return Err("RPC URL cannot be empty".to_string());
}
if self.transaction.max_gas == 0 {
return Err("Max gas must be greater than 0".to_string());
}
if self.transaction.confirmation_timeout_ms == 0 {
return Err("Confirmation timeout must be greater than 0".to_string());
}
if self.checkpoint.max_epoch_lookback == 0 {
return Err("Epoch lookback must be greater than 0".to_string());
}
if self.seal_contract.module_address.is_empty() {
return Err("Seal contract address cannot be empty".to_string());
}
Ok(())
}
pub fn chain_id(&self) -> u8 {
self.network.chain_id()
}
pub fn f_plus_one(&self) -> u64 {
let n = self.network.known_validator_count();
(2 * n) / 3 + 1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_network_chain_ids() {
assert_eq!(AptosNetwork::Mainnet.chain_id(), 1);
assert_eq!(AptosNetwork::Testnet.chain_id(), 2);
assert_eq!(AptosNetwork::Devnet.chain_id(), 4);
assert_eq!(
AptosNetwork::Custom {
chain_id: 99,
name: "local".to_string()
}
.chain_id(),
99
);
}
#[test]
fn test_default_rpc_urls() {
assert!(AptosNetwork::Mainnet.default_rpc_url().contains("mainnet"));
assert!(AptosNetwork::Testnet.default_rpc_url().contains("testnet"));
}
#[test]
fn test_config_validation() {
let config = AptosConfig::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_config_custom_rpc() {
let config = AptosConfig::with_rpc(AptosNetwork::Mainnet, "https://custom.example.com");
assert_eq!(config.rpc_url, "https://custom.example.com");
assert_eq!(config.network.chain_id(), 1);
}
#[test]
fn test_f_plus_one() {
let config = AptosConfig::new(AptosNetwork::Devnet);
assert!(config.f_plus_one() >= 3);
}
#[test]
fn test_invalid_config() {
let mut config = AptosConfig::default();
config.rpc_url = "".to_string();
assert!(config.validate().is_err());
let mut config = AptosConfig::default();
config.transaction.max_gas = 0;
assert!(config.validate().is_err());
}
}