Skip to content

Commit fbee386

Browse files
authored
Merge pull request #23 from JamesVictor-O/feature/error-handling-custom-types
feat: add unified error handling and custom error types
2 parents 6f22a6c + ae184f1 commit fbee386

5 files changed

Lines changed: 165 additions & 6 deletions

File tree

contracts/campaign/src/lib.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,20 @@ impl CampaignContract {
1818
/// Get campaign status
1919
pub fn get_status(env: Env) -> (Symbol, Symbol, i128, u64) {
2020
let key = Symbol::new(&env, "campaign_data");
21-
env.storage().instance().get(&key).unwrap()
21+
env.storage()
22+
.instance()
23+
.get(&key)
24+
.expect("campaign not initialized")
2225
}
2326

2427
/// Check if campaign is active
2528
pub fn is_active(env: Env) -> bool {
2629
let key = Symbol::new(&env, "campaign_data");
27-
let (_id, _title, _target, deadline): (Symbol, Symbol, i128, u64) =
28-
env.storage().instance().get(&key).unwrap();
30+
let (_id, _title, _target, deadline): (Symbol, Symbol, i128, u64) = env
31+
.storage()
32+
.instance()
33+
.get(&key)
34+
.expect("campaign not initialized");
2935

3036
let current_time = env.ledger().timestamp();
3137
current_time < deadline

contracts/withdrawal/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ impl WithdrawalContract {
1818
/// Withdraw funds from the contract
1919
pub fn withdraw(env: Env, amount: i128) -> bool {
2020
let key = Symbol::new(&env, "settings");
21-
let (beneficiary, max_withdrawal): (Address, i128) =
22-
env.storage().instance().get(&key).unwrap();
21+
let (beneficiary, max_withdrawal): (Address, i128) = env
22+
.storage()
23+
.instance()
24+
.get(&key)
25+
.expect("withdrawal not initialized");
2326

2427
beneficiary.require_auth();
2528

src/errors.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//! Unified error types for the StellarAid blockchain integration layer.
2+
//!
3+
//! [`StellarAidError`] is the top-level error enum that downstream code should
4+
//! use as its return type. Each variant maps to a distinct failure domain and
5+
//! carries enough context for callers to decide how to recover (or surface a
6+
//! useful message).
7+
//!
8+
//! Existing per-module error types ([`KeyError`](crate::utils::keypair::KeyError),
9+
//! [`StellarError`](crate::friendbot::utils::types::StellarError),
10+
//! [`TokenSetupError`](crate::setup::token_setup::TokenSetupError)) are
11+
//! automatically converted via [`From`] impls so the `?` operator works
12+
//! transparently.
13+
14+
use thiserror::Error;
15+
16+
use crate::friendbot::utils::types::StellarError;
17+
use crate::setup::token_setup::TokenSetupError;
18+
use crate::utils::keypair::KeyError;
19+
20+
/// Top-level error type for the StellarAid integration layer.
21+
#[derive(Debug, Error)]
22+
pub enum StellarAidError {
23+
/// Stellar Horizon REST-API returned an error or an unexpected response.
24+
#[error("Horizon API error: {0}")]
25+
HorizonError(String),
26+
27+
/// Soroban JSON-RPC call failed.
28+
#[error("Soroban RPC error (code {code}): {message}")]
29+
SorobanError { code: i64, message: String },
30+
31+
/// Key generation, parsing, or derivation failed.
32+
#[error("Keypair error: {0}")]
33+
KeypairError(String),
34+
35+
/// Input or state did not meet a business-logic precondition.
36+
#[error("Validation error: {0}")]
37+
ValidationError(String),
38+
39+
/// A submitted transaction was rejected or reverted by the network.
40+
#[error("Transaction failed: {0}")]
41+
TransactionFailed(String),
42+
43+
/// An on-chain smart-contract call returned an error.
44+
#[error("Contract error: {0}")]
45+
ContractError(String),
46+
47+
/// A lower-level network / HTTP / I/O error.
48+
#[error("Network error: {0}")]
49+
NetworkError(String),
50+
}
51+
52+
// ── From impls for ergonomic `?` propagation ────────────────────────────────
53+
54+
impl From<KeyError> for StellarAidError {
55+
fn from(err: KeyError) -> Self {
56+
Self::KeypairError(err.to_string())
57+
}
58+
}
59+
60+
impl From<StellarError> for StellarAidError {
61+
fn from(err: StellarError) -> Self {
62+
match &err {
63+
StellarError::FriendbotNotAvailable { .. } => Self::NetworkError(err.to_string()),
64+
StellarError::HttpRequestFailed(_) => Self::NetworkError(err.to_string()),
65+
StellarError::FriendbotError { .. } => Self::HorizonError(err.to_string()),
66+
}
67+
}
68+
}
69+
70+
impl From<TokenSetupError> for StellarAidError {
71+
fn from(err: TokenSetupError) -> Self {
72+
Self::ContractError(err.to_string())
73+
}
74+
}
75+
76+
impl From<reqwest::Error> for StellarAidError {
77+
fn from(err: reqwest::Error) -> Self {
78+
Self::NetworkError(err.to_string())
79+
}
80+
}
81+
82+
#[cfg(test)]
83+
mod tests {
84+
use super::*;
85+
use crate::friendbot::utils::types::StellarError;
86+
use crate::setup::token_setup::TokenSetupError;
87+
use crate::utils::keypair::KeyError;
88+
89+
#[test]
90+
fn key_error_converts_to_keypair_variant() {
91+
let err: StellarAidError = KeyError::InvalidSecretKey("bad".into()).into();
92+
assert!(matches!(err, StellarAidError::KeypairError(_)));
93+
assert!(err.to_string().contains("bad"));
94+
}
95+
96+
#[test]
97+
fn stellar_friendbot_not_available_converts_to_network() {
98+
let err: StellarAidError = StellarError::FriendbotNotAvailable {
99+
network: "mainnet".into(),
100+
}
101+
.into();
102+
assert!(matches!(err, StellarAidError::NetworkError(_)));
103+
}
104+
105+
#[test]
106+
fn stellar_http_failure_converts_to_network() {
107+
let err: StellarAidError = StellarError::HttpRequestFailed("timeout".into()).into();
108+
assert!(matches!(err, StellarAidError::NetworkError(_)));
109+
}
110+
111+
#[test]
112+
fn stellar_friendbot_error_converts_to_horizon() {
113+
let err: StellarAidError = StellarError::FriendbotError {
114+
status: 500,
115+
body: "internal".into(),
116+
}
117+
.into();
118+
assert!(matches!(err, StellarAidError::HorizonError(_)));
119+
}
120+
121+
#[test]
122+
fn token_setup_error_converts_to_contract() {
123+
let err: StellarAidError = TokenSetupError::CommandFailed {
124+
command: "stellar deploy".into(),
125+
stderr: "oops".into(),
126+
}
127+
.into();
128+
assert!(matches!(err, StellarAidError::ContractError(_)));
129+
}
130+
131+
#[test]
132+
fn display_includes_variant_prefix() {
133+
let err = StellarAidError::ValidationError("amount must be positive".into());
134+
assert_eq!(err.to_string(), "Validation error: amount must be positive");
135+
}
136+
137+
#[test]
138+
fn soroban_error_formats_code_and_message() {
139+
let err = StellarAidError::SorobanError {
140+
code: -32600,
141+
message: "invalid request".into(),
142+
};
143+
assert!(err.to_string().contains("-32600"));
144+
assert!(err.to_string().contains("invalid request"));
145+
}
146+
}

src/friendbot/utils/friendbot.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ pub fn fund_account(public_key: &str) -> Result<(), StellarError> {
7171
return Err(StellarError::FriendbotError { status: 400, body });
7272
}
7373

74-
let body = response.text().unwrap_or_default();
74+
let body = response
75+
.text()
76+
.map_err(|e: reqwest::Error| StellarError::HttpRequestFailed(e.to_string()))?;
7577
Err(StellarError::FriendbotError {
7678
status: status.as_u16(),
7779
body,

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
pub mod errors;
2+
13
pub mod friendbot;
24
pub mod horizon;
35
mod setup;

0 commit comments

Comments
 (0)