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
3 changes: 2 additions & 1 deletion crates/iota-sdk-graphql-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ bcs.workspace = true
chrono = "0.4.26"
cynic.workspace = true
derive_more = { workspace = true, features = ["from"] }
eyre.workspace = true
futures.workspace = true
reqwest = { workspace = true, default-features = false, features = ["rustls-tls", "json"] }
serde.workspace = true
serde_json.workspace = true
strum = { workspace = true, features = ["derive"] }
thiserror.workspace = true
tokio = { workspace = true, features = ["time"] }
tracing = "0.1.37"
url = "2.5.3"
Expand All @@ -30,6 +30,7 @@ iota-types = { workspace = true, features = ["serde", "hash"] }
getrandom = { version = "0.2", features = ["js"] }

[dev-dependencies]
eyre.workspace = true
rand.workspace = true

iota-types = { workspace = true, features = ["serde", "rand", "hash"] }
Expand Down
80 changes: 45 additions & 35 deletions crates/iota-sdk-graphql-client/src/faucet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use std::time::Duration;

use eyre::{bail, eyre};
use iota_types::{Address, Digest, ObjectId};
use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize};
Expand All @@ -18,6 +17,26 @@ pub const FAUCET_LOCAL_HOST: &str = "http://localhost:9123";
const FAUCET_REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
const FAUCET_POLL_INTERVAL: Duration = Duration::from_secs(2);

#[derive(thiserror::Error, Debug)]
pub enum FaucetError {
#[error("Cannot fetch request status due to a bad gateway.")]
BadGateway,
#[error("Faucet request was unsuccessful: {0}")]
Request(String),
#[error("Reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),
#[error("Faucet request was unsuccessful: {0}")]
StatusCode(StatusCode),
#[error("Faucet request timed out")]
TimedOut,
#[error(
"Faucet service received too many requests from this IP address. Please try again after 60 minutes."
)]
TooManyRequests,
#[error("Faucet service is currently overloaded or unavailable. Please try again later.")]
Unavailable,
}

