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