Skip to content
Merged
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
12 changes: 9 additions & 3 deletions contracts/campaign/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@ impl CampaignContract {
/// Get campaign status
pub fn get_status(env: Env) -> (Symbol, Symbol, i128, u64) {
let key = Symbol::new(&env, "campaign_data");
env.storage().instance().get(&key).unwrap()
env.storage()
.instance()
.get(&key)
.expect("campaign not initialized")
}

/// Check if campaign is active
pub fn is_active(env: Env) -> bool {
let key = Symbol::new(&env, "campaign_data");
let (_id, _title, _target, deadline): (Symbol, Symbol, i128, u64) =
env.storage().instance().get(&key).unwrap();
let (_id, _title, _target, deadline): (Symbol, Symbol, i128, u64) = env
.storage()
.instance()
.get(&key)
.expect("campaign not initialized");

let current_time = env.ledger().timestamp();
current_time < deadline
Expand Down
7 changes: 5 additions & 2 deletions contracts/withdrawal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ impl WithdrawalContract {
/// Withdraw funds from the contract
pub fn withdraw(env: Env, amount: i128) -> bool {
let key = Symbol::new(&env, "settings");
let (beneficiary, max_withdrawal): (Address, i128) =
env.storage().instance().get(&key).unwrap();
let (beneficiary, max_withdrawal): (Address, i128) = env
.storage()
.instance()
.get(&key)
.expect("withdrawal not initialized");

beneficiary.require_auth();

Expand Down
146 changes: 146 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//! Unified error types for the StellarAid blockchain integration layer.
//!
//! [`StellarAidError`] is the top-level error enum that downstream code should
//! use as its return type. Each variant maps to a distinct failure domain and
//! carries enough context for callers to decide how to recover (or surface a
//! useful message).
//!
//! Existing per-module error types ([`KeyError`](crate::utils::keypair::KeyError),
//! [`StellarError`](crate::friendbot::utils::types::StellarError),
//! [`TokenSetupError`](crate::setup::token_setup::TokenSetupError)) are
//! automatically converted via [`From`] impls so the `?` operator works
//! transparently.

use thiserror::Error;

use crate::friendbot::utils::types::StellarError;
use crate::setup::token_setup::TokenSetupError;
use crate::utils::keypair::KeyError;

/// Top-level error type for the StellarAid integration layer.
#[derive(Debug, Error)]
pub enum StellarAidError {
/// Stellar Horizon REST-API returned an error or an unexpected response.
#[error("Horizon API error: {0}")]
HorizonError(String),

/// Soroban JSON-RPC call failed.
#[error("Soroban RPC error (code {code}): {message}")]
SorobanError { code: i64, message: String },

/// Key generation, parsing, or derivation failed.
#[error("Keypair error: {0}")]
KeypairError(String),

/// Input or state did not meet a business-logic precondition.
#[error("Validation error: {0}")]
ValidationError(String),

/// A submitted transaction was rejected or reverted by the network.
#[error("Transaction failed: {0}")]
TransactionFailed(String),

/// An on-chain smart-contract call returned an error.
#[error("Contract error: {0}")]
ContractError(String),

/// A lower-level network / HTTP / I/O error.
#[error("Network error: {0}")]
NetworkError(String),
}

// ── From impls for ergonomic `?` propagation ────────────────────────────────

impl From<KeyError> for StellarAidError {
fn from(err: KeyError) -> Self {
Self::KeypairError(err.to_string())
}
}

impl From<StellarError> for StellarAidError {
fn from(err: StellarError) -> Self {
match &err {
StellarError::FriendbotNotAvailable { .. } => Self::NetworkError(err.to_string()),
StellarError::HttpRequestFailed(_) => Self::NetworkError(err.to_string()),
StellarError::FriendbotError { .. } => Self::HorizonError(err.to_string()),
}
}
}

impl From<TokenSetupError> for StellarAidError {
fn from(err: TokenSetupError) -> Self {
Self::ContractError(err.to_string())
}
}

impl From<reqwest::Error> for StellarAidError {
fn from(err: reqwest::Error) -> Self {
Self::NetworkError(err.to_string())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::friendbot::utils::types::StellarError;
use crate::setup::token_setup::TokenSetupError;
use crate::utils::keypair::KeyError;

#[test]
fn key_error_converts_to_keypair_variant() {
let err: StellarAidError = KeyError::InvalidSecretKey("bad".into()).into();
assert!(matches!(err, StellarAidError::KeypairError(_)));
assert!(err.to_string().contains("bad"));
}

#[test]
fn stellar_friendbot_not_available_converts_to_network() {
let err: StellarAidError = StellarError::FriendbotNotAvailable {
network: "mainnet".into(),
}
.into();
assert!(matches!(err, StellarAidError::NetworkError(_)));
}

#[test]
fn stellar_http_failure_converts_to_network() {
let err: StellarAidError = StellarError::HttpRequestFailed("timeout".into()).into();
assert!(matches!(err, StellarAidError::NetworkError(_)));
}

#[test]
fn stellar_friendbot_error_converts_to_horizon() {
let err: StellarAidError = StellarError::FriendbotError {
status: 500,
body: "internal".into(),
}
.into();
assert!(matches!(err, StellarAidError::HorizonError(_)));
}

#[test]
fn token_setup_error_converts_to_contract() {
let err: StellarAidError = TokenSetupError::CommandFailed {
command: "stellar deploy".into(),
stderr: "oops".into(),
}
.into();
assert!(matches!(err, StellarAidError::ContractError(_)));
}

#[test]
fn display_includes_variant_prefix() {
let err = StellarAidError::ValidationError("amount must be positive".into());
assert_eq!(err.to_string(), "Validation error: amount must be positive");
}

#[test]
fn soroban_error_formats_code_and_message() {
let err = StellarAidError::SorobanError {
code: -32600,
message: "invalid request".into(),
};
assert!(err.to_string().contains("-32600"));
assert!(err.to_string().contains("invalid request"));
}
}
4 changes: 3 additions & 1 deletion src/friendbot/utils/friendbot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ pub fn fund_account(public_key: &str) -> Result<(), StellarError> {
return Err(StellarError::FriendbotError { status: 400, body });
}

let body = response.text().unwrap_or_default();
let body = response
.text()
.map_err(|e: reqwest::Error| StellarError::HttpRequestFailed(e.to_string()))?;
Err(StellarError::FriendbotError {
status: status.as_u16(),
body,
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod errors;

pub mod friendbot;
pub mod horizon;
mod setup;
Expand Down