use crate::cli::output::{field, newline};
use actix_cors::Cors;
use actix_web::{
dev::Server, get, http, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder,
};
const DEFAULT_HOST: &str = "0.0.0.0";
const BANNER_ART: &str = concat!(
"\n",
r" ____ __ _ __ __
/ __/__ / / ___ _______ / |/ /__ / /_
_\ \/ _ \/ _ \/ -_) __/ -_) / -_) __/
/___/ .__/_//_/\__/_/ \__/_/|_/\__/\__/
/_/ a d m i n a p i"
);
#[derive(Clone)]
struct ApiState {
rpc_url: String,
}
pub fn server(rpc_url: String, port: u16) -> Server {
let state = ApiState { rpc_url };
HttpServer::new(move || {
let cors = Cors::default()
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(3600);
App::new()
.wrap(cors)
.app_data(web::Data::new(state.clone()))
.service(index)
.service(health)
.service(monetary_policy)
.service(validator_whitelist)
.service(program_whitelist)
.service(balance)
.service(epoch)
.service(vote_account)
.service(stake_account)
.service(airdrop)
})
.bind(format!("{}:{}", DEFAULT_HOST, port))
.expect("unable to bind server")
.run()
}
pub fn serve(rpc_url: &str, port: u16) -> eyre::Result<()> {
let mut banner = String::from(BANNER_ART);
banner.push_str(newline());
banner.push_str(newline());
banner.push_str(&field(
"Listening",
format!("http://{}:{}", DEFAULT_HOST, port),
));
banner.push_str(&field("RPC", rpc_url));
banner.push_str(newline());
banner.push_str("Endpoints:");
banner.push_str(newline());
let ep = |sig: &str, desc: &str| format!(" {:<24}{}\n", sig, desc);
banner.push_str(&ep("GET /", "index"));
banner.push_str(&ep("GET /health", "health check"));
banner.push_str(&ep("GET /mp", "monetary policy"));
banner.push_str(&ep("GET /vw", "validator whitelist"));
banner.push_str(&ep("GET /pw", "program whitelist"));
banner.push_str(&ep("GET /epoch", "current epoch"));
banner.push_str(&ep("GET /balance/{pubkey}", "native SPHR balance"));
banner.push_str(&ep("GET /vote/{pubkey}", "vote account"));
banner.push_str(&ep("GET /stake/{pubkey}", "stake account"));
banner.push_str(&ep(
"POST /airdrop/{pubkey}",
"request airdrop (testnet, ?amount=)",
));
banner.push_str(newline());
banner.push_str("Append ?pretty for pretty-printed JSON.");
println!("{}", banner);
let rpc_url = rpc_url.to_string();
actix_web::rt::System::new()
.block_on(async move { server(rpc_url, port).await })
.map_err(|e| eyre::eyre!("server error: {}", e))?;
Ok(())
}
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Ok().body("spherenet-admin API")
}
#[get("/health")]
async fn health() -> impl Responder {
HttpResponse::Ok()
}
#[get("/mp")]
async fn monetary_policy(req: HttpRequest, state: web::Data<ApiState>) -> impl Responder {
let rpc_url = state.rpc_url.clone();
respond(
&req,
web::block(move || crate::mp::show::fetch(&rpc_url)).await,
)
}
#[get("/vw")]
async fn validator_whitelist(req: HttpRequest, state: web::Data<ApiState>) -> impl Responder {
let rpc_url = state.rpc_url.clone();
respond(
&req,
web::block(move || crate::vw::show::fetch(&rpc_url)).await,
)
}
#[get("/pw")]
async fn program_whitelist(req: HttpRequest, state: web::Data<ApiState>) -> impl Responder {
let rpc_url = state.rpc_url.clone();
respond(
&req,
web::block(move || crate::pw::show::fetch(&rpc_url)).await,
)
}
#[get("/balance/{pubkey}")]
async fn balance(
req: HttpRequest,
state: web::Data<ApiState>,
pubkey: web::Path<String>,
) -> impl Responder {
let rpc_url = state.rpc_url.clone();
let pubkey = pubkey.into_inner();
respond(
&req,
web::block(move || crate::utils::show::fetch_balance(&rpc_url, pubkey)).await,
)
}
#[get("/epoch")]
async fn epoch(req: HttpRequest, state: web::Data<ApiState>) -> impl Responder {
let rpc_url = state.rpc_url.clone();
respond(
&req,
web::block(move || crate::utils::show::fetch_epoch(&rpc_url)).await,
)
}
#[get("/vote/{pubkey}")]
async fn vote_account(
req: HttpRequest,
state: web::Data<ApiState>,
pubkey: web::Path<String>,
) -> impl Responder {
let rpc_url = state.rpc_url.clone();
let pubkey = pubkey.into_inner();
respond_opt(
&req,
web::block(move || crate::vote::show::fetch(&rpc_url, &pubkey)).await,
)
}
#[get("/stake/{pubkey}")]
async fn stake_account(
req: HttpRequest,
state: web::Data<ApiState>,
pubkey: web::Path<String>,
) -> impl Responder {
let rpc_url = state.rpc_url.clone();
let pubkey = pubkey.into_inner();
respond_opt(
&req,
web::block(move || crate::stake::show::fetch(&rpc_url, &pubkey)).await,
)
}
#[derive(serde::Deserialize)]
struct AirdropParams {
amount: Option<f64>,
}
#[post("/airdrop/{pubkey}")]
async fn airdrop(
req: HttpRequest,
state: web::Data<ApiState>,
pubkey: web::Path<String>,
params: web::Query<AirdropParams>,
) -> impl Responder {
let rpc_url = state.rpc_url.clone();
let pubkey = pubkey.into_inner();
let amount = params.amount.unwrap_or(1.0);
respond(
&req,
web::block(move || crate::utils::run::request_airdrop(&rpc_url, pubkey, amount)).await,
)
}
fn respond<T: serde::Serialize>(
req: &HttpRequest,
result: Result<eyre::Result<T>, actix_web::error::BlockingError>,
) -> HttpResponse {
match result {
Ok(Ok(view)) => json_response(&view, wants_pretty(req)),
Ok(Err(e)) => HttpResponse::InternalServerError().body(e.to_string()),
Err(e) => HttpResponse::InternalServerError().body(format!("blocking error: {e}")),
}
}
fn respond_opt<T: serde::Serialize>(
req: &HttpRequest,
result: Result<eyre::Result<Option<T>>, actix_web::error::BlockingError>,
) -> HttpResponse {
match result {
Ok(Ok(Some(view))) => json_response(&view, wants_pretty(req)),
Ok(Ok(None)) => HttpResponse::NotFound().json(serde_json::json!({ "found": false })),
Ok(Err(e)) => HttpResponse::InternalServerError().body(e.to_string()),
Err(e) => HttpResponse::InternalServerError().body(format!("blocking error: {e}")),
}
}
fn wants_pretty(req: &HttpRequest) -> bool {
req.query_string()
.split('&')
.any(|kv| kv.split('=').next() == Some("pretty"))
}
fn json_response<T: serde::Serialize>(value: &T, pretty: bool) -> HttpResponse {
if pretty {
match serde_json::to_string_pretty(value) {
Ok(body) => HttpResponse::Ok()
.content_type("application/json")
.body(body),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
}
} else {
HttpResponse::Ok().json(value)
}
}