sui_indexer_alt_jsonrpc/api/write/
mod.rs1mod response;
5
6use anyhow::Context as _;
7use diesel::ExpressionMethods;
8use diesel::JoinOnDsl;
9use diesel::QueryDsl;
10use fastcrypto::encoding::Base64;
11use jsonrpsee::core::RpcResult;
12use jsonrpsee::proc_macros::rpc;
13use prost_types::FieldMask;
14use sui_indexer_alt_schema::schema::kv_epoch_starts;
15use sui_indexer_alt_schema::schema::kv_protocol_configs;
16use sui_json_rpc_types::DevInspectArgs;
17use sui_json_rpc_types::DevInspectResults;
18use sui_json_rpc_types::DryRunTransactionBlockResponse;
19use sui_json_rpc_types::SuiTransactionBlockResponse;
20use sui_json_rpc_types::SuiTransactionBlockResponseOptions;
21use sui_open_rpc::Module;
22use sui_open_rpc_macros::open_rpc;
23use sui_rpc::field::FieldMaskUtil;
24use sui_rpc::proto::sui::rpc::v2 as proto;
25use sui_types::base_types::SuiAddress;
26use sui_types::crypto::ToFromBytes;
27use sui_types::signature::GenericSignature;
28use sui_types::sui_serde::BigInt;
29use sui_types::transaction::TransactionData;
30use sui_types::transaction::TransactionKind;
31use sui_types::transaction_driver_types::ExecuteTransactionRequestType;
32
33use crate::api::rpc_module::RpcModule;
34use crate::context::Context;
35use crate::error::RpcError;
36use crate::error::invalid_params;
37
38#[open_rpc(namespace = "sui", tag = "Write API")]
39#[rpc(server, client, namespace = "sui")]
40pub trait WriteApi {
41 #[method(name = "executeTransactionBlock")]
45 async fn execute_transaction_block(
46 &self,
47 tx_bytes: Base64,
49 signatures: Vec<Base64>,
51 options: Option<SuiTransactionBlockResponseOptions>,
53 request_type: Option<ExecuteTransactionRequestType>,
55 ) -> RpcResult<SuiTransactionBlockResponse>;
56
57 #[method(name = "devInspectTransactionBlock")]
61 async fn dev_inspect_transaction_block(
62 &self,
63 sender_address: SuiAddress,
64 tx_bytes: Base64,
66 gas_price: Option<BigInt<u64>>,
68 epoch: Option<BigInt<u64>>,
70 additional_args: Option<DevInspectArgs>,
72 ) -> RpcResult<DevInspectResults>;
73
74 #[method(name = "dryRunTransactionBlock")]
77 async fn dry_run_transaction_block(
78 &self,
79 tx_bytes: Base64,
80 ) -> RpcResult<DryRunTransactionBlockResponse>;
81}
82
83pub(crate) struct Write {
84 context: Context,
85}
86
87#[derive(Debug, thiserror::Error)]
88pub enum Error {
89 #[error("WaitForLocalExecution mode is deprecated")]
90 DeprecatedWaitForLocalExecution,
91
92 #[error("Invalid transaction bytes: {0}")]
93 InvalidTransactionBytes(String),
94
95 #[error("Invalid signature: {0}")]
96 InvalidSignature(String),
97
98 #[error("Transaction execution failed: {0}")]
99 ExecutionFailed(String),
100}
101
102impl Write {
103 pub(crate) fn new(context: Context) -> Self {
104 Self { context }
105 }
106}
107
108#[async_trait::async_trait]
109impl WriteApiServer for Write {
110 async fn execute_transaction_block(
111 &self,
112 tx_bytes: Base64,
113 signatures: Vec<Base64>,
114 options: Option<SuiTransactionBlockResponseOptions>,
115 request_type: Option<ExecuteTransactionRequestType>,
116 ) -> RpcResult<SuiTransactionBlockResponse> {
117 Ok(self
118 .execute_transaction_block_impl(tx_bytes, signatures, options, request_type)
119 .await?)
120 }
121
122 async fn dev_inspect_transaction_block(
123 &self,
124 sender_address: SuiAddress,
125 tx_bytes: Base64,
126 gas_price: Option<BigInt<u64>>,
127 _epoch: Option<BigInt<u64>>,
129 additional_args: Option<DevInspectArgs>,
130 ) -> RpcResult<DevInspectResults> {
131 Ok(self
132 .dev_inspect_transaction_block_impl(
133 sender_address,
134 tx_bytes,
135 gas_price,
136 additional_args,
137 )
138 .await?)
139 }
140
141 async fn dry_run_transaction_block(
142 &self,
143 tx_bytes: Base64,
144 ) -> RpcResult<DryRunTransactionBlockResponse> {
145 Ok(self.dry_run_transaction_block_impl(tx_bytes).await?)
146 }
147}
148
149impl RpcModule for Write {
150 fn schema(&self) -> Module {
151 WriteApiOpenRpc::module_doc()
152 }
153
154 fn into_impl(self) -> jsonrpsee::RpcModule<Self> {
155 self.into_rpc()
156 }
157}
158
159impl Write {
160 async fn execute_transaction_block_impl(
161 &self,
162 tx_bytes: Base64,
163 signatures: Vec<Base64>,
164 options: Option<SuiTransactionBlockResponseOptions>,
165 request_type: Option<ExecuteTransactionRequestType>,
166 ) -> Result<SuiTransactionBlockResponse, RpcError<Error>> {
167 let client = self.context.fullnode_client()?;
168 if let Some(ExecuteTransactionRequestType::WaitForLocalExecution) = request_type {
169 return Err(invalid_params(Error::DeprecatedWaitForLocalExecution));
170 }
171
172 let options = options.unwrap_or_default();
173 let tx_data = parse_transaction_data(&tx_bytes)?;
174 let parsed_sigs = parse_signatures_impl(&signatures)?;
175 let read_mask = build_execute_read_mask(&options);
176
177 let grpc_response = client
178 .execute_transaction(tx_data.clone(), parsed_sigs.clone(), read_mask)
179 .await
180 .map_err(grpc_error_to_rpc_error)?;
181
182 let executed_tx = grpc_response
183 .transaction
184 .as_ref()
185 .context("Missing transaction in gRPC response")?;
186
187 response::transaction(&self.context, tx_data, parsed_sigs, executed_tx, &options).await
188 }
189
190 async fn dev_inspect_transaction_block_impl(
191 &self,
192 sender_address: SuiAddress,
193 tx_bytes: Base64,
194 gas_price: Option<BigInt<u64>>,
195 additional_args: Option<DevInspectArgs>,
196 ) -> Result<DevInspectResults, RpcError<Error>> {
197 let client = self.context.fullnode_client()?;
198
199 let DevInspectArgs {
200 gas_sponsor,
201 gas_budget,
202 gas_objects,
203 skip_checks,
204 show_raw_txn_data_and_effects,
205 } = additional_args.unwrap_or_default();
206
207 let skip_checks = skip_checks.unwrap_or(true);
208 let show_raw_txn_data_and_effects = show_raw_txn_data_and_effects.unwrap_or(false);
209
210 let kind = parse_transaction_kind(&tx_bytes)?;
211 let (reference_gas_price, max_tx_gas) = gas_defaults(&self.context).await?;
212
213 let tx_data = TransactionData::new_with_gas_coins_allow_sponsor(
217 kind,
218 sender_address,
219 gas_objects.unwrap_or_default(),
220 gas_budget.map(|budget| *budget).unwrap_or(max_tx_gas),
221 gas_price.map(|price| *price).unwrap_or(reference_gas_price),
222 gas_sponsor.unwrap_or(sender_address),
223 );
224
225 let raw_txn_data = if show_raw_txn_data_and_effects {
230 bcs::to_bytes(&tx_data).context("Failed to serialize transaction data")?
231 } else {
232 vec![]
233 };
234
235 let mut proto_tx = proto::Transaction::default();
236 proto_tx.bcs = Some(
237 proto::Bcs::serialize(&tx_data).context("Failed to serialize transaction for gRPC")?,
238 );
239
240 let read_mask = FieldMask::from_paths([
241 "transaction.effects.bcs",
242 "transaction.events.bcs",
243 "command_outputs",
244 ]);
245
246 let grpc_response = client
250 .simulate_transaction(proto_tx, !skip_checks, false, read_mask)
251 .await
252 .map_err(grpc_error_to_rpc_error)?;
253
254 let executed_tx = grpc_response
255 .transaction
256 .as_ref()
257 .context("Missing transaction in dev inspect gRPC response")?;
258
259 response::dev_inspect(
260 &self.context,
261 tx_data,
262 executed_tx,
263 &grpc_response.command_outputs,
264 raw_txn_data,
265 show_raw_txn_data_and_effects,
266 )
267 .await
268 }
269
270 async fn dry_run_transaction_block_impl(
271 &self,
272 tx_bytes: Base64,
273 ) -> Result<DryRunTransactionBlockResponse, RpcError<Error>> {
274 let client = self.context.fullnode_client()?;
275 let tx_data = parse_transaction_data(&tx_bytes)?;
276
277 let mut proto_tx = proto::Transaction::default();
278 proto_tx.bcs = Some(
279 proto::Bcs::serialize(&tx_data).context("Failed to serialize transaction for gRPC")?,
280 );
281
282 let read_mask = FieldMask::from_paths([
283 "transaction.effects.bcs",
284 "transaction.transaction.bcs",
285 "transaction.events.bcs",
286 "transaction.balance_changes",
287 "transaction.effects.changed_objects",
288 "transaction.objects.objects.bcs",
289 "transaction.checkpoint",
290 "transaction.timestamp",
291 "suggested_gas_price",
292 ]);
293
294 let grpc_response = client
295 .simulate_transaction(proto_tx, true, false, read_mask)
296 .await
297 .map_err(grpc_error_to_rpc_error)?;
298
299 let executed_tx = grpc_response
300 .transaction
301 .as_ref()
302 .context("Missing transaction in dry run gRPC response")?;
303
304 response::dry_run(
305 &self.context,
306 tx_data,
307 executed_tx,
308 grpc_response.suggested_gas_price,
309 )
310 .await
311 }
312}
313
314async fn gas_defaults(ctx: &Context) -> Result<(u64, u64), RpcError<Error>> {
322 use kv_epoch_starts::dsl as e;
323 use kv_protocol_configs::dsl as p;
324
325 let mut conn = ctx
326 .pg_reader()
327 .connect()
328 .await
329 .context("Failed to connect to the database")?;
330
331 let (reference_gas_price, max_tx_gas): (i64, Option<String>) = conn
335 .first(
336 e::kv_epoch_starts
337 .inner_join(p::kv_protocol_configs.on(p::protocol_version.eq(e::protocol_version)))
338 .filter(p::config_name.eq("max_tx_gas"))
339 .order(e::epoch.desc())
340 .select((e::reference_gas_price, p::config_value)),
341 )
342 .await
343 .context("Failed to fetch the latest epoch's gas parameters")?;
344
345 let max_tx_gas: u64 = max_tx_gas
346 .context("max_tx_gas is not set")?
347 .parse()
348 .context("Failed to parse max_tx_gas")?;
349
350 Ok((reference_gas_price as u64, max_tx_gas))
351}
352
353fn parse_transaction_kind(tx_bytes: &Base64) -> Result<TransactionKind, RpcError<Error>> {
354 let raw_tx_bytes = tx_bytes
355 .to_vec()
356 .map_err(|e| invalid_params(Error::InvalidTransactionBytes(e.to_string())))?;
357 bcs::from_bytes(&raw_tx_bytes).map_err(|e| {
358 invalid_params(Error::InvalidTransactionBytes(format!(
359 "Failed to deserialize TransactionKind: {e}"
360 )))
361 })
362}
363
364fn parse_transaction_data(tx_bytes: &Base64) -> Result<TransactionData, RpcError<Error>> {
365 let raw_tx_bytes = tx_bytes
366 .to_vec()
367 .map_err(|e| invalid_params(Error::InvalidTransactionBytes(e.to_string())))?;
368 bcs::from_bytes(&raw_tx_bytes).map_err(|e| {
369 invalid_params(Error::InvalidTransactionBytes(format!(
370 "Failed to deserialize TransactionData: {e}"
371 )))
372 })
373}
374
375fn parse_signatures_impl(signatures: &[Base64]) -> Result<Vec<GenericSignature>, RpcError<Error>> {
376 signatures
377 .iter()
378 .enumerate()
379 .map(|(i, sig)| {
380 let bytes = sig.to_vec().map_err(|e| {
381 invalid_params(Error::InvalidSignature(format!(
382 "Invalid base64 in signature {i}: {e}"
383 )))
384 })?;
385 GenericSignature::from_bytes(&bytes).map_err(|e| {
386 invalid_params(Error::InvalidSignature(format!(
387 "Invalid signature {i}: {e}"
388 )))
389 })
390 })
391 .collect()
392}
393
394fn build_execute_read_mask(options: &SuiTransactionBlockResponseOptions) -> FieldMask {
395 let mut paths = vec!["checkpoint", "timestamp"];
396
397 if options.show_effects || options.show_raw_effects || options.show_object_changes {
398 paths.push("effects.bcs");
399 }
400
401 if options.show_object_changes {
402 paths.push("effects.changed_objects");
403 paths.push("objects.objects.bcs");
404 }
405
406 if options.show_events {
407 paths.push("events.bcs");
408 }
409
410 if options.show_balance_changes {
411 paths.push("balance_changes");
412 }
413
414 FieldMask::from_paths(paths)
415}
416
417fn grpc_error_to_rpc_error(
418 error: sui_indexer_alt_reader::fullnode_client::Error,
419) -> RpcError<Error> {
420 use sui_indexer_alt_reader::fullnode_client::Error;
421 match error {
422 Error::GrpcExecutionError(status)
423 if matches!(
424 status.code(),
425 tonic::Code::InvalidArgument | tonic::Code::NotFound
426 ) =>
427 {
428 invalid_params(crate::api::write::Error::ExecutionFailed(
429 status.message().to_string(),
430 ))
431 }
432 Error::Internal(err) => err.context("Write API gRPC request failed").into(),
433 Error::GrpcExecutionError(status) => anyhow::Error::new(status)
434 .context("Write API gRPC request failed")
435 .into(),
436 }
437}