Skip to main content

sui_graphql/client/
transactions.rs

1//! Transaction-related convenience methods.
2
3use sui_graphql_macros::Response;
4use sui_graphql_macros::graphql_query;
5use sui_sdk_types::Transaction;
6use sui_sdk_types::TransactionEffects;
7
8use super::Client;
9use crate::bcs::Bcs;
10use crate::error::Error;
11use crate::scalars::DateTime;
12
13/// A balance change from a transaction (re-exported from sui-rpc).
14pub use sui_rpc::proto::sui::rpc::v2::BalanceChange;
15
16/// A transaction response containing the transaction data and its effects.
17///
18/// This struct combines the transaction data with its execution results.
19#[derive(Debug, Clone)]
20#[non_exhaustive]
21pub struct TransactionResponse {
22    /// The transaction data (sender, commands, gas payment, etc.)
23    pub transaction: Transaction,
24    /// The execution effects (status, gas used, object changes, etc.)
25    pub effects: TransactionEffects,
26    /// Balance changes from this transaction.
27    pub balance_changes: Vec<BalanceChange>,
28    /// The checkpoint sequence number this transaction was finalized in.
29    pub checkpoint: u64,
30    /// Timestamp when this transaction was finalized.
31    pub timestamp: DateTime,
32}
33
34impl Client {
35    /// Fetch a transaction by its digest and deserialize from BCS.
36    ///
37    /// Returns:
38    /// - `Ok(Some(response))` if the transaction exists
39    /// - `Ok(None)` if the transaction does not exist
40    /// - `Err(Error::Request)` for network errors
41    /// - `Err(Error::Base64)` / `Err(Error::Bcs)` for decoding errors
42    ///
43    /// # Example
44    ///
45    /// ```no_run
46    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
47    /// use sui_graphql::Client;
48    ///
49    /// let client = Client::new("https://graphql.mainnet.sui.io/graphql")?;
50    /// let digest = "ABC123..."; // transaction digest
51    ///
52    /// match client.get_transaction(digest).await? {
53    ///     Some(tx) => {
54    ///         println!("Sender: {}", tx.transaction.sender);
55    ///         println!("Status: {:?}", tx.effects.status());
56    ///     }
57    ///     None => println!("Transaction not found"),
58    /// }
59    /// # Ok(())
60    /// # }
61    /// ```
62    pub async fn get_transaction(
63        &self,
64        digest: &str,
65    ) -> Result<Option<TransactionResponse>, Error> {
66        #[derive(Response)]
67        struct Response {
68            #[field(path = "transaction?.transactionBcs?")]
69            transaction_bcs: Option<Bcs<Transaction>>,
70            #[field(path = "transaction?.effects?.effectsBcs?")]
71            effects_bcs: Option<Bcs<TransactionEffects>>,
72            #[field(path = "transaction?.effects?.balanceChangesJson?")]
73            balance_changes: Option<Vec<BalanceChange>>,
74            #[field(path = "transaction?.effects?.checkpoint?.sequenceNumber?")]
75            checkpoint: Option<u64>,
76            #[field(path = "transaction?.effects?.timestamp?")]
77            timestamp: Option<DateTime>,
78        }
79
80        const QUERY: &str = graphql_query!(
81            "query($digest: String!) {
82                transaction(digest: $digest) {
83                    transactionBcs
84                    effects {
85                        effectsBcs
86                        balanceChangesJson
87                        checkpoint {
88                            sequenceNumber
89                        }
90                        timestamp
91                    }
92                }
93            }"
94        );
95
96        let variables = serde_json::json!({ "digest": digest });
97
98        let response = self.query::<Response>(QUERY, variables).await?;
99
100        let Some(data) = response.into_data() else {
101            return Ok(None);
102        };
103
104        let (Some(transaction), Some(effects)) = (data.transaction_bcs, data.effects_bcs) else {
105            return Ok(None);
106        };
107
108        let transaction = transaction.0;
109        let effects = effects.0;
110        let balance_changes = data.balance_changes.unwrap_or_default();
111        let checkpoint = data.checkpoint.ok_or(Error::MissingData("checkpoint"))?;
112        let timestamp = data.timestamp.ok_or(Error::MissingData("timestamp"))?;
113
114        Ok(Some(TransactionResponse {
115            transaction,
116            effects,
117            balance_changes,
118            checkpoint,
119            timestamp,
120        }))
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use wiremock::Mock;
128    use wiremock::MockServer;
129    use wiremock::ResponseTemplate;
130    use wiremock::matchers::method;
131    use wiremock::matchers::path;
132
133    #[tokio::test]
134    async fn test_get_transaction_not_found() {
135        let mock_server = MockServer::start().await;
136
137        Mock::given(method("POST"))
138            .and(path("/"))
139            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
140                "data": {
141                    "transaction": null
142                }
143            })))
144            .mount(&mock_server)
145            .await;
146
147        let client = Client::new(&mock_server.uri()).unwrap();
148
149        let result = client.get_transaction("nonexistent").await;
150        assert!(result.is_ok());
151        assert!(result.unwrap().is_none());
152    }
153}