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