Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions crates/aptos-rust-sdk-types/src/api_types/ledger_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use crate::api_types::chain_id::ChainId;
use crate::api_types::numbers::U64;
use serde::{Deserialize, Serialize};

/// Ledger information returned from the RPC API
/// This represents the JSON response from the `/v1` endpoint
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct LedgerInfo {
/// The chain ID of the network
#[serde(rename = "chain_id")]
pub chain_id: u8,

/// The current epoch number
#[serde(rename = "epoch")]
pub epoch: U64,

/// The current ledger version
#[serde(rename = "ledger_version")]
pub ledger_version: U64,

/// The oldest ledger version available
#[serde(rename = "oldest_ledger_version")]
pub oldest_ledger_version: U64,

/// The ledger timestamp in microseconds
#[serde(rename = "ledger_timestamp")]
pub ledger_timestamp: U64,

/// The role of the node (e.g., "full_node")
#[serde(rename = "node_role")]
pub node_role: String,

/// The oldest block height available
#[serde(rename = "oldest_block_height")]
pub oldest_block_height: U64,

/// The current block height
#[serde(rename = "block_height")]
pub block_height: U64,

/// The git hash of the node software
#[serde(rename = "git_hash")]
pub git_hash: String,
}

impl LedgerInfo {
/// Get the chain ID as a ChainId enum
pub fn chain_id(&self) -> ChainId {
ChainId::from_u8(self.chain_id)
}

/// Get the epoch as u64
pub fn epoch_u64(&self) -> u64 {
self.epoch.as_u64()
}

/// Get the ledger version as u64
pub fn ledger_version_u64(&self) -> u64 {
self.ledger_version.as_u64()
}

/// Get the oldest ledger version as u64
pub fn oldest_ledger_version_u64(&self) -> u64 {
self.oldest_ledger_version.as_u64()
}

/// Get the ledger timestamp as u64
pub fn ledger_timestamp_u64(&self) -> u64 {
self.ledger_timestamp.as_u64()
}

/// Get the oldest block height as u64
pub fn oldest_block_height_u64(&self) -> u64 {
self.oldest_block_height.as_u64()
}

/// Get the block height as u64
pub fn block_height_u64(&self) -> u64 {
self.block_height.as_u64()
}
}
1 change: 1 addition & 0 deletions crates/aptos-rust-sdk-types/src/api_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod chain_id;
pub mod event;
pub mod hash;
pub mod identifier;
pub mod ledger_info;
pub mod module_id;
pub mod move_types;
pub mod numbers;
Expand Down
17 changes: 17 additions & 0 deletions crates/aptos-rust-sdk-types/src/api_types/numbers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,20 @@ impl<'de> Deserialize<'de> for U64 {
.map_err(|err| D::Error::custom(err.to_string()))
}
}