pub struct FaucetClient {
faucet_url: Url,
inner: reqwest::Client,
Expand Down Expand Up @@ -95,13 +114,13 @@ impl FaucetClient {
/// Request gas from the faucet. Note that this will return the UUID of the
/// request and not wait until the token is received. Use
/// `request_and_wait` to wait for the token.
pub async fn request(&self, address: Address) -> eyre::Result<Option<String>> {
pub async fn request(&self, address: Address) -> Result<Option<String>, FaucetError> {
self.request_impl(address).await
}

/// Internal implementation of a faucet request. It returns the task Uuid as
/// a String.
async fn request_impl(&self, address: Address) -> eyre::Result<Option<String>> {
async fn request_impl(&self, address: Address) -> Result<Option<String>, FaucetError> {
let address = address.to_string();
let json_body = json![{
"FixedAmountRequest": {
Expand All @@ -126,27 +145,23 @@ impl FaucetClient {

if let Some(err) = faucet_resp.error {
error!("Faucet request was unsuccessful: {err}");
bail!("Faucet request was unsuccessful: {err}")
Err(FaucetError::Request(err))
} else {
info!("Request successful: {:?}", faucet_resp.task);
Ok(faucet_resp.task)
}
}
StatusCode::TOO_MANY_REQUESTS => {
error!("Faucet service received too many requests from this IP address.");
bail!(
"Faucet service received too many requests from this IP address. Please try again after 60 minutes."
);
Err(FaucetError::TooManyRequests)
}
StatusCode::SERVICE_UNAVAILABLE => {
error!("Faucet service is currently overloaded or unavailable.");
bail!(
"Faucet service is currently overloaded or unavailable. Please try again later."
);
Err(FaucetError::Unavailable)
}
status_code => {
error!("Faucet request was unsuccessful: {status_code}");
bail!("Faucet request was unsuccessful: {status_code}");
Err(FaucetError::StatusCode(status_code))
}
}
}
Expand All @@ -158,21 +173,25 @@ impl FaucetClient {
///
/// Note that the faucet is heavily rate-limited, so calling repeatedly the
/// faucet would likely result in a 429 code or 502 code.
pub async fn request_and_wait(&self, address: Address) -> eyre::Result<Option<FaucetReceipt>> {
pub async fn request_and_wait(
&self,
address: Address,
) -> Result<Option<FaucetReceipt>, FaucetError> {
let request_id = self.request(address).await?;

if let Some(request_id) = request_id {
let poll_response = tokio::time::timeout(FAUCET_REQUEST_TIMEOUT, async {
let status_response = tokio::time::timeout(FAUCET_REQUEST_TIMEOUT, async {
let mut interval = tokio::time::interval(FAUCET_POLL_INTERVAL);
loop {
interval.tick().await;
info!("Polling faucet request status: {request_id}");
let req = self.request_status(request_id.clone()).await;
let status_response = self.request_status(request_id.clone()).await?;

if let Ok(Some(poll_response)) = req {
match poll_response.status {
if let Some(status_response) = status_response {
match status_response.status {
BatchSendStatusType::Succeeded => {
info!("Faucet request {request_id} succeeded");
break Ok(poll_response);
break Ok::<_, FaucetError>(status_response);
}
BatchSendStatusType::Discarded => {
break Ok(BatchSendStatus {
Expand All @@ -184,12 +203,6 @@ impl FaucetClient {
continue;
}
}
} else if let Some(err) = req.err() {
error!("Faucet request {request_id} failed. Error: {:?}", err);
break Err(eyre!(
"Faucet request {request_id} failed. Error: {:?}",
err
));
}
}
})
Expand All @@ -199,9 +212,9 @@ impl FaucetClient {
"Faucet request {request_id} timed out. Timeout set to {} seconds",
FAUCET_REQUEST_TIMEOUT.as_secs()
);
eyre!("Faucet request timed out")
FaucetError::TimedOut
})??;
Ok(poll_response.transferred_gas_objects)
Ok(status_response.transferred_gas_objects)
} else {
Ok(None)
}
Expand All @@ -210,22 +223,19 @@ impl FaucetClient {
/// Check the faucet request status.
///
/// Possible statuses are defined in: [`BatchSendStatusType`]
pub async fn request_status(&self, id: String) -> eyre::Result<Option<BatchSendStatus>> {
pub async fn request_status(&self, id: String) -> Result<Option<BatchSendStatus>, FaucetError> {
let status_url = format!("{}v1/status/{}", self.faucet_url, id);
info!("Checking status of faucet request: {status_url}");
let response = self.inner.get(&status_url).send().await?;

if response.status() == StatusCode::TOO_MANY_REQUESTS {
bail!("Cannot fetch request status due to too many requests from this IP address.");
return Err(FaucetError::TooManyRequests);
} else if response.status() == StatusCode::BAD_GATEWAY {
bail!("Cannot fetch request status due to a bad gateway.")
return Err(FaucetError::BadGateway);
}
let json = response
.json::<BatchStatusFaucetResponse>()
.await
.map_err(|e| {
error!("Failed to parse faucet response: {:?}", e);
eyre!("Failed to parse faucet response: {:?}", e)
})?;

let json = response.json::<BatchStatusFaucetResponse>().await?;

Ok(json.status)
}
}
4 changes: 2 additions & 2 deletions crates/iota-sdk-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ proptest = ["dep:proptest", "dep:test-strategy", "serde"]
base64ct = { workspace = true, features = ["alloc"] }
bnum.workspace = true
bs58 = "0.5.1"
eyre.workspace = true
hex.workspace = true
paste.workspace = true
roaring.workspace = true
Expand All @@ -58,7 +57,7 @@ serde_json = { workspace = true, optional = true }
serde_repr = { version = "0.1", optional = true }
serde_with = { version = "3.9", default-features = false, features = ["alloc"], optional = true }

# JsonSchema definitions for types, useful for generating an OpenAPI Specificaiton.
# JsonSchema definitions for types, useful for generating an OpenAPI Specification.
schemars = { version = "0.8.21", optional = true }

# RNG support
Expand All @@ -73,6 +72,7 @@ test-strategy = { workspace = true, optional = true }

[dev-dependencies]
bcs.workspace = true
eyre.workspace = true
jsonschema = { version = "0.20", default-features = false }
num-bigint = "0.4.6"
paste.workspace = true
Expand Down
31 changes: 18 additions & 13 deletions crates/iota-sdk-types/src/crypto/intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
#[cfg(feature = "serde")]
use std::str::FromStr;

#[cfg(feature = "serde")]
use eyre::eyre;

pub const INTENT_PREFIX_LENGTH: usize = 3;

pub enum IntentError {
Bytes,
Intent,
Scope,
Version,
AppId,
}

/// A Signing Intent
///
/// An intent is a compact struct that serves as the domain separator for a
Expand Down Expand Up @@ -95,9 +100,9 @@ impl Intent {
}

#[cfg(feature = "serde")]
pub fn from_bytes(bytes: &[u8]) -> Result<Self, eyre::Report> {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, IntentError> {
if bytes.len() != INTENT_PREFIX_LENGTH {
return Err(eyre!("Invalid Intent"));
return Err(IntentError::Bytes);
}
Ok(Self {
scope: bytes[0].try_into()?,
Expand All @@ -109,11 +114,11 @@ impl Intent {

#[cfg(feature = "serde")]
impl FromStr for Intent {
type Err = eyre::Report;
type Err = IntentError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes: Vec<u8> =
hex::decode(s.strip_prefix("0x").unwrap_or(s)).map_err(|_| eyre!("Invalid Intent"))?;
hex::decode(s.strip_prefix("0x").unwrap_or(s)).map_err(|_| IntentError::Intent)?;
Self::from_bytes(bytes.as_slice())
}
}
Expand Down Expand Up @@ -170,10 +175,10 @@ impl IntentScope {

#[cfg(feature = "serde")]
impl TryFrom<u8> for IntentScope {
type Error = eyre::Report;
type Error = IntentError;

fn try_from(value: u8) -> Result<Self, Self::Error> {
bcs::from_bytes(&[value]).map_err(|_| eyre!("Invalid IntentScope"))
bcs::from_bytes(&[value]).map_err(|_| IntentError::Scope)
}
}

Expand Down Expand Up @@ -207,10 +212,10 @@ impl IntentVersion {

#[cfg(feature = "serde")]
impl TryFrom<u8> for IntentVersion {
type Error = eyre::Report;
type Error = IntentError;

fn try_from(value: u8) -> Result<Self, Self::Error> {
bcs::from_bytes(&[value]).map_err(|_| eyre!("Invalid IntentVersion"))
bcs::from_bytes(&[value]).map_err(|_| IntentError::Version)
}
}

Expand Down Expand Up @@ -247,10 +252,10 @@ impl IntentAppId {

#[cfg(feature = "serde")]
impl TryFrom<u8> for IntentAppId {
type Error = eyre::Report;
type Error = IntentError;

fn try_from(value: u8) -> Result<Self, Self::Error> {
bcs::from_bytes(&[value]).map_err(|_| eyre!("Invalid IntentAppId"))
bcs::from_bytes(&[value]).map_err(|_| IntentError::AppId)
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/iota-sdk-types/src/iota_names/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use std::str::FromStr;

use crate::{Address, ObjectId};
use crate::{ObjectId, address::Address, iota_names::error::IotaNamesError};

#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(
Expand Down Expand Up @@ -48,7 +48,7 @@ impl IotaNamesConfig {
}
}

pub fn from_env() -> eyre::Result<Self> {
pub fn from_env() -> Result<Self, IotaNamesError> {
Ok(Self::new(
std::env::var("IOTA_NAMES_PACKAGE_ADDRESS")?.parse()?,
std::env::var("IOTA_NAMES_OBJECT_ID")?.parse()?,
Expand Down
8 changes: 6 additions & 2 deletions crates/iota-sdk-types/src/iota_names/error.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright (c) 2025 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use crate::ObjectId;
use crate::{ObjectId, address::AddressParseError};

#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
// #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum IotaNamesError {
#[error("Name length {0} exceeds maximum length {1}")]
NameLengthExceeded(usize, usize),
Expand All @@ -26,4 +26,8 @@ pub enum IotaNamesError {
MalformedObject(ObjectId),
#[error("Invalid TLN {0}")]
InvalidTln(String),
#[error("Missing environment variable {0}")]
MissingEnvVar(#[from] std::env::VarError),
#[error("Address parser error {0}")]
AddressParser(#[from] AddressParseError),
}
11 changes: 5 additions & 6 deletions crates/iota-sdk-types/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,12 +449,11 @@ impl Object {
}

#[cfg(feature = "serde")]
pub fn to_rust<T: serde::de::DeserializeOwned>(&self) -> eyre::Result<T> {
use eyre::OptionExt;

Ok(bcs::from_bytes::<T>(
&self.as_struct_opt().ok_or_eyre("not a struct")?.contents,
)?)
pub fn to_rust<T: serde::de::DeserializeOwned>(
&self,
) -> Result<T, Box<dyn std::error::Error + Send + Sync>> {
let contents = &self.as_struct_opt().ok_or("not a struct")?.contents;
Ok(bcs::from_bytes::<T>(contents)?)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ edition = "2021"

[dependencies]
bcs.workspace = true
eyre.workspace = true
fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "5f2c63266a065996d53f98156f0412782b468597" }
futures.workspace = true
tokio.workspace = true

iota-json-rpc-types = { path = "../../../../../../../iota/crates/iota-json-rpc-types" }
iota-types = { path = "../../../../../../../iota/crates/iota-types" }
test-cluster = { path = "../../../../../../../iota/crates/test-cluster" }

[dev-dependencies]
eyre.workspace = true