sui_graphql/client/
execution.rs1use 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#[derive(Debug, Clone)]
18#[non_exhaustive]
19pub struct ExecutionResult {
20 pub effects: Option<TransactionEffects>,
22 pub balance_changes: Vec<BalanceChange>,
24}
25
26impl Client {
27 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 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 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 assert!(result.effects.is_none());
207 assert!(result.balance_changes.is_empty());
208 }
209}