Skip to main content

sui_indexer_alt_jsonrpc/api/write/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4mod 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    /// Execute the transaction with options to show different information in the response. The only
42    /// supported request type is `WaitForEffectsCert`: waits for TransactionEffectsCert and then
43    /// return to client. `WaitForLocalExecution` mode has been deprecated.
44    #[method(name = "executeTransactionBlock")]
45    async fn execute_transaction_block(
46        &self,
47        /// BCS serialized transaction data bytes without its type tag, as base-64 encoded string.
48        tx_bytes: Base64,
49        /// A list of signatures (`flag || signature || pubkey` bytes, as base-64 encoded string). Signature is committed to the intent message of the transaction data, as base-64 encoded string.
50        signatures: Vec<Base64>,
51        /// options for specifying the content to be returned
52        options: Option<SuiTransactionBlockResponseOptions>,
53        /// The request type, derived from `SuiTransactionBlockResponseOptions` if None
54        request_type: Option<ExecuteTransactionRequestType>,
55    ) -> RpcResult<SuiTransactionBlockResponse>;
56
57    /// Runs the transaction in dev-inspect mode, which allows for nearly any transaction (or Move
58    /// call) with any arguments. Detailed results are provided, including both the transaction
59    /// effects and any return values.
60    #[method(name = "devInspectTransactionBlock")]
61    async fn dev_inspect_transaction_block(
62        &self,
63        sender_address: SuiAddress,
64        /// BCS encoded TransactionKind (as opposed to TransactionData, which includes gasBudget and gasPrice).
65        tx_bytes: Base64,
66        /// Gas is not charged, but gas usage is still calculated. Default to use reference gas price.
67        gas_price: Option<BigInt<u64>>,
68        /// The epoch to perform the call. Will be set from the system state object if not provided.
69        epoch: Option<BigInt<u64>>,
70        /// Additional arguments including gas_budget, gas_objects, gas_sponsor and skip_checks.
71        additional_args: Option<DevInspectArgs>,
72    ) -> RpcResult<DevInspectResults>;
73
74    /// Return transaction execution effects including the gas cost summary,
75    /// while the effects are not committed to the chain.
76    #[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        // The legacy fullnode implementation also ignores this argument.
128        _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        // Synthesize the full TransactionData the caller would have signed, the same way the legacy
214        // fullnode implementation does. An empty gas payment is replaced by a mock gas coin on the
215        // fullnode during simulation.
216        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        // The raw transaction data reflects what the caller specified: the gas payment stays empty
226        // here, and the mock gas coin the fullnode injects during simulation only shows up in the
227        // effects (matching the legacy implementation, which captures these bytes before simulation
228        // for the same reason).
229        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        // Sending BCS TransactionData makes the fullnode skip transaction resolution and gas
247        // selection, and allows it to inject a mock gas coin -- the exact code path the legacy
248        // devInspect implementation uses.
249        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
314/// Fetch the reference gas price of the latest epoch, and the maximum gas budget under the protocol
315/// version that epoch started with. These are the defaults dev-inspect uses when the request does
316/// not specify a gas price or budget.
317///
318/// The max budget is read from `kv_protocol_configs` (rather than this binary's own
319/// `ProtocolConfig`) so that the RPC keeps working when the chain's protocol version is newer than
320/// the binary.
321async 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    // The inner join skips epoch rows whose protocol configs have not been indexed yet, falling
332    // back to the previous epoch's values for the duration of that (sub-second) window, rather than
333    // failing the request.
334    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}