sui_graphql_client/
faucet.rs1use 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 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 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 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 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 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}