1use reqwest::StatusCode;
2use serde::Deserialize;
3use serde::Serialize;
4use serde_json::json;
5use sui_sdk_types::Address;
6use sui_sdk_types::Digest;
7
8type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
9
10#[derive(Serialize, Deserialize, Debug, Clone)]
11#[serde(rename_all = "camelCase")]
12pub struct CoinInfo {
13 pub amount: u64,
14 pub id: Address,
15 pub transfer_tx_digest: Digest,
16}
17
18#[derive(Clone)]
19pub struct FaucetClient {
20 faucet_url: http::Uri,
21 inner: reqwest::Client,
22}
23
24impl FaucetClient {
25 pub const TESTNET: &str = "https://faucet.testnet.sui.io";
27 pub const DEVNET: &str = "https://faucet.devnet.sui.io";
29 pub const LOCAL: &str = "http://localhost:9123";
31
32 pub fn new<T>(faucet_url: T) -> Result<Self, BoxError>
36 where
37 T: TryInto<http::Uri>,
38 T::Error: Into<BoxError>,
39 {
40 let inner = reqwest::Client::new();
41 let faucet_url = faucet_url.try_into().map_err(Into::into)?;
42 Ok(FaucetClient { faucet_url, inner })
43 }
44
45 pub async fn request(&self, address: Address) -> Result<Vec<CoinInfo>, BoxError> {
48 const FAUCET_REQUEST_PATH: &str = "v2/gas";
49
50 let address = address.to_string();
51 let json_body = json![{
52 "FixedAmountRequest": {
53 "recipient": &address
54 }
55 }];
56
57 let url = format!("{}{}", self.faucet_url, FAUCET_REQUEST_PATH);
58 let resp = self
59 .inner
60 .post(url)
61 .header("content-type", "application/json")
62 .json(&json_body)
63 .send()
64 .await?;
65 match resp.status() {
66 StatusCode::ACCEPTED | StatusCode::CREATED | StatusCode::OK => {
67 let faucet_resp: FaucetResponse = resp.json().await?;
68
69 match faucet_resp.status {
70 RequestStatus::Success => {
71 Ok(faucet_resp.coins_sent.unwrap_or_default())
72 }
73 RequestStatus::Failure(err) => {
74 Err(format!("Faucet request was unsuccessful: {err}").into())
75 }
76 }
77 }
78 StatusCode::TOO_MANY_REQUESTS => {
79 Err("Faucet service received too many requests from this IP address. Please try again after 60 minutes.".into())
80 }
81 StatusCode::SERVICE_UNAVAILABLE => {
82 Err("Faucet service is currently overloaded or unavailable. Please try again later.".into())
83 }
84 status_code => {
85 Err(format!("Faucet request was unsuccessful: {status_code}").into())
86 }
87 }
88 }
89}
90
91#[derive(Deserialize)]
92enum RequestStatus {
93 Success,
94 Failure(FaucetError),
95}
96
97#[derive(Deserialize)]
98struct FaucetResponse {
99 status: RequestStatus,
100 coins_sent: Option<Vec<CoinInfo>>,
101}
102
103#[derive(Deserialize)]
104enum FaucetError {
105 MissingTurnstileTokenHeader,
106 TooManyRequests(String),
107 Internal(String),
108 InvalidUserAgent(String),
109}
110
111impl std::fmt::Display for FaucetError {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 FaucetError::MissingTurnstileTokenHeader => f.write_str(
115"Missing X-Turnstile-Token header. For testnet tokens, please use the Web UI: https://faucet.sui.io"
116 ),
117 FaucetError::TooManyRequests(s) => write!(f, "Request limiit exceeded: {s}"),
118 FaucetError::Internal(s) => write!(f, "Internal error: {s}"),
119 FaucetError::InvalidUserAgent(s) => write!(f, "Invalid user agent: {s}"),
120 }
121 }
122}