Skip to content

Commit 536efc0

Browse files
stefan-mystenDaughterOfMars
authored andcommitted
sui-graphql-client: add coins function (#12)
1 parent 99abfff commit 536efc0

File tree

2 files changed

+121
-3
lines changed

2 files changed

+121
-3
lines changed

crates/sui-graphql-client/Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@ description = "Sui GraphQL RPC Client for the Sui Blockchain"
1010

1111
[dependencies]
1212
anyhow = "1.0.8"
13+
async-stream = "0.3.5"
1314
async-trait = "0.1.8"
1415
base64ct = { version = "1.6.0", features = ["alloc"] }
1516
bcs = "0.1.6"
16-
chrono = { version = "0.4.38" }
17-
cynic = { version = "3.8.0" }
17+
chrono = "0.4.38"
18+
cynic = "3.8.0"
19+
futures = "0.3.30"
1820
reqwest = { version = "0.12", features = ["json"] }
1921
sui-types = { package = "sui-sdk-types", path = "../sui-sdk-types", features = ["serde"] }
22+
tokio = "1.40.0"
23+
24+
[dev-dependencies]
2025
tokio = { version = "1.40.0", features = ["full"] }
2126

2227
[build-dependencies]

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

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ use query_types::{
1717
};
1818
use reqwest::Url;
1919
use sui_types::types::{
20-
Address, CheckpointSequenceNumber, CheckpointSummary, Event, Object, SignedTransaction,
20+
framework::Coin, Address, CheckpointSequenceNumber, CheckpointSummary, Event, Object,
21+
SignedTransaction,
2122
};
2223

2324
use anyhow::{anyhow, ensure, Error, Result};
2425
use cynic::{serde, GraphQlResponse, Operation, QueryBuilder};
26+
use futures::Stream;
27+
use std::pin::Pin;
2528

2629
const MAINNET_HOST: &str = "https://sui-mainnet.mystenlabs.com/graphql";
2730
const TESTNET_HOST: &str = "https://sui-testnet.mystenlabs.com/graphql";
@@ -231,6 +234,87 @@ impl Client {
231234
// Coin API
232235
// ===========================================================================
233236

237+
/// Get the list of coins for the specified address.
238+
///
239+
/// If `coin_type` is not provided, it will default to `0x2::coin::Coin`, which will return all
240+
/// coins. For SUI coin, pass in the coin type: `0x2::coin::Coin<0x2::sui::SUI>`.
241+
pub async fn coins(
242+
&self,
243+
owner: Address,
244+
after: Option<&str>,
245+
before: Option<&str>,
246+
first: Option<i32>,
247+
last: Option<i32>,
248+
coin_type: Option<&str>,
249+
) -> Result<Option<Page<Coin>>, Error> {
250+
let response = self
251+
.objects(
252+
after,
253+
before,
254+
Some(ObjectFilter {
255+
type_: Some(coin_type.unwrap_or("0x2::coin::Coin")),
256+
owner: Some(owner.into()),
257+
object_ids: None,
258+
object_keys: None,
259+
}),
260+
first,
261+
last,
262+
)
263+
.await?;
264+
265+
Ok(response.map(|x| {
266+
Page::new(
267+
x.page_info,
268+
x.data
269+
.iter()
270+
.flat_map(Coin::try_from_object)
271+
.map(|c| c.into_owned())
272+
.collect::<Vec<_>>(),
273+
)
274+
}))
275+
}
276+
277+
/// Stream of coins for the specified address and coin type.
278+
pub fn coins_stream<'a>(
279+
&'a self,
280+
owner: Address,
281+
coin_type: Option<&'a str>,
282+
) -> Pin<Box<dyn Stream<Item = Result<Coin, Error>> + 'a>> {
283+
Box::pin(async_stream::try_stream! {
284+
let mut after = None;
285+
loop {
286+
let response = self.objects(
287+
after.as_deref(),
288+
None,
289+
Some(ObjectFilter {
290+
type_: Some(coin_type.unwrap_or("0x2::coin::Coin")),
291+
owner: Some(owner.into()),
292+
object_ids: None,
293+
object_keys: None,
294+
}),
295+
None,
296+
None,
297+
).await?;
298+
299+
if let Some(page) = response {
300+
for object in page.data {
301+
if let Some(coin) = Coin::try_from_object(&object) {
302+
yield coin.into_owned();
303+
}
304+
}
305+
306+
if let Some(end_cursor) = page.page_info.end_cursor {
307+
after = Some(end_cursor);
308+
} else {
309+
break;
310+
}
311+
} else {
312+
break;
313+
}
314+
}
315+
})
316+
}
317+
234318
pub async fn coin_metadata(&self, coin_type: &str) -> Result<Option<CoinMetadata>, Error> {
235319
let operation = CoinMetadataQuery::build(CoinMetadataArgs { coin_type });
236320
let response = self.run_query(&operation).await?;
@@ -591,6 +675,8 @@ impl Client {
591675

592676
#[cfg(test)]
593677
mod tests {
678+
use futures::StreamExt;
679+
594680
use crate::{Client, DEFAULT_LOCAL_HOST, DEVNET_HOST, MAINNET_HOST, TESTNET_HOST};
595681
const NETWORKS: [(&str, &str); 2] = [(MAINNET_HOST, "35834a8a"), (TESTNET_HOST, "4c78adac")];
596682

@@ -783,4 +869,31 @@ mod tests {
783869
);
784870
}
785871
}
872+
873+
#[tokio::test]
874+
async fn test_coins_query() {
875+
for (n, _) in NETWORKS {
876+
let client = Client::new(n).unwrap();
877+
let coins = client
878+
.coins("0x1".parse().unwrap(), None, None, None, None, None)
879+
.await;
880+
assert!(
881+
coins.is_ok(),
882+
"Coins query failed for network: {n}. Error: {}",
883+
coins.unwrap_err()
884+
);
885+
}
886+
}
887+
888+
#[tokio::test]
889+
async fn test_coins_stream() {
890+
let client = Client::new_testnet();
891+
let mut stream = client.coins_stream("0x1".parse().unwrap(), None);
892+
let mut num_coins = 0;
893+
while let Some(result) = stream.next().await {
894+
assert!(result.is_ok());
895+
num_coins += 1;
896+
}
897+
assert!(num_coins > 0);
898+
}
786899
}

0 commit comments

Comments
 (0)