sui_graphql/client/
execution.rs

1//! Transaction execution methods.
2
3use base64ct::Base64;
4use base64ct::Encoding;
5use sui_graphql_macros::Response;
6use sui_rpc::proto::sui::rpc::v2::BalanceChange;
7use sui_sdk_types::Transaction;
8use sui_sdk_types::TransactionEffects;
9use sui_sdk_types::UserSignature;
10
11use super::Client;
12use crate::bcs::Bcs;
13use crate::error::Error;
14
15/// The result of executing a transaction on chain.
16#[derive(Debug, Clone)]
17#[non_exhaustive]
18pub struct ExecutionResult {
19    /// The transaction effects if execution was successful.
20    pub effects: Option<TransactionEffects>,
21    /// Balance changes from this transaction.
22    pub balance_changes: Vec<BalanceChange>,
23}
24
25impl Client {
26    /// Execute a signed transaction on chain.
27    ///
28    /// This commits the transaction to the blockchain and waits for finality.
29    ///
30    /// Execution errors (e.g., invalid signatures, insufficient gas) are returned as
31    /// GraphQL errors with code `BAD_USER_INPUT`, accessible via `Response::errors()`.
32    ///
33    /// # Arguments
34    ///
35    /// * `transaction` - The transaction to execute
36    /// * `signatures` - List of signatures authorizing the transaction
37    ///
38    /// # Returns
39    ///
40    /// - `Ok(result)` with `effects` and `balance_changes` if successful
41    /// - `Err(...)` for network or decoding errors
42    pub async fn execute_transaction(
43        &self,
44        transaction: &Transaction,
45        signatures: &[UserSignature],
46    ) -> Result<ExecutionResult, Error> {
47        #[derive(Response)]
48        #[response(root_type = "Mutation")]
49        struct Response {
50            #[field(path = "executeTransaction?.effects?.effectsBcs?")]
51            effects_bcs: Option<Bcs<TransactionEffects>>,
52            #[field(path = "executeTransaction?.effects?.balanceChangesJson?")]
53            balance_changes: Option<Vec<BalanceChange>>,
54        }
55
56        const MUTATION: &str = r#"
57            mutation($txDataBcs: Base64!, $signatures: [Base64!]!) {
58                executeTransaction(transactionDataBcs: $txDataBcs, signatures: $signatures) {
59                    effects {
60                        effectsBcs
61                        balanceChangesJson
62                    }
63                }
64            }
65        "#;
66
67        let tx_bytes =
68            bcs::to_bytes(transaction).map_err(|e| Error::Serialization(e.to_string()))?;
69        let tx_data_base64 = Base64::encode_string(&tx_bytes);
70        let signatures_base64: Vec<String> = signatures.iter().map(|sig| sig.to_base64()).collect();
71
72        let variables = serde_json::json!({
73            "txDataBcs": tx_data_base64,
74            "signatures": signatures_base64,
75        });
76
77        let response = self.query::<Response>(MUTATION, variables).await?;
78
79        let Some(data) = response.into_data() else {
80            return Ok(ExecutionResult {
81                effects: None,
82                balance_changes: vec![],
83            });
84        };
85
86        let effects = data.effects_bcs.map(|bcs| bcs.0);
87        let balance_changes = data.balance_changes.unwrap_or_default();
88
89        Ok(ExecutionResult {
90            effects,
91            balance_changes,
92        })
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use sui_sdk_types::Address;
100    use sui_sdk_types::GasPayment;
101    use sui_sdk_types::ObjectReference;
102    use sui_sdk_types::ProgrammableTransaction;
103    use sui_sdk_types::SimpleSignature;
104    use sui_sdk_types::TransactionExpiration;
105    use sui_sdk_types::TransactionKind;
106    use wiremock::Mock;
107    use wiremock::MockServer;
108    use wiremock::ResponseTemplate;
109    use wiremock::matchers::method;
110    use wiremock::matchers::path;
111
112    /// Create a minimal test transaction.
113    fn test_transaction() -> Transaction {
114        let sender: Address = "0x1".parse().unwrap();
115        let gas_object = ObjectReference::new(
116            "0x2".parse().unwrap(),
117            1,
118            "4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi"
119                .parse()
120                .unwrap(),
121        );
122
123        Transaction {
124            kind: TransactionKind::ProgrammableTransaction(ProgrammableTransaction {
125                inputs: vec![],
126                commands: vec![],
127            }),
128            sender,
129            gas_payment: GasPayment {
130                objects: vec![gas_object],
131                owner: sender,
132                price: 1000,
133                budget: 10_000_000,
134            },
135            expiration: TransactionExpiration::None,
136        }
137    }
138
139    /// Create a minimal test signature (not cryptographically valid, just for API testing).
140    fn test_signature() -> UserSignature {
141        UserSignature::Simple(SimpleSignature::Ed25519 {
142            signature: [0u8; 64].into(),
143            public_key: [0u8; 32].into(),
144        })
145    }
146
147    #[tokio::test]
148    async fn test_execute_transaction_success() {
149        let mock_server = MockServer::start().await;
150
151        Mock::given(method("POST"))
152            .and(path("/"))
153            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
154                "data": {
155                    "executeTransaction": {
156                        "effects": {
157                            "effectsBcs": null,
158                            "balanceChangesJson": null
159                        }
160                    }
161                }
162            })))
163            .mount(&mock_server)
164            .await;
165
166        let client = Client::new(&mock_server.uri()).unwrap();
167        let transaction = test_transaction();
168        let signature = test_signature();
169
170        let result = client
171            .execute_transaction(&transaction, &[signature])
172            .await
173            .unwrap();
174
175        assert!(result.effects.is_none());
176        assert!(result.balance_changes.is_empty());
177    }
178
179    #[tokio::test]
180    async fn test_execute_transaction_graphql_error() {
181        let mock_server = MockServer::start().await;
182
183        Mock::given(method("POST"))
184            .and(path("/"))
185            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
186                "data": null,
187                "errors": [{
188                    "message": "Invalid argument: Invalid user signature",
189                    "extensions": { "code": "BAD_USER_INPUT" }
190                }]
191            })))
192            .mount(&mock_server)
193            .await;
194
195        let client = Client::new(&mock_server.uri()).unwrap();
196        let transaction = test_transaction();
197        let signature = test_signature();
198
199        let result = client
200            .execute_transaction(&transaction, &[signature])
201            .await
202            .unwrap();
203
204        // No data returned, effects should be None
205        assert!(result.effects.is_none());
206        assert!(result.balance_changes.is_empty());
207    }
208}