use sui_graphql_macros::Response;
use sui_graphql_macros::graphql_query;
use sui_sdk_types::Transaction;
use sui_sdk_types::TransactionEffects;
use super::Client;
use crate::bcs::Bcs;
use crate::error::Error;
use crate::scalars::DateTime;
pub use sui_rpc::proto::sui::rpc::v2::BalanceChange;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct TransactionResponse {
pub transaction: Transaction,
pub effects: TransactionEffects,
pub balance_changes: Vec<BalanceChange>,
pub checkpoint: u64,
pub timestamp: DateTime,
}
impl Client {
pub async fn get_transaction(
&self,
digest: &str,
) -> Result<Option<TransactionResponse>, Error> {
#[derive(Response)]
struct Response {
#[field(path = "transaction?.transactionBcs?")]
transaction_bcs: Option<Bcs<Transaction>>,
#[field(path = "transaction?.effects?.effectsBcs?")]
effects_bcs: Option<Bcs<TransactionEffects>>,
#[field(path = "transaction?.effects?.balanceChangesJson?")]
balance_changes: Option<Vec<BalanceChange>>,
#[field(path = "transaction?.effects?.checkpoint?.sequenceNumber?")]
checkpoint: Option<u64>,
#[field(path = "transaction?.effects?.timestamp?")]
timestamp: Option<DateTime>,
}
const QUERY: &str = graphql_query!(
"query($digest: String!) {
transaction(digest: $digest) {
transactionBcs
effects {
effectsBcs
balanceChangesJson
checkpoint {
sequenceNumber
}
timestamp
}
}
}"
);
let variables = serde_json::json!({ "digest": digest });
let response = self.query::<Response>(QUERY, variables).await?;
let Some(data) = response.into_data() else {
return Ok(None);
};
let (Some(transaction), Some(effects)) = (data.transaction_bcs, data.effects_bcs) else {
return Ok(None);
};
let transaction = transaction.0;
let effects = effects.0;
let balance_changes = data.balance_changes.unwrap_or_default();
let checkpoint = data.checkpoint.ok_or(Error::MissingData("checkpoint"))?;
let timestamp = data.timestamp.ok_or(Error::MissingData("timestamp"))?;
Ok(Some(TransactionResponse {
transaction,
effects,
balance_changes,
checkpoint,
timestamp,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
#[tokio::test]
async fn test_get_transaction_not_found() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"data": {
"transaction": null
}
})))
.mount(&mock_server)
.await;
let client = Client::new(&mock_server.uri()).unwrap();
let result = client.get_transaction("nonexistent").await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
}