Skip to content

Commit 282157d

Browse files
sui-graphql-client: support faucet v2 api (#108)
1 parent 83ff809 commit 282157d

File tree

7 files changed

+109
-194
lines changed

7 files changed

+109
-194
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ jobs:
9797
- name: Get the Sui testnet binary and start a local network
9898
shell: bash
9999
env:
100-
SUI_BINARY_VERSION: "1.44.2" # used for downloading a specific Sui binary versions that matches the GraphQL schema for local network tests
100+
SUI_BINARY_VERSION: "1.48.0" # used for downloading a specific Sui binary versions that matches the GraphQL schema for local network tests
101101
SUI_NETWORK_RELEASE: "testnet" # which release to use
102102
run: |
103103
ASSET_NAME="sui-$SUI_NETWORK_RELEASE-v$SUI_BINARY_VERSION-ubuntu-x86_64.tgz"

crates/sui-graphql-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ serde = { version = "1.0.144" }
2121
serde_json = {version = "1.0.95"}
2222
sui-types = { package = "sui-sdk-types", version = "0.0.4", path = "../sui-sdk-types", features = ["serde"] }
2323
tracing = "0.1.37"
24+
thiserror = "2.0.12"
2425
tokio = "1.36.0"
2526
url = "2.5.3"
2627

crates/sui-graphql-client/README.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ async fn main() -> Result<()> {
3737
```
3838

3939
## Requesting gas from the faucet
40-
The client provides an API to request gas from the faucet. The `request_and_wait` function sends a request to the faucet and waits until the transaction is confirmed. The function returns the transaction details if the request is successful.
40+
The client provides an API to request gas from the faucet. The `request` function sends a request to the faucet and waits until the transaction is confirmed. The function returns the transaction details if the request is successful.
4141

4242
### Example for standard devnet/testnet/local networks.
4343
```rust, no_run
@@ -52,22 +52,21 @@ async fn main() -> Result<()> {
5252
let address = Address::from_str("SUI_ADDRESS_HERE")?;
5353
// Request gas from the faucet and wait until a coin is received
5454
// As the client is set to devnet, faucet will use the devnet faucet.
55-
let faucet = FaucetClient::devnet().request_and_wait(address).await?;
56-
if let Some(resp) = faucet {
57-
let coins = resp.sent;
58-
for coin in coins {
55+
let faucet = FaucetClient::devnet().request(address).await;
56+
if let Ok(resp) = faucet {
57+
for coin in resp.coins_sent {
5958
println!("coin: {:?}", coin);
6059
}
6160
}
6261
6362
// Request gas from the testnet faucet by explicitly setting the faucet to testnet
64-
let faucet_testnet = FaucetClient::testnet().request_and_wait(address).await?;
63+
let faucet_testnet = FaucetClient::testnet().request(address).await?;
6564
Ok(())
6665
}
6766
```
6867

6968
### Example for custom faucet service.
70-
Note that this `FaucetClient` is explicitly designed to work with two endpoints: `v1/gas`, and `v1/status`. When passing in the custom faucet URL, skip the final endpoint and only pass in the top-level url (e.g., `https://faucet.devnet.sui.io`).
69+
Note that this `FaucetClient` is explicitly designed to work with this endpoint: `v2/gas`, When passing in the custom faucet URL, skip the final endpoint and only pass in the top-level url (e.g., `https://faucet.devnet.sui.io`).
7170
```rust, no_run
7271
use sui_graphql_client::faucet::FaucetClient;
7372
use sui_types::Address;
@@ -80,10 +79,9 @@ async fn main() -> Result<()> {
8079
let address = Address::from_str("SUI_ADDRESS_HERE")?;
8180
// Request gas from the faucet and wait until a coin is received
8281
// As the client is set to devnet, faucet will use the devnet faucet.
83-
let faucet = FaucetClient::new("https://myfaucet_testnet.com").request_and_wait(address).await?;
84-
if let Some(resp) = faucet {
85-
let coins = resp.sent;
86-
for coin in coins {
82+
let faucet = FaucetClient::new("https://myfaucet_testnet.com").request(address).await;
83+
if let Ok(resp) = faucet {
84+
for coin in resp.coins_sent {
8785
println!("coin: {:?}", coin);
8886
}
8987
}

crates/sui-graphql-client/src/faucet.rs

Lines changed: 46 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,40 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
use sui_types::Address;
5-
use sui_types::ObjectId;
65
use sui_types::TransactionDigest;
76

8-
use anyhow::anyhow;
97
use anyhow::bail;
108
use reqwest::StatusCode;
119
use reqwest::Url;
1210
use serde::Deserialize;
1311
use serde::Serialize;
1412
use serde_json::json;
15-
use std::time::Duration;
16-
use tracing::error;
13+
use sui_types::ObjectId;
14+
use thiserror::Error;
15+
use tracing::error as tracing_error;
1716
use tracing::info;
1817

1918
pub const FAUCET_DEVNET_HOST: &str = "https://faucet.devnet.sui.io";
2019
pub const FAUCET_TESTNET_HOST: &str = "https://faucet.testnet.sui.io";
2120
pub const FAUCET_LOCAL_HOST: &str = "http://localhost:9123";
2221

23-
const FAUCET_REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
24-
const FAUCET_POLL_INTERVAL: Duration = Duration::from_secs(2);
22+
pub const FAUCET_REQUEST_PATH: &str = "v2/gas";
2523

2624
pub struct FaucetClient {
2725
faucet_url: Url,
2826
inner: reqwest::Client,
2927
}
3028

31-
#[derive(serde::Deserialize)]
32-
struct FaucetResponse {
33-
task: Option<String>,
34-
error: Option<String>,
29+
#[derive(Serialize, Deserialize, Debug)]
30+
pub enum RequestStatus {
31+
Success,
32+
Failure(FaucetError),
3533
}
3634

37-
#[derive(Serialize, Deserialize, Debug, Clone)]
38-
#[serde(rename_all = "camelCase")]
39-
struct BatchStatusFaucetResponse {
40-
pub status: Option<BatchSendStatus>,
41-
pub error: Option<String>,
42-
}
43-
44-
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
45-
#[serde(rename_all = "UPPERCASE")]
46-
pub enum BatchSendStatusType {
47-
Inprogress,
48-
Succeeded,
49-
Discarded,
50-
}
51-
52-
#[derive(Serialize, Deserialize, Debug, Clone)]
53-
pub struct BatchSendStatus {
54-
pub status: BatchSendStatusType,
55-
pub transferred_gas_objects: Option<FaucetReceipt>,
56-
}
57-
58-
#[derive(Serialize, Deserialize, Debug, Clone)]
59-
pub struct FaucetReceipt {
60-
pub sent: Vec<CoinInfo>,
61-
}
62-
63-
#[derive(Serialize, Deserialize, Debug, Clone)]
64-
struct BatchFaucetReceipt {
65-
pub task: String,
35+
#[derive(Serialize, Deserialize, Debug)]
36+
pub struct FaucetResponse {
37+
pub status: RequestStatus,
38+
pub coins_sent: Option<Vec<CoinInfo>>,
6639
}
6740

6841
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -73,13 +46,25 @@ pub struct CoinInfo {
7346
pub transfer_tx_digest: TransactionDigest,
7447
}
7548

49+
#[derive(Serialize, Deserialize, Error, Debug, PartialEq, Eq)]
50+
pub 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+
7664
impl FaucetClient {
7765
/// Construct a new `FaucetClient` with the given faucet service URL. This [`FaucetClient`]
78-
/// expects that the service provides two endpoints: /v1/gas and /v1/status. As such, do not
66+
/// expects that the service provides this endpoint: /v2/gas. As such, do not
7967
/// provide the request endpoint, just the top level service endpoint.
80-
///
81-
/// - /v1/gas is used to request gas
82-
/// - /v1/status/taks-uuid is used to check the status of the request
8368
pub fn new(faucet_url: &str) -> Self {
8469
let inner = reqwest::Client::new();
8570
let faucet_url = Url::parse(faucet_url).expect("Invalid faucet URL");
@@ -110,21 +95,17 @@ impl FaucetClient {
11095
}
11196
}
11297

113-
/// Request gas from the faucet. Note that this will return the UUID of the request and not
114-
/// wait until the token is received. Use `request_and_wait` to wait for the token.
115-
pub async fn request(&self, address: Address) -> Result<Option<String>, anyhow::Error> {
116-
self.request_impl(address).await
117-
}
118-
119-
/// Internal implementation of a faucet request. It returns the task Uuid as a String.
120-
async fn request_impl(&self, address: Address) -> Result<Option<String>, anyhow::Error> {
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> {
121101
let address = address.to_string();
122102
let json_body = json![{
123103
"FixedAmountRequest": {
124104
"recipient": &address
125105
}
126106
}];
127-
let url = format!("{}v1/gas", self.faucet_url);
107+
108+
let url = format!("{}{}", self.faucet_url, FAUCET_REQUEST_PATH);
128109
info!(
129110
"Requesting gas from faucet for address {} : {}",
130111
address, url
@@ -137,112 +118,32 @@ impl FaucetClient {
137118
.send()
138119
.await?;
139120
match resp.status() {
140-
StatusCode::ACCEPTED | StatusCode::CREATED => {
121+
StatusCode::ACCEPTED | StatusCode::CREATED | StatusCode::OK => {
141122
let faucet_resp: FaucetResponse = resp.json().await?;
142123

143-
if let Some(err) = faucet_resp.error {
144-
error!("Faucet request was unsuccessful: {err}");
145-
bail!("Faucet request was unsuccessful: {err}")
146-
} else {
147-
info!("Request succesful: {:?}", faucet_resp.task);
148-
Ok(faucet_resp.task)
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+
}
149133
}
150134
}
151135
StatusCode::TOO_MANY_REQUESTS => {
152-
error!("Faucet service received too many requests from this IP address.");
136+
tracing_error!("Faucet service received too many requests from this IP address.");
153137
bail!("Faucet service received too many requests from this IP address. Please try again after 60 minutes.");
154138
}
155139
StatusCode::SERVICE_UNAVAILABLE => {
156-
error!("Faucet service is currently overloaded or unavailable.");
140+
tracing_error!("Faucet service is currently overloaded or unavailable.");
157141
bail!("Faucet service is currently overloaded or unavailable. Please try again later.");
158142
}
159143
status_code => {
160-
error!("Faucet request was unsuccessful: {status_code}");
144+
tracing_error!("Faucet request was unsuccessful: {status_code}");
161145
bail!("Faucet request was unsuccessful: {status_code}");
162146
}
163147
}
164148
}
165-
166-
/// Request gas from the faucet and wait until the request is completed and token is
167-
/// transferred. Returns `FaucetReceipt` if the request is successful, which contains the list
168-
/// of tokens transferred, and the transaction digest.
169-
///
170-
/// Note that the faucet is heavily rate-limited, so calling repeatedly the faucet would likely
171-
/// result in a 429 code or 502 code.
172-
pub async fn request_and_wait(
173-
&self,
174-
address: Address,
175-
) -> Result<Option<FaucetReceipt>, anyhow::Error> {
176-
let request_id = self.request(address).await?;
177-
if let Some(request_id) = request_id {
178-
let poll_response = tokio::time::timeout(FAUCET_REQUEST_TIMEOUT, async {
179-
let mut interval = tokio::time::interval(FAUCET_POLL_INTERVAL);
180-
loop {
181-
interval.tick().await;
182-
info!("Polling faucet request status: {request_id}");
183-
let req = self.request_status(request_id.clone()).await;
184-
185-
if let Ok(Some(poll_response)) = req {
186-
match poll_response.status {
187-
BatchSendStatusType::Succeeded => {
188-
info!("Faucet request {request_id} succeeded");
189-
break Ok(poll_response);
190-
}
191-
BatchSendStatusType::Discarded => {
192-
break Ok(BatchSendStatus {
193-
status: BatchSendStatusType::Discarded,
194-
transferred_gas_objects: None,
195-
});
196-
}
197-
BatchSendStatusType::Inprogress => {
198-
continue;
199-
}
200-
}
201-
} else if let Some(err) = req.err() {
202-
error!("Faucet request {request_id} failed. Error: {:?}", err);
203-
break Err(anyhow!(
204-
"Faucet request {request_id} failed. Error: {:?}",
205-
err
206-
));
207-
}
208-
}
209-
})
210-
.await
211-
.map_err(|_| {
212-
error!(
213-
"Faucet request {request_id} timed out. Timeout set to {} seconds",
214-
FAUCET_REQUEST_TIMEOUT.as_secs()
215-
);
216-
anyhow!("Faucet request timed out")
217-
})??;
218-
Ok(poll_response.transferred_gas_objects)
219-
} else {
220-
Ok(None)
221-
}
222-
}
223-
224-
/// Check the faucet request status.
225-
///
226-
/// Possible statuses are defined in: [`BatchSendStatusType`]
227-
pub async fn request_status(
228-
&self,
229-
id: String,
230-
) -> Result<Option<BatchSendStatus>, anyhow::Error> {
231-
let status_url = format!("{}v1/status/{}", self.faucet_url, id);
232-
info!("Checking status of faucet request: {status_url}");
233-
let response = self.inner.get(&status_url).send().await?;
234-
if response.status() == StatusCode::TOO_MANY_REQUESTS {
235-
bail!("Cannot fetch request status due to too many requests from this IP address.");
236-
} else if response.status() == StatusCode::BAD_GATEWAY {
237-
bail!("Cannot fetch request status due to a bad gateway.")
238-
}
239-
let json = response
240-
.json::<BatchStatusFaucetResponse>()
241-
.await
242-
.map_err(|e| {
243-
error!("Failed to parse faucet response: {:?}", e);
244-
anyhow!("Failed to parse faucet response: {:?}", e)
245-
})?;
246-
Ok(json.status)
247-
}
248149
}

crates/sui-graphql-client/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,7 @@ impl Client {
588588
owner: Address,
589589
coin_type: Option<&str>,
590590
pagination_filter: PaginationFilter,
591-
) -> Result<Page<Coin>> {
591+
) -> Result<Page<Coin<'static>>> {
592592
let response = self
593593
.objects(
594594
Some(ObjectFilter {
@@ -620,7 +620,7 @@ impl Client {
620620
address: Address,
621621
coin_type: Option<&'static str>,
622622
streaming_direction: Direction,
623-
) -> impl Stream<Item = Result<Coin>> {
623+
) -> impl Stream<Item = Result<Coin<'static>>> + use<'_> {
624624
stream_paginated_query(
625625
move |filter| self.coins(address, coin_type, filter),
626626
streaming_direction,
@@ -2176,7 +2176,7 @@ mod tests {
21762176
};
21772177
let key = Ed25519PublicKey::generate(rand::thread_rng());
21782178
let address = key.derive_address();
2179-
faucet.request_and_wait(address).await.unwrap();
2179+
faucet.request(address).await.unwrap();
21802180

21812181
const MAX_RETRIES: u32 = 10;
21822182
const RETRY_DELAY: time::Duration = time::Duration::from_secs(1);

crates/sui-sdk-types/src/lib.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
//! compiled code. By default, no features are enabled which allows one to enable a subset
1313
//! specifically for their use case. Below is a list of the available feature flags.
1414
//!
15-
//! - `serde`: Enables support for serializing and deserializing types to/from BCS utilizing
16-
//! [serde] library.
15+
//! - `serde`: Enables support for serializing and deserializing types to/from BCS utilizing [serde]
16+
//! library.
1717
//! - `rand`: Enables support for generating random instances of a number of types via the [rand]
18-
//! library.
18+
//! library.
1919
//! - `hash`: Enables support for hashing, which is required for deriving addresses and calculating
20-
//! digests for various types.
20+
//! digests for various types.
2121
//! - `proptest`: Enables support for the [proptest] library by providing implementations of
22-
//! [proptest::arbitrary::Arbitrary] for many types.
22+
//! [proptest::arbitrary::Arbitrary] for many types.
2323
//!
2424
//! [feature flags]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section
2525
//! [serde]: https://docs.rs/serde

0 commit comments

Comments
 (0)