sui_rpc/
faucet.rs

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    /// URL for the testnet faucet.
26    pub const TESTNET: &str = "https://faucet.testnet.sui.io";
27    /// URL for the devnet faucet.
28    pub const DEVNET: &str = "https://faucet.devnet.sui.io";
29    /// URL for the local faucet.
30    pub const LOCAL: &str = "http://localhost:9123";
31
32    /// Construct a new `FaucetClient` with the given faucet service URL. This [`FaucetClient`]
33    /// expects that the service provides this endpoint: /v2/gas. As such, do not
34    /// provide the request endpoint, just the top level service endpoint.
35    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    /// Make a faucet request. It returns a list of [`CoinInfo`] type, which upon success contains
46    /// the information about the coins sent.
47    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}