impl U64 {
/// Get the inner u64 value
pub fn as_u64(&self) -> u64 {
self.0
}

/// Convert into u64
pub fn into_u64(self) -> u64 {
self.0
}

/// Create a new U64 from a u64
pub fn new(value: u64) -> Self {
U64(value)
}
}
130 changes: 109 additions & 21 deletions crates/aptos-rust-sdk/src/client/builder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::client::config::AptosNetwork;
use crate::client::rest_api::AptosFullnodeClient;
use aptos_rust_sdk_types::headers::X_APTOS_CLIENT;
use aptos_rust_sdk_types::AptosResult;
use aptos_rust_sdk_types::{api_types::ledger_info::LedgerInfo, headers::X_APTOS_CLIENT};
use reqwest::{
header::{self, HeaderMap, HeaderName, HeaderValue},
Client as ReqwestClient, ClientBuilder as ReqwestClientBuilder,
Expand All @@ -23,25 +23,20 @@ pub struct AptosClientBuilder {

impl AptosClientBuilder {
/// A hidden constructor, please use `AptosClient::builder()` to create
pub fn new(network: AptosNetwork) -> Self {
let mut headers = HeaderMap::new();
pub fn new(network: AptosNetwork, headers: Option<HeaderMap>) -> Self {
let mut headers = headers.unwrap_or_default();

headers.insert(
X_APTOS_CLIENT,
HeaderValue::from_static(X_APTOS_SDK_HEADER_VALUE),
);

let mut client_builder = Self {
Self {
rest_api_client_builder: ReqwestClient::builder(),
network,
timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECONDS), // Default to 5 seconds
headers,
};

// TODO: This seems like a bit of a hack here and needs to be documented
if let Ok(key) = env::var("X_API_KEY") {
client_builder = client_builder.api_key(&key).unwrap();
}
client_builder
}

pub fn network(mut self, network: AptosNetwork) -> Self {
Expand Down Expand Up @@ -70,16 +65,109 @@ impl AptosClientBuilder {
Ok(self)
}

pub fn build(self) -> AptosFullnodeClient {
AptosFullnodeClient {
network: self.network,
rest_client: self
.rest_api_client_builder
.default_headers(self.headers)
.timeout(self.timeout)
.cookie_store(true)
.build()
.unwrap(),
}
pub async fn build(self) -> Result<AptosFullnodeClient, anyhow::Error> {
let rest_client = self
.rest_api_client_builder
.default_headers(self.headers)
.timeout(self.timeout)
.cookie_store(true)
.build()?;

// Fetch chain_id from RPC if not set
let network = if self.network.chain_id().is_some() {
self.network
} else {
let url = self.network.rest_url().join("v1")?;
let ledger_info: LedgerInfo = rest_client.get(url).send().await?.json().await?;
let chain_id = ledger_info.chain_id();
self.network.with_chain_id(Some(chain_id))
};

Ok(AptosFullnodeClient {
network,
rest_client,
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use aptos_rust_sdk_types::api_types::chain_id::ChainId;
use url::Url;

#[tokio::test]
async fn test_build_with_invalid_url() {
// Test with an invalid URL that cannot be parsed
// This should fail when trying to create the network
let invalid_url = match Url::parse("not-a-valid-url") {
Ok(url) => url,
Err(_) => {
// If URL parsing fails, we can't even create the network
// This is expected behavior - invalid URLs should fail at network creation
return;
}
};

// If we somehow get here, try to create a network with invalid URL
let network = AptosNetwork::new("invalid", invalid_url, None, None);
let builder = AptosClientBuilder::new(network, None);

// Building should fail when trying to join the path
let result = builder.build().await;
assert!(result.is_err(), "Building with invalid URL should fail");
}

#[tokio::test]
async fn test_build_with_unreachable_url() {
// Test with a valid URL format but unreachable server
// Use a non-existent domain to ensure connection failure
let unreachable_url =
Url::parse("http://this-domain-does-not-exist-12345.invalid:8080").unwrap();
// Ensure chain_id is None so it tries to fetch from server
let network = AptosNetwork::new("unreachable", unreachable_url, None, None);
let builder = AptosClientBuilder::new(network, None);

// Building should fail when trying to fetch chain_id from unreachable server
let result = builder.build().await;
assert!(result.is_err(), "Building with unreachable URL should fail");

// Check that the error is related to connection failure or DNS resolution
let error_msg = format!("{}", result.unwrap_err());
assert!(
error_msg.contains("connection")
|| error_msg.contains("refused")
|| error_msg.contains("timeout")
|| error_msg.contains("failed")
|| error_msg.contains("error sending request")
|| error_msg.contains("error decoding response body")
|| error_msg.contains("dns")
|| error_msg.contains("resolve"),
"Error should be related to connection/DNS failure, got: {}",
error_msg
);
}

#[tokio::test]
async fn test_build_with_unreachable_url_no_chain_id() {
// Test fetching chain_id from server when it's not set initially
// Use mainnet URL but without chain_id to test fetching chain_id from server
let base_url = AptosNetwork::mainnet().rest_url().clone();
// Ensure chain_id is None so it tries to fetch from server
let network = AptosNetwork::new("test", base_url, None, None);
let builder = AptosClientBuilder::new(network, None);

// Build should succeed and fetch chain_id from the server
let client = builder
.build()
.await
.expect("Should successfully build client");

// Verify that chain_id was fetched from the server
let chain_id = client
.network
.chain_id()
.expect("Chain id should be fetched from server");
assert_eq!(chain_id, ChainId::Mainnet, "Chain id should match mainnet");
}
}
52 changes: 44 additions & 8 deletions crates/aptos-rust-sdk/src/client/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use aptos_rust_sdk_types::api_types::chain_id::ChainId;
use url::Url;

const MAINNET_REST_URL: &str = "https://api.mainnet.aptoslabs.com";
Expand All @@ -15,47 +16,58 @@ const LOCAL_INDEXER_URL: &str = "http://127.0.0.1:8090";
pub struct AptosNetwork {
name: &'static str,
rest_url: Url,
indexer_url: Url,
indexer_url: Option<Url>,
chain_id: Option<ChainId>,
}

impl AptosNetwork {
pub const fn new(name: &'static str, rest_url: Url, indexer_url: Url) -> AptosNetwork {
pub const fn new(
name: &'static str,
rest_url: Url,
indexer_url: Option<Url>,
chain_id: Option<ChainId>,
) -> AptosNetwork {
AptosNetwork {
name,
rest_url,
indexer_url,
chain_id,
}
}

pub fn mainnet() -> Self {
Self::new(
"mainnet",
Url::parse(MAINNET_REST_URL).unwrap(),
Url::parse(MAINNET_INDEXER_URL).unwrap(),
Some(Url::parse(MAINNET_INDEXER_URL).unwrap()),
Some(ChainId::Mainnet),
)
}

pub fn testnet() -> Self {
Self::new(
"testnet",
Url::parse(TESTNET_REST_URL).unwrap(),
Url::parse(TESTNET_INDEXER_URL).unwrap(),
Some(Url::parse(TESTNET_INDEXER_URL).unwrap()),
Some(ChainId::Testnet),
)
}

pub fn devnet() -> Self {
Self::new(
"devnet",
Url::parse(DEVNET_REST_URL).unwrap(),
Url::parse(DEVNET_INDEXER_URL).unwrap(),
Some(Url::parse(DEVNET_INDEXER_URL).unwrap()),
None,
)
}

pub fn localnet() -> Self {
Self::new(
"localnet",
Url::parse(LOCAL_REST_URL).unwrap(),
Url::parse(LOCAL_INDEXER_URL).unwrap(),
Some(Url::parse(LOCAL_INDEXER_URL).unwrap()),
Some(ChainId::Localnet),
)
}

Expand All @@ -67,7 +79,31 @@ impl AptosNetwork {
&self.rest_url
}

pub fn indexer_url(&self) -> &Url {
&self.indexer_url
pub fn indexer_url(&self) -> Option<&Url> {
self.indexer_url.as_ref()
}

pub fn chain_id(&self) -> Option<ChainId> {
self.chain_id
}

pub fn with_name(mut self, name: &'static str) -> Self {
self.name = name;
self
}

pub fn with_rest_url(mut self, rest_url: Url) -> Self {
self.rest_url = rest_url;
self
}

pub fn with_indexer_url(mut self, indexer_url: Option<Url>) -> Self {
self.indexer_url = indexer_url;
self
}

pub fn with_chain_id(mut self, chain_id: Option<ChainId>) -> Self {
self.chain_id = chain_id;
self
}
}
Loading