sui_graphql_client/
faucet.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use sui_types::Address;
5use sui_types::Digest;
6
7use anyhow::bail;
8use reqwest::StatusCode;
9use reqwest::Url;
10use serde::Deserialize;
11use serde::Serialize;
12use serde_json::json;
13use thiserror::Error;
14use tracing::error as tracing_error;
15use tracing::info;
16
17pub const FAUCET_DEVNET_HOST: &str = "https://faucet.devnet.sui.io";
18pub const FAUCET_TESTNET_HOST: &str = "https://faucet.testnet.sui.io";
19pub const FAUCET_LOCAL_HOST: &str = "http://localhost:9123";
20
21pub const FAUCET_REQUEST_PATH: &str = "v2/gas";
22
23pub struct FaucetClient {
24    faucet_url: Url,
25    inner: reqwest::Client,
26}
27
28#[derive(Serialize, Deserialize, Debug)]
29pub enum RequestStatus {
30    Success,
31    Failure(FaucetError),
32}
33
34#[derive(Serialize, Deserialize, Debug)]
35pub struct FaucetResponse {
36    pub status: RequestStatus,
37    pub coins_sent: Option<Vec<CoinInfo>>,
38}
39
40#[derive(Serialize, Deserialize, Debug, Clone)]
41#[serde(rename_all = "camelCase")]
42pub struct CoinInfo {
43    pub amount: u64,
44    pub id: Address,
45    pub transfer_tx_digest: Digest,
46}
47
48#[derive(Serialize, Deserialize, Error, Debug, PartialEq, Eq)]
49pub enum FaucetError {
50    #[error("Missing X-Turnstile-Token header. For testnet tokens, please use the Web UI: https://faucet.sui.io")]
51    MissingTurnstileTokenHeader,
52
53    #[error("Request limit exceeded. {0}")]
54    TooManyRequests(String),
55
56    #[error("Internal error: {0}")]
57    Internal(String),
58
59    #[error("Invalid user agent: {0}")]
60    InvalidUserAgent(String),
61}
62
63impl FaucetClient {
64    /// Construct a new `FaucetClient` with the given faucet service URL. This [`FaucetClient`]
65    /// expects that the service provides this endpoint: /v2/gas. As such, do not
66    /// provide the request endpoint, just the top level service endpoint.
67    pub fn new(faucet_url: &str) -> Self {
68        let inner = reqwest::Client::new();
69        let faucet_url = Url::parse(faucet_url).expect("Invalid faucet URL");
70        FaucetClient { faucet_url, inner }
71    }
72
73    /// Set to local faucet.
74    pub fn local() -> Self {
75        Self {
76            faucet_url: Url::parse(FAUCET_LOCAL_HOST).expect("Invalid faucet URL"),
77            inner: reqwest::Client::new(),
78        }
79    }
80
81    /// Set to devnet faucet.
82    pub fn devnet() -> Self {
83        Self {
84            faucet_url: Url::parse(FAUCET_DEVNET_HOST).expect("Invalid faucet URL"),
85            inner: reqwest::Client::new(),
86        }
87    }
88
89    /// Set to testnet faucet.
90    pub fn testnet() -> Self {
91        Self {
92            faucet_url: Url::parse(FAUCET_TESTNET_HOST).expect("Invalid faucet URL"),
93            inner: reqwest::Client::new(),
94        }
95    }
96
97    /// Make a faucet request. It returns a [`FaucetResponse`] type, which upon success contains
98    /// the information about the coin sent.
99    pub async fn request(&self, address: Address) -> Result<FaucetResponse, anyhow::Error> {
100        let address = address.to_string();
101        let json_body = json![{
102            "FixedAmountRequest": {
103                "recipient": &address
104            }
105        }];
106
107        let url = format!("{}{}", self.faucet_url, FAUCET_REQUEST_PATH);
108        info!(
109            "Requesting gas from faucet for address {} : {}",
110            address, url
111        );
112        let resp = self
113            .inner
114            .post(url)
115            .header("content-type", "application/json")
116            .json(&json_body)
117            .send()
118            .await?;
119        match resp.status() {
120            StatusCode::ACCEPTED | StatusCode::CREATED | StatusCode::OK => {
121                let faucet_resp: FaucetResponse = resp.json().await?;
122
123                match faucet_resp.status {
124                    RequestStatus::Success => {
125                        info!("Faucet request was successful: {:?}", faucet_resp);
126                        Ok(faucet_resp)
127                    }
128                    RequestStatus::Failure(err) => {
129                        tracing_error!("Faucet request was unsuccessful: {:?}", err);
130                        bail!("Faucet request was unsuccessful: {:?}", err)
131                    }
132                }
133            }
134            StatusCode::TOO_MANY_REQUESTS => {
135                tracing_error!("Faucet service received too many requests from this IP address.");
136                bail!("Faucet service received too many requests from this IP address. Please try again after 60 minutes.");
137            }
138            StatusCode::SERVICE_UNAVAILABLE => {
139                tracing_error!("Faucet service is currently overloaded or unavailable.");
140                bail!("Faucet service is currently overloaded or unavailable. Please try again later.");
141            }
142            status_code => {
143                tracing_error!("Faucet request was unsuccessful: {status_code}");
144                bail!("Faucet request was unsuccessful: {status_code}");
145            }
146        }
147    }
148}