#![warn(missing_debug_implementations)]
#![warn(rust_2018_idioms)]
mod client;
pub mod api;
pub mod error;
pub mod types;
use std::sync::Arc;
use crate::api::{
alpha::Alpha, coordination_alerts::CoordinationAlerts, deployer::Deployer,
first_touch_subscriptions::FirstTouchSubscriptions, kol::Kol, me::Me,
price_alerts::PriceAlerts, signals::Signals, sniper::Sniper, stream::Stream, token::Token,
tools::Tools, wallet::Wallet, wallet_tracker::WalletTracker, webhooks::Webhooks,
};
use crate::client::HttpCore;
use crate::error::{MadeOnSolError, Result};
pub use crate::error::MadeOnSolError as Error;
#[derive(Debug, Clone)]
pub struct MadeOnSol {
pub kol: Kol,
pub deployer: Deployer,
pub alpha: Alpha,
pub token: Token,
pub me: Me,
pub wallet_tracker: WalletTracker,
pub wallet: Wallet,
pub coordination_alerts: CoordinationAlerts,
pub first_touch_subscriptions: FirstTouchSubscriptions,
pub price_alerts: PriceAlerts,
pub signals: Signals,
pub sniper: Sniper,
pub tools: Tools,
pub stream: Stream,
pub webhooks: Webhooks,
}
impl MadeOnSol {
pub fn new(api_key: impl Into<String>) -> Result<Self> {
let api_key = api_key.into();
if !api_key.starts_with("msk_") {
eprintln!(
"\n[madeonsol] Missing or invalid API key.\n\
→ Get a free key (200 req/day, no card) at https://madeonsol.com/pricing\n\
→ Then: madeonsol::MadeOnSol::new(std::env::var(\"MADEONSOL_API_KEY\")?)?\n"
);
return Err(MadeOnSolError::MissingApiKey);
}
let core = Arc::new(HttpCore::new(api_key));
Ok(Self {
kol: Kol { core: Arc::clone(&core) },
deployer: Deployer { core: Arc::clone(&core) },
alpha: Alpha { core: Arc::clone(&core) },
token: Token { core: Arc::clone(&core) },
me: Me { core: Arc::clone(&core) },
wallet_tracker: WalletTracker { core: Arc::clone(&core) },
wallet: Wallet { core: Arc::clone(&core) },
coordination_alerts: CoordinationAlerts { core: Arc::clone(&core) },
first_touch_subscriptions: FirstTouchSubscriptions { core: Arc::clone(&core) },
price_alerts: PriceAlerts { core: Arc::clone(&core) },
signals: Signals { core: Arc::clone(&core) },
sniper: Sniper { core: Arc::clone(&core) },
tools: Tools { core: Arc::clone(&core) },
stream: Stream { core: Arc::clone(&core) },
webhooks: Webhooks { core },
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_missing_api_key() {
let err = MadeOnSol::new("").unwrap_err();
assert!(matches!(err, MadeOnSolError::MissingApiKey));
}
#[test]
fn rejects_wrong_prefix() {
let err = MadeOnSol::new("sk_live_abc").unwrap_err();
assert!(matches!(err, MadeOnSolError::MissingApiKey));
}
#[test]
fn accepts_valid_prefix() {
let client = MadeOnSol::new("msk_test_abcdef").unwrap();
let _cloned = client.clone();
}
#[test]
fn stream_token_deserializes_null_expiry() {
let t: crate::types::StreamToken = serde_json::from_str(
r#"{"token":"abc","expires_at":null,"next_refresh_at":null,"rotated":false,
"lifetime":"This token does not expire.",
"ws_url":"wss://madeonsol.com/ws/v1/stream","usage":"connect"}"#,
)
.unwrap();
assert_eq!(t.token, "abc");
assert!(t.expires_at.is_none());
assert!(t.next_refresh_at.is_none());
assert_eq!(t.rotated, Some(false));
assert!(t.lifetime.is_some());
assert!(t.dex_ws_url.is_none());
let old: crate::types::StreamToken = serde_json::from_str(
r#"{"token":"abc","expires_at":"2026-08-28T00:00:00Z",
"ws_url":"wss://madeonsol.com/ws/v1/stream","usage":"connect"}"#,
)
.unwrap();
assert_eq!(old.expires_at.as_deref(), Some("2026-08-28T00:00:00Z"));
assert!(old.rotated.is_none());
assert!(old.lifetime.is_none());
}
}