diff --git a/crates/sui-graphql-client/src/lib.rs b/crates/sui-graphql-client/src/lib.rs index 9d890a069..16dbbfd4c 100644 --- a/crates/sui-graphql-client/src/lib.rs +++ b/crates/sui-graphql-client/src/lib.rs @@ -18,6 +18,8 @@ use query_types::CheckpointQuery; use query_types::CoinMetadata; use query_types::CoinMetadataArgs; use query_types::CoinMetadataQuery; +use query_types::DryRunArgs; +use query_types::DryRunQuery; use query_types::EpochSummaryArgs; use query_types::EpochSummaryQuery; use query_types::EventFilter; @@ -40,9 +42,10 @@ use query_types::TransactionBlockArgs; use query_types::TransactionBlockQuery; use query_types::TransactionBlocksQuery; use query_types::TransactionBlocksQueryArgs; +use query_types::TransactionMetadata; use query_types::TransactionsFilter; use query_types::Validator; -use reqwest::Url; + use sui_types::types::framework::Coin; use sui_types::types::Address; use sui_types::types::CheckpointSequenceNumber; @@ -52,6 +55,7 @@ use sui_types::types::Object; use sui_types::types::SignedTransaction; use sui_types::types::Transaction; use sui_types::types::TransactionEffects; +use sui_types::types::TransactionKind; use sui_types::types::UserSignature; use anyhow::anyhow; @@ -64,6 +68,7 @@ use cynic::MutationBuilder; use cynic::Operation; use cynic::QueryBuilder; use futures::Stream; +use reqwest::Url; use std::pin::Pin; const MAINNET_HOST: &str = "https://sui-mainnet.mystenlabs.com/graphql"; @@ -72,6 +77,12 @@ const DEVNET_HOST: &str = "https://sui-devnet.mystenlabs.com/graphql"; const LOCAL_HOST: &str = "http://localhost:9125/graphql"; static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); +#[derive(Debug)] +pub struct DryRunResult { + pub effects: Option, + pub error: Option, +} + #[derive(Debug)] /// A page of items returned by the GraphQL server. pub struct Page { @@ -671,6 +682,87 @@ impl Client { } } + // =========================================================================== + // Dry Run API + // =========================================================================== + + /// Dry run a [`Transaction`] and return the transaction effects and dry run error (if any). + /// + /// `skipChecks` optional flag disables the usual verification checks that prevent access to + /// objects that are owned by addresses other than the sender, and calling non-public, + /// non-entry functions, and some other checks. Defaults to false. + pub async fn dry_run_tx( + &self, + tx: &Transaction, + skip_checks: Option, + ) -> Result { + let tx_bytes = base64ct::Base64::encode_string( + &bcs::to_bytes(&tx).map_err(|_| Error::msg("Cannot encode Transaction as BCS"))?, + ); + self.dry_run(tx_bytes, skip_checks, None).await + } + + /// Dry run a [`TransactionKind`] and return the transaction effects and dry run error (if any). + /// + /// `skipChecks` optional flag disables the usual verification checks that prevent access to + /// objects that are owned by addresses other than the sender, and calling non-public, + /// non-entry functions, and some other checks. Defaults to false. + /// + /// `tx_meta` is the transaction metadata. + pub async fn dry_run_tx_kind( + &self, + tx_kind: &TransactionKind, + skip_checks: Option, + tx_meta: TransactionMetadata, + ) -> Result { + let tx_bytes = base64ct::Base64::encode_string( + &bcs::to_bytes(&tx_kind).map_err(|_| Error::msg("Cannot encode Transaction as BCS"))?, + ); + self.dry_run(tx_bytes, skip_checks, Some(tx_meta)).await + } + + /// Internal implementation of the dry run API. + async fn dry_run( + &self, + tx_bytes: String, + skip_checks: Option, + tx_meta: Option, + ) -> Result { + let skip_checks = skip_checks.unwrap_or(false); + let operation = DryRunQuery::build(DryRunArgs { + tx_bytes, + skip_checks, + tx_meta, + }); + let response = self.run_query(&operation).await?; + + // Query errors + if let Some(errors) = response.errors { + return Err(Error::msg(format!("{:?}", errors))); + } + + // Dry Run errors + let error = response + .data + .as_ref() + .and_then(|tx| tx.dry_run_transaction_block.error.clone()); + + let effects = response + .data + .map(|tx| tx.dry_run_transaction_block) + .and_then(|tx| tx.transaction) + .and_then(|tx| tx.effects) + .and_then(|bcs| bcs.bcs) + .map(|bcs| base64ct::Base64::decode_vec(bcs.0.as_str())) + .transpose() + .map_err(|_| Error::msg("Cannot decode bcs bytes from Base64 for transaction effects"))? + .map(|bcs| bcs::from_bytes::(&bcs)) + .transpose() + .map_err(|_| Error::msg("Cannot decode bcs bytes into TransactionEffects"))?; + + Ok(DryRunResult { effects, error }) + } + // =========================================================================== // Transaction API // =========================================================================== @@ -1061,4 +1153,15 @@ mod tests { ); } } + + #[tokio::test] + async fn test_dry_run() { + let client = Client::new_testnet(); + // this tx bytes works on testnet + let tx_bytes = "AAACAAiA8PoCAAAAAAAg7q6yDns6nPznaKLd9pUD2K6NFiiibC10pDVQHJKdP2kCAgABAQAAAQECAAABAQBGLuHCJ/xjZfhC4vTJt/Zrvq1gexKLaKf3aVzyIkxRaAFUHzz8ftiZdY25qP4f9zySuT1K/qyTWjbGiTu0i0Z1ZFA4gwUAAAAAILeG86EeQm3qY3ajat3iUnY2Gbrk/NbdwV/d9MZviAwwRi7hwif8Y2X4QuL0ybf2a76tYHsSi2in92lc8iJMUWjoAwAAAAAAAECrPAAAAAAAAA=="; + + let dry_run = client.dry_run(tx_bytes.to_string(), None, None).await; + + assert!(dry_run.is_ok()); + } } diff --git a/crates/sui-graphql-client/src/query_types/dry_run.rs b/crates/sui-graphql-client/src/query_types/dry_run.rs new file mode 100644 index 000000000..4e5641e1f --- /dev/null +++ b/crates/sui-graphql-client/src/query_types/dry_run.rs @@ -0,0 +1,58 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use sui_types::types::ObjectReference; + +use crate::query_types::schema; +use crate::query_types::Address; +use crate::query_types::TransactionBlock; + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema = "rpc", graphql_type = "Query", variables = "DryRunArgs")] +pub struct DryRunQuery { + #[arguments(txBytes: $tx_bytes, skipChecks: $skip_checks, txMeta: $tx_meta)] + pub dry_run_transaction_block: DryRunResult, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema = "rpc", graphql_type = "DryRunResult")] +pub struct DryRunResult { + pub error: Option, + pub transaction: Option, +} + +#[derive(cynic::QueryVariables, Debug)] +pub struct DryRunArgs { + pub tx_bytes: String, + pub skip_checks: bool, + pub tx_meta: Option, +} + +#[derive(cynic::InputObject, Debug)] +#[cynic(schema = "rpc", graphql_type = "TransactionMetadata")] +pub struct TransactionMetadata { + pub gas_budget: Option, + pub gas_objects: Option>, + pub gas_price: Option, + pub gas_sponsor: Option
, + pub sender: Option
, +} + +#[derive(cynic::InputObject, Debug)] +#[cynic(schema = "rpc", graphql_type = "ObjectRef")] +pub struct ObjectRef { + pub address: Address, + pub digest: String, + pub version: u64, +} + +impl From for ObjectRef { + fn from(value: ObjectReference) -> Self { + let address: Address = (*value.object_id()).into(); + ObjectRef { + address, + version: value.version(), + digest: value.digest().to_string(), + } + } +} diff --git a/crates/sui-graphql-client/src/query_types/mod.rs b/crates/sui-graphql-client/src/query_types/mod.rs index fcc7e84a0..824b6d65e 100644 --- a/crates/sui-graphql-client/src/query_types/mod.rs +++ b/crates/sui-graphql-client/src/query_types/mod.rs @@ -6,6 +6,7 @@ mod balance; mod chain; mod checkpoint; mod coin; +mod dry_run; mod epoch; mod events; mod execute_tx; @@ -20,7 +21,6 @@ pub use active_validators::EpochValidator; pub use active_validators::Validator; pub use active_validators::ValidatorConnection; pub use active_validators::ValidatorSet; -use anyhow::anyhow; pub use balance::Balance; pub use balance::BalanceArgs; pub use balance::BalanceQuery; @@ -32,6 +32,10 @@ pub use checkpoint::CheckpointQuery; pub use coin::CoinMetadata; pub use coin::CoinMetadataArgs; pub use coin::CoinMetadataQuery; +pub use dry_run::DryRunArgs; +pub use dry_run::DryRunQuery; +pub use dry_run::DryRunResult; +pub use dry_run::TransactionMetadata; pub use epoch::Epoch; pub use epoch::EpochSummaryArgs; pub use epoch::EpochSummaryQuery; @@ -55,13 +59,16 @@ pub use protocol_config::ProtocolVersionArgs; pub use service_config::Feature; pub use service_config::ServiceConfig; pub use service_config::ServiceConfigQuery; -use sui_types::types::Address; +pub use transaction::TransactionBlock; pub use transaction::TransactionBlockArgs; pub use transaction::TransactionBlockQuery; pub use transaction::TransactionBlocksQuery; pub use transaction::TransactionBlocksQueryArgs; pub use transaction::TransactionsFilter; +use sui_types::types::Address; + +use anyhow::anyhow; use cynic::impl_scalar; #[cynic::schema("rpc")] diff --git a/crates/sui-graphql-client/src/query_types/transaction.rs b/crates/sui-graphql-client/src/query_types/transaction.rs index 9b76ed77a..5df1cb516 100644 --- a/crates/sui-graphql-client/src/query_types/transaction.rs +++ b/crates/sui-graphql-client/src/query_types/transaction.rs @@ -58,6 +58,13 @@ pub struct TransactionBlocksQueryArgs { #[cynic(schema = "rpc", graphql_type = "TransactionBlock")] pub struct TransactionBlock { pub bcs: Option, + pub effects: Option, +} + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(schema = "rpc", graphql_type = "TransactionBlockEffects")] +pub struct TransactionBlockEffects { + pub bcs: Option, } #[derive(cynic::Enum, Clone, Copy, Debug)]