1use crate::types::*;
5use crate::{AppState, FaucetConfig, FaucetError, FaucetRequest};
6use axum::{
7 BoxError, Extension, Json, Router,
8 error_handling::HandleErrorLayer,
9 http::StatusCode,
10 response::IntoResponse,
11 routing::{get, post},
12};
13use http::Method;
14use std::{
15 borrow::Cow,
16 net::{IpAddr, SocketAddr},
17 path::PathBuf,
18 sync::Arc,
19 time::Duration,
20};
21use sui_config::SUI_CLIENT_CONFIG;
22use sui_sdk::wallet_context::WalletContext;
23use tower::ServiceBuilder;
24use tower_http::cors::{Any, CorsLayer};
25use tracing::info;
26
27async fn health() -> &'static str {
29 "OK"
30}
31
32async fn request_local_gas(
33 Extension(state): Extension<Arc<AppState>>,
34 Json(payload): Json<FaucetRequest>,
35 ) -> impl IntoResponse {
37 let FaucetRequest::FixedAmountRequest(request) = payload;
38 info!("Local request for address: {}", request.recipient);
39 let request = state
40 .faucet
41 .local_request_execute_tx(request.recipient)
42 .await;
43
44 if let Err(e) = request {
45 return (
46 StatusCode::INTERNAL_SERVER_ERROR,
47 Json(FaucetResponse {
48 status: RequestStatus::Failure(e),
49 coins_sent: None,
50 }),
51 );
52 }
53
54 let Ok(coins) = request else {
55 return (
56 StatusCode::INTERNAL_SERVER_ERROR,
57 Json(FaucetResponse {
58 status: RequestStatus::Failure(FaucetError::internal(format!(
59 "Failed to execute transaction: {}",
60 request.unwrap_err()
61 ))),
62 coins_sent: None,
63 }),
64 );
65 };
66
67 (
68 StatusCode::OK,
69 Json(FaucetResponse {
70 status: RequestStatus::Success,
71 coins_sent: Some(coins),
72 }),
73 )
74}
75
76pub fn create_wallet_context(
77 timeout_secs: u64,
78 config_dir: PathBuf,
79) -> Result<WalletContext, anyhow::Error> {
80 let wallet_conf = config_dir.join(SUI_CLIENT_CONFIG);
81 info!("Initialize wallet from config path: {:?}", wallet_conf);
82 WalletContext::new(&wallet_conf).map(|ctx| {
83 ctx.with_request_timeout(Duration::from_secs(timeout_secs))
84 .with_max_concurrent_requests(1000)
85 })
86}
87
88async fn handle_error(error: BoxError) -> impl IntoResponse {
89 if error.is::<tower::load_shed::error::Overloaded>() {
90 return (
91 StatusCode::SERVICE_UNAVAILABLE,
92 Cow::from("service is overloaded, please try again later"),
93 );
94 }
95
96 (
97 StatusCode::INTERNAL_SERVER_ERROR,
98 Cow::from(format!("Unhandled internal error: {}", error)),
99 )
100}
101
102pub async fn start_faucet(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
105 let cors = CorsLayer::new()
106 .allow_methods(vec![Method::GET, Method::POST])
107 .allow_headers(Any)
108 .allow_origin(Any);
109 let FaucetConfig { port, host_ip, .. } = app_state.config;
110
111 info!("Starting faucet in local mode");
112 let app = Router::new()
113 .route("/", get(health))
114 .route("/v2/gas", post(request_local_gas))
115 .route("/v1/gas", post(request_local_gas))
116 .route("/gas", post(request_local_gas))
117 .layer(
118 ServiceBuilder::new()
119 .layer(HandleErrorLayer::new(handle_error))
120 .load_shed()
121 .layer(Extension(app_state.clone()))
122 .layer(cors)
123 .into_inner(),
124 );
125
126 let addr = SocketAddr::new(IpAddr::V4(host_ip), port);
127 info!("listening on {}", addr);
128 let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
129 axum::serve(
130 listener,
131 app.into_make_service_with_connect_info::<SocketAddr>(),
132 )
133 .await?;
134
135 Ok(())
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::LocalFaucet;
142 use serde_json::json;
143 use sui_sdk::types::base_types::SuiAddress;
144 use test_cluster::TestClusterBuilder;
145
146 #[tokio::test]
147 async fn test_v2_gas_endpoint() {
148 let cluster = TestClusterBuilder::new().build().await;
150 let port = 9090;
151 let config = FaucetConfig {
152 host_ip: "127.0.0.1".parse().unwrap(),
153 port,
154 ..Default::default()
155 };
156 let local_faucet = LocalFaucet::new(cluster.wallet, config.clone())
157 .await
158 .unwrap();
159
160 let app_state = Arc::new(AppState {
161 faucet: local_faucet,
162 config,
163 });
164
165 let handle = tokio::spawn(async move {
167 start_faucet(app_state)
168 .await
169 .expect("Failed to start faucet");
170 });
171
172 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
174
175 let client = reqwest::Client::new();
176
177 let recipient = SuiAddress::random_for_testing_only();
179 let req = FaucetRequest::new_fixed_amount_request(recipient);
180 let response = client
181 .post(format!("http://127.0.0.1:{port}/v2/gas",))
182 .json(&req)
183 .send()
184 .await
185 .unwrap();
186
187 assert_eq!(response.status(), StatusCode::OK);
188 let faucet_response = response.json::<FaucetResponse>().await.unwrap();
189
190 assert!(faucet_response.coins_sent.is_some());
192
193 let response = client
195 .post(format!("http://127.0.0.1:{port}/v2/gas",))
196 .json(&json!({
197 "recipient": recipient.to_string(),
198 }))
199 .send()
200 .await
201 .unwrap();
202
203 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
204 handle.abort();
205 }
206}