iota_sdk_graphql_client/
faucet.rs1use std::{collections::HashSet, time::Duration};
6
7use iota_types::{Address, ObjectId, TransactionDigest};
8use reqwest::{StatusCode, Url};
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use tracing::{error, info};
12
13use crate::WaitForTransaction;
14
15pub const FAUCET_LOCAL_HOST: &str = "http://localhost:9123";
16
17const FAUCET_REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
18const FAUCET_POLL_INTERVAL: Duration = Duration::from_secs(2);
19
20#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum FaucetError {
23 #[error("Cannot fetch request status due to a bad gateway.")]
24 BadGateway,
25 #[error("Invalid faucet URL: {0}")]
26 InvalidUrl(#[from] url::ParseError),
27 #[error("Faucet request was unsuccessful: {0}")]
28 Request(String),
29 #[error("Reqwest error: {0}")]
30 Reqwest(#[from] reqwest::Error),
31 #[error("Faucet request was unsuccessful with status code {0}")]
32 StatusCode(StatusCode),
33 #[error("Faucet request timed out")]
34 TimedOut,
35 #[error(
36 "Faucet URL scheme `{0}` needs TLS: enable the `tls-ring` or `tls-aws-lc` feature, or pass \
37 your own client to `new_with_reqwest_client`"
38 )]
39 TlsUnavailable(String),
40 #[error(
41 "Faucet service received too many requests from this IP address. Please try again later."
42 )]
43 TooManyRequests,
44 #[error("Faucet service is currently overloaded or unavailable. Please try again later.")]
45 Unavailable,
46}
47
48pub struct FaucetClient {
49 faucet_url: Url,
50 inner: reqwest::Client,
51}
52
53#[derive(serde::Deserialize)]
54struct FaucetResponse {
55 task: Option<String>,
56 error: Option<String>,
57}
58
59#[derive(Clone, Debug, Deserialize, Serialize)]
60#[serde(rename_all = "camelCase")]
61struct BatchStatusFaucetResponse {
62 pub status: Option<BatchSendStatus>,
63 pub error: Option<String>,
64}
65
66#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67#[serde(rename_all = "UPPERCASE")]
68#[non_exhaustive]
69pub enum BatchSendStatusType {
70 InProgress,
71 Succeeded,
72 Discarded,
73}
74
75#[derive(Clone, Debug, Deserialize, Serialize)]
76pub struct BatchSendStatus {
77 pub status: BatchSendStatusType,
78 pub transferred_gas_objects: Option<FaucetReceipt>,
79}
80
81#[derive(Clone, Debug, Deserialize, Serialize)]
82pub struct FaucetReceipt {
83 pub sent: Vec<CoinInfo>,
84}
85
86#[derive(Clone, Debug, Deserialize, Serialize)]
87#[serde(rename_all = "camelCase")]
88pub struct CoinInfo {
89 pub amount: u64,
90 pub id: ObjectId,
91 pub transfer_tx_digest: TransactionDigest,
92}
93
94impl FaucetClient {
95 pub fn new(faucet_url: &str) -> Result<Self, FaucetError> {
110 if let Some(scheme) = crate::tls::unsupported_scheme(faucet_url) {
111 return Err(FaucetError::TlsUnavailable(scheme));
112 }
113 Self::new_with_reqwest_client(
114 faucet_url,
115 crate::tls::default_http_client_builder().build()?,
116 )
117 }
118
119 pub fn new_with_reqwest_client(
128 faucet_url: &str,
129 client: reqwest::Client,
130 ) -> Result<Self, FaucetError> {
131 Ok(FaucetClient {
132 faucet_url: Url::parse(faucet_url)?,
133 inner: client,
134 })
135 }
136
137 pub fn new_localnet() -> Self {
139 Self::new(FAUCET_LOCAL_HOST).expect("cannot build localnet faucet client")
140 }
141
142 pub async fn request(&self, address: Address) -> Result<Option<String>, FaucetError> {
146 self.request_impl(address).await
147 }
148
149 async fn request_impl(&self, address: Address) -> Result<Option<String>, FaucetError> {
152 let address = address.to_string();
153 let json_body = json![{
154 "FixedAmountRequest": {
155 "recipient": &address
156 }
157 }];
158 let url = format!("{}v1/gas", self.faucet_url);
159 info!(
160 "Requesting gas from faucet for address {} : {}",
161 address, url
162 );
163 let resp = self
164 .inner
165 .post(url)
166 .header("content-type", "application/json")
167 .json(&json_body)
168 .send()
169 .await?;
170 match resp.status() {
171 StatusCode::ACCEPTED | StatusCode::CREATED => {
172 let faucet_resp: FaucetResponse = resp.json().await?;
173
174 if let Some(err) = faucet_resp.error {
175 error!("Faucet request was unsuccessful: {err}");
176 Err(FaucetError::Request(err))
177 } else {
178 info!("Request successful: {:?}", faucet_resp.task);
179 Ok(faucet_resp.task)
180 }
181 }
182 StatusCode::TOO_MANY_REQUESTS => {
183 error!("Faucet service received too many requests from this IP address.");
184 Err(FaucetError::TooManyRequests)
185 }
186 StatusCode::SERVICE_UNAVAILABLE => {
187 error!("Faucet service is currently overloaded or unavailable.");
188 Err(FaucetError::Unavailable)
189 }
190 status_code => {
191 error!("Faucet request was unsuccessful: {status_code}");
192 Err(FaucetError::StatusCode(status_code))
193 }
194 }
195 }
196
197 pub async fn request_and_wait(
209 &self,
210 address: Address,
211 ) -> Result<Option<FaucetReceipt>, FaucetError> {
212 let request_id = self.request(address).await?;
213
214 if let Some(request_id) = request_id {
215 let status_response = crate::wait::timeout(FAUCET_REQUEST_TIMEOUT, async {
216 loop {
217 info!("Polling faucet request status: {request_id}");
218 let status_response = self.request_status(request_id.clone()).await?;
219
220 if let Some(status_response) = status_response {
221 match status_response.status {
222 BatchSendStatusType::Succeeded => {
223 info!("Faucet request {request_id} succeeded");
224 break Ok::<_, FaucetError>(status_response);
225 }
226 BatchSendStatusType::Discarded => {
227 break Ok(BatchSendStatus {
228 status: BatchSendStatusType::Discarded,
229 transferred_gas_objects: None,
230 });
231 }
232 BatchSendStatusType::InProgress => {}
234 }
235 }
236
237 crate::wait::sleep(FAUCET_POLL_INTERVAL).await;
238 }
239 })
240 .await
241 .map_err(|_| {
242 error!(
243 "Faucet request {request_id} timed out. Timeout set to {} seconds",
244 FAUCET_REQUEST_TIMEOUT.as_secs()
245 );
246 FaucetError::TimedOut
247 })??;
248 Ok(status_response.transferred_gas_objects)
249 } else {
250 Ok(None)
251 }
252 }
253
254 pub async fn request_and_wait_for_finalized(
263 &self,
264 address: Address,
265 client: &crate::GraphQLClient,
266 ) -> Result<Option<FaucetReceipt>, crate::error::GraphQLError> {
267 let Some(receipt) = self.request_and_wait(address).await? else {
268 return Ok(None);
269 };
270
271 let tx_digests = receipt
273 .sent
274 .iter()
275 .map(|coin| coin.transfer_tx_digest)
276 .collect::<HashSet<_>>();
277 for digest in tx_digests {
278 client
279 .wait_for_transaction(digest, WaitForTransaction::Finalized)
280 .await?;
281 }
282
283 Ok(Some(receipt))
284 }
285
286 pub async fn request_status(&self, id: String) -> Result<Option<BatchSendStatus>, FaucetError> {
290 let status_url = format!("{}v1/status/{}", self.faucet_url, id);
291 info!("Checking status of faucet request: {status_url}");
292 let response = self.inner.get(&status_url).send().await?;
293
294 if response.status() == StatusCode::TOO_MANY_REQUESTS {
295 return Err(FaucetError::TooManyRequests);
296 } else if response.status() == StatusCode::BAD_GATEWAY {
297 return Err(FaucetError::BadGateway);
298 }
299
300 let json = response.json::<BatchStatusFaucetResponse>().await?;
301
302 Ok(json.status)
303 }
304}