iota_sdk_graphql_client/
faucet.rs1use std::time::Duration;
6
7use eyre::{bail, eyre};
8use iota_types::{Address, Digest, ObjectId};
9use reqwest::{StatusCode, Url};
10use serde::{Deserialize, Serialize};
11use serde_json::json;
12use tracing::{error, info};
13
14pub const FAUCET_DEVNET_HOST: &str = "https://faucet.devnet.iota.cafe";
15pub const FAUCET_TESTNET_HOST: &str = "https://faucet.testnet.iota.cafe";
16pub const FAUCET_LOCAL_HOST: &str = "http://localhost:9123";
17
18const FAUCET_REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
19const FAUCET_POLL_INTERVAL: Duration = Duration::from_secs(2);
20
21pub struct FaucetClient {
22 faucet_url: Url,
23 inner: reqwest::Client,
24}
25
26#[derive(serde::Deserialize)]
27struct FaucetResponse {
28 task: Option<String>,
29 error: Option<String>,
30}
31
32#[derive(Serialize, Deserialize, Debug, Clone)]
33#[serde(rename_all = "camelCase")]
34struct BatchStatusFaucetResponse {
35 pub status: Option<BatchSendStatus>,
36 pub error: Option<String>,
37}
38
39#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
40#[serde(rename_all = "UPPERCASE")]
41pub enum BatchSendStatusType {
42 InProgress,
43 Succeeded,
44 Discarded,
45}
46
47#[derive(Serialize, Deserialize, Debug, Clone)]
48pub struct BatchSendStatus {
49 pub status: BatchSendStatusType,
50 pub transferred_gas_objects: Option<FaucetReceipt>,
51}
52
53#[derive(Serialize, Deserialize, Debug, Clone)]
54pub struct FaucetReceipt {
55 pub sent: Vec<CoinInfo>,
56}
57
58#[derive(Serialize, Deserialize, Debug, Clone)]
59#[serde(rename_all = "camelCase")]
60pub struct CoinInfo {
61 pub amount: u64,
62 pub id: ObjectId,
63 pub transfer_tx_digest: Digest,
64}
65
66impl FaucetClient {
67 pub fn new(faucet_url: &str) -> Self {
75 let inner = reqwest::Client::new();
76 let faucet_url = Url::parse(faucet_url).expect("Invalid faucet URL");
77 FaucetClient { faucet_url, inner }
78 }
79
80 pub fn new_testnet() -> Self {
82 Self::new(FAUCET_TESTNET_HOST)
83 }
84
85 pub fn new_devnet() -> Self {
87 Self::new(FAUCET_DEVNET_HOST)
88 }
89
90 pub fn new_localnet() -> Self {
92 Self::new(FAUCET_LOCAL_HOST)
93 }
94
95 pub async fn request(&self, address: Address) -> eyre::Result<Option<String>> {
99 self.request_impl(address).await
100 }
101
102 async fn request_impl(&self, address: Address) -> eyre::Result<Option<String>> {
105 let address = address.to_string();
106 let json_body = json![{
107 "FixedAmountRequest": {
108 "recipient": &address
109 }
110 }];
111 let url = format!("{}v1/gas", self.faucet_url);
112 info!(
113 "Requesting gas from faucet for address {} : {}",
114 address, url
115 );
116 let resp = self
117 .inner
118 .post(url)
119 .header("content-type", "application/json")
120 .json(&json_body)
121 .send()
122 .await?;
123 match resp.status() {
124 StatusCode::ACCEPTED | StatusCode::CREATED => {
125 let faucet_resp: FaucetResponse = resp.json().await?;
126
127 if let Some(err) = faucet_resp.error {
128 error!("Faucet request was unsuccessful: {err}");
129 bail!("Faucet request was unsuccessful: {err}")
130 } else {
131 info!("Request successful: {:?}", faucet_resp.task);
132 Ok(faucet_resp.task)
133 }
134 }
135 StatusCode::TOO_MANY_REQUESTS => {
136 error!("Faucet service received too many requests from this IP address.");
137 bail!(
138 "Faucet service received too many requests from this IP address. Please try again after 60 minutes."
139 );
140 }
141 StatusCode::SERVICE_UNAVAILABLE => {
142 error!("Faucet service is currently overloaded or unavailable.");
143 bail!(
144 "Faucet service is currently overloaded or unavailable. Please try again later."
145 );
146 }
147 status_code => {
148 error!("Faucet request was unsuccessful: {status_code}");
149 bail!("Faucet request was unsuccessful: {status_code}");
150 }
151 }
152 }
153
154 pub async fn request_and_wait(&self, address: Address) -> eyre::Result<Option<FaucetReceipt>> {
162 let request_id = self.request(address).await?;
163 if let Some(request_id) = request_id {
164 let poll_response = tokio::time::timeout(FAUCET_REQUEST_TIMEOUT, async {
165 let mut interval = tokio::time::interval(FAUCET_POLL_INTERVAL);
166 loop {
167 interval.tick().await;
168 info!("Polling faucet request status: {request_id}");
169 let req = self.request_status(request_id.clone()).await;
170
171 if let Ok(Some(poll_response)) = req {
172 match poll_response.status {
173 BatchSendStatusType::Succeeded => {
174 info!("Faucet request {request_id} succeeded");
175 break Ok(poll_response);
176 }
177 BatchSendStatusType::Discarded => {
178 break Ok(BatchSendStatus {
179 status: BatchSendStatusType::Discarded,
180 transferred_gas_objects: None,
181 });
182 }
183 BatchSendStatusType::InProgress => {
184 continue;
185 }
186 }
187 } else if let Some(err) = req.err() {
188 error!("Faucet request {request_id} failed. Error: {:?}", err);
189 break Err(eyre!(
190 "Faucet request {request_id} failed. Error: {:?}",
191 err
192 ));
193 }
194 }
195 })
196 .await
197 .map_err(|_| {
198 error!(
199 "Faucet request {request_id} timed out. Timeout set to {} seconds",
200 FAUCET_REQUEST_TIMEOUT.as_secs()
201 );
202 eyre!("Faucet request timed out")
203 })??;
204 Ok(poll_response.transferred_gas_objects)
205 } else {
206 Ok(None)
207 }
208 }
209
210 pub async fn request_status(&self, id: String) -> eyre::Result<Option<BatchSendStatus>> {
214 let status_url = format!("{}v1/status/{}", self.faucet_url, id);
215 info!("Checking status of faucet request: {status_url}");
216 let response = self.inner.get(&status_url).send().await?;
217 if response.status() == StatusCode::TOO_MANY_REQUESTS {
218 bail!("Cannot fetch request status due to too many requests from this IP address.");
219 } else if response.status() == StatusCode::BAD_GATEWAY {
220 bail!("Cannot fetch request status due to a bad gateway.")
221 }
222 let json = response
223 .json::<BatchStatusFaucetResponse>()
224 .await
225 .map_err(|e| {
226 error!("Failed to parse faucet response: {:?}", e);
227 eyre!("Failed to parse faucet response: {:?}", e)
228 })?;
229 Ok(json.status)
230 }
231}