Skip to main content

sui_graphql/client/
execution.rs

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