sui_graphql/client/
transactions.rs

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