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