diff --git a/apps/transfers/scripts/listen.sh b/apps/transfers/scripts/listen.sh index 279c2756..614ea4f8 100755 --- a/apps/transfers/scripts/listen.sh +++ b/apps/transfers/scripts/listen.sh @@ -1,6 +1,6 @@ #!/bin/bash -ROOT=${ROOT:-$HOME} +ROOT=${ROOT:-$(git rev-parse --show-toplevel)} DEFAULT_NODE="127.0.0.1:26657" NODE_URL=${NODE_URL:-$DEFAULT_NODE} # Use the QUARTZ_PORT environment variable if set, otherwise default to 11090 @@ -54,11 +54,11 @@ REPORT_SIG_FILE="/tmp/${USER}_datareportsig" STATE=$($CMD query wasm contract-state raw $CONTRACT $(printf '%s' "state" | \ hexdump -ve '/1 "%02X"') -o json | jq -r .data | base64 -d) - cd "$ROOT/cycles-quartz/apps/transfers" + cd "$ROOT/apps/transfers" export TRUSTED_HASH=$(cat trusted.hash) export TRUSTED_HEIGHT=$(cat trusted.height) - cd $ROOT/cycles-quartz/utils/tm-prover + cd $ROOT/utils/tm-prover export PROOF_FILE="light-client-proof.json" if [ -f "$PROOF_FILE" ]; then rm "$PROOF_FILE" @@ -85,7 +85,7 @@ REPORT_SIG_FILE="/tmp/${USER}_datareportsig" export REQUEST_MSG=$(jq --argjson msg "$ENCLAVE_REQUEST" '. + {msg: $msg}' <<< "$POP") export PROTO_MSG=$(jq -nc --arg message "$REQUEST_MSG" '$ARGS.named') - cd $ROOT/cycles-quartz/apps/transfers/enclave + cd $ROOT/apps/transfers/enclave echo "... executing transfer" export ATTESTED_MSG=$(grpcurl -plaintext -import-path ./proto/ -proto transfers.proto \ @@ -135,7 +135,7 @@ REPORT_SIG_FILE="/tmp/${USER}_datareportsig" --arg ephemeral_pubkey "$EPHEMERAL_PUBKEY" '$ARGS.named') export REQUEST_MSG=$(jq -nc --arg message "$ENCLAVE_REQUEST" '$ARGS.named') - cd $ROOT/cycles-quartz/apps/transfers/enclave + cd $ROOT/apps/transfers/enclave echo "... executing query balance" ATTESTED_MSG=$(grpcurl -plaintext -import-path ./proto/ -proto transfers.proto \ diff --git a/cli/README.md b/cli/README.md index b1385c57..2b212c95 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,7 +1,6 @@ # quartz CLI -A CLI tool to manage Quartz applications. The `quartz` CLI tool is designed to streamline the development and deployment -process of Quartz applications. +The `quartz` CLI tool is designed to streamline the development and deployment process of Quartz applications. It provides helpful information about each command and its options. To get a list of all available subcommands and their descriptions, use the `--help` flag: @@ -74,3 +73,60 @@ apps/transfers/ ├── frontend/ └── README.md ``` + +## Intsructions to quickly setup the transfers app +You can use these instructions to run the transfers app. For your own app, you will need to adjust the env vars and paths as needed. + +> Note - Run all commands from within the `/cli` folder + +```bash +# setup env vars in ALL THREE terminals +export MOCK_SGX=true +export NODE_URL=143.244.186.205:26657 +export CHAIN_ID=testing + +#------------------------------------------------------------------------------- +# TERMINAL 1 - setup enclave + +# build enclave +cargo run -- enclave build --manifest-path "../apps/transfers/enclave/Cargo.toml" + +# start enclave +cargo run -- enclave start --app-dir "../apps/transfers" --chain-id $CHAIN_ID + +#------------------------------------------------------------------------------- +# TERMINAL 2 - After enclave is setup, setup contracts + +# build contracts +cargo run -- --mock-sgx contract build --manifest-path "../apps/transfers/contracts/Cargo.toml" + +# deploy contracts +cargo run -- \ + --mock-sgx \ + contract deploy \ + --wasm-bin-path "../apps/transfers/contracts/target/wasm32-unknown-unknown/release/transfers_contract.wasm" \ + --init-msg '{"denom": "ucosm"}' + +# retrieve contract addr from output and store in env +export CONTRACT= + +# handshake +cargo run -- --mock-sgx handshake --app-dir "../apps/transfers/" --contract $CONTRACT + +# listen - NOTE - still using bash instead of cli +bash ../apps/transfers/scripts/listen.sh $CONTRACT + +#------------------------------------------------------------------------------- +# TERMINAL 3 - After contracts are setup, interact with them + +export CONTRACT= + +## example 1 +cargo run -- contract tx --msg "\"deposit\"" --amount 1000ucosm --gas 200000 --contract $CONTRACT + +## example 2 +cargo run -- \ + contract tx \ + --msg "{\"query_request\": {\"emphemeral_pubkey\": \"$EPHEMERAL_PUBKEY\"}}" \ + --contract $CONTRACT +``` diff --git a/cli/src/cli.rs b/cli/src/cli.rs index d5f893b2..4bd8af07 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -87,10 +87,12 @@ pub enum Command { #[derive(Debug, Clone, Subcommand)] pub enum ContractCommand { + // Build the Quartz app's contract Build { #[clap(long)] manifest_path: PathBuf, }, + // Deploy a contract to the chain Deploy { /// Json-formatted cosmwasm contract initialization message #[clap(long, default_value = "{}")] @@ -111,6 +113,30 @@ pub enum ContractCommand { #[clap(long)] wasm_bin_path: PathBuf, }, + /// Submit a transaction to the contract + Tx { + /// : to tendermint rpc interface for this chain + #[clap(long, default_value_t = default_node_url())] + node_url: String, + /// Contract account ID + #[arg(short, long, value_parser = wasmaddr_to_id)] + contract: AccountId, + /// The network chain ID + #[arg(long, default_value = "testing")] + chain_id: ChainId, + /// Gas to send with tx + #[arg(long, default_value = "900000000")] + gas: u64, + /// Name or address of private key with which to sign + #[arg(short, long, default_value = "admin")] + sender: String, + /// Method to call on the contract + #[arg(long)] + msg: String, + /// Amount of base coin to send with tx, i.e. "1000ucosm" + #[arg(long)] + amount: Option, + }, } #[derive(Debug, Clone, Subcommand)] diff --git a/cli/src/handler.rs b/cli/src/handler.rs index 7dd693c1..4f1447f3 100644 --- a/cli/src/handler.rs +++ b/cli/src/handler.rs @@ -6,6 +6,7 @@ pub mod utils; // commands pub mod contract_build; pub mod contract_deploy; +pub mod contract_tx; pub mod enclave_build; pub mod enclave_start; pub mod handshake; @@ -30,6 +31,7 @@ impl Handler for Request { Request::Handshake(request) => request.handle(config).await, Request::ContractBuild(request) => request.handle(config).await, Request::ContractDeploy(request) => request.handle(config).await, + Request::ContractTx(request) => request.handle(config).await, Request::EnclaveBuild(request) => request.handle(config).await, Request::EnclaveStart(request) => request.handle(config).await, } diff --git a/cli/src/handler/contract_tx.rs b/cli/src/handler/contract_tx.rs new file mode 100644 index 00000000..b2a530e7 --- /dev/null +++ b/cli/src/handler/contract_tx.rs @@ -0,0 +1,71 @@ +use async_trait::async_trait; +use cycles_sync::wasmd_client::{CliWasmdClient, WasmdClient}; +use reqwest::Url; +use serde_json::Value; +use tokio::time::{sleep, Duration}; +use tracing::info; + +use super::utils::types::WasmdTxResponse; +use crate::{ + error::Error, + handler::Handler, + request::contract_tx::ContractTxRequest, + response::{contract_tx::ContractTxResponse, Response}, + Config, +}; + +#[async_trait] +impl Handler for ContractTxRequest { + type Error = Error; + type Response = Response; + + async fn handle(self, _: Config) -> Result { + let tx_hash = tx(self) + .await + .map_err(|e| Error::GenericErr(e.to_string()))?; + + Ok(ContractTxResponse { tx_hash }.into()) + } +} + +async fn tx(args: ContractTxRequest) -> Result { + let httpurl = Url::parse(&format!("http://{}", args.node_url))?; + let wasmd_client = CliWasmdClient::new(Url::parse(httpurl.as_str())?); + + info!("🚀 Submitting Tx with msg: {}", args.msg); + + let tx_output: WasmdTxResponse = serde_json::from_str(&wasmd_client.tx_execute( + &args.contract, + &args.chain_id, + args.gas, + &args.sender, + args.msg, + args.amount, + )?)?; + + let hash = tx_output.txhash.to_string(); + info!("🚀 Successfully sent tx with hash {}", hash); + info!("Waiting 5 seconds for tx to be included in block....."); + + // TODO - a more robust timeout mechanism with polling blocks + sleep(Duration::from_secs(5)).await; + + // Query the transaction + let tx_result: Value = wasmd_client.query_tx(&hash)?; + + // Check if the transaction was successful, otherwise return raw log or error + if let Some(code) = tx_result["code"].as_u64() { + if code == 0 { + info!("Transaction was successful!"); + } else { + return Err(anyhow::anyhow!( + "Transaction failed. Inspect raw log: {}", + tx_result["raw_log"] + )); + } + } else { + return Err(anyhow::anyhow!("Unable to determine transaction status")); + } + + Ok(tx_output.txhash.to_string()) +} diff --git a/cli/src/handler/handshake.rs b/cli/src/handler/handshake.rs index ed9d6d12..e7a41aac 100644 --- a/cli/src/handler/handshake.rs +++ b/cli/src/handler/handshake.rs @@ -74,6 +74,7 @@ async fn handshake(args: HandshakeRequest, mock_sgx: bool) -> Result Result( Ok(query_result) } -// Note: time until tx commit is empiraclly 800ms on DO wasmd chain. +// Note: time until tx commit is empirically 800ms on DO wasmd chain. pub async fn block_tx_commit(client: &HttpClient, tx: Hash) -> Result { let re = Regex::new(r"tx \([A-F0-9]{64}\) not found")?; diff --git a/cli/src/request.rs b/cli/src/request.rs index 8a33389c..210e84a4 100644 --- a/cli/src/request.rs +++ b/cli/src/request.rs @@ -5,13 +5,14 @@ use crate::{ error::Error, request::{ contract_build::ContractBuildRequest, contract_deploy::ContractDeployRequest, - enclave_build::EnclaveBuildRequest, enclave_start::EnclaveStartRequest, - handshake::HandshakeRequest, init::InitRequest, + contract_tx::ContractTxRequest, enclave_build::EnclaveBuildRequest, + enclave_start::EnclaveStartRequest, handshake::HandshakeRequest, init::InitRequest, }, }; pub mod contract_build; pub mod contract_deploy; +pub mod contract_tx; pub mod enclave_build; pub mod enclave_start; pub mod handshake; @@ -23,6 +24,7 @@ pub enum Request { Handshake(HandshakeRequest), ContractBuild(ContractBuildRequest), ContractDeploy(ContractDeployRequest), + ContractTx(ContractTxRequest), EnclaveBuild(EnclaveBuildRequest), EnclaveStart(EnclaveStartRequest), } @@ -105,6 +107,24 @@ impl TryFrom for Request { Ok(ContractBuildRequest { manifest_path }.into()) } + ContractCommand::Tx { + node_url, + contract, + chain_id, + gas, + sender, + msg, + amount, + } => Ok(ContractTxRequest { + node_url, + contract, + chain_id, + gas, + sender, + msg, + amount, + } + .into()), } } } diff --git a/cli/src/request/contract_tx.rs b/cli/src/request/contract_tx.rs new file mode 100644 index 00000000..d7e13ede --- /dev/null +++ b/cli/src/request/contract_tx.rs @@ -0,0 +1,20 @@ +use cosmrs::{tendermint::chain::Id as ChainId, AccountId}; + +use crate::request::Request; + +#[derive(Clone, Debug)] +pub struct ContractTxRequest { + pub node_url: String, + pub contract: AccountId, + pub chain_id: ChainId, + pub gas: u64, + pub sender: String, + pub msg: String, + pub amount: Option, +} + +impl From for Request { + fn from(request: ContractTxRequest) -> Self { + Self::ContractTx(request) + } +} diff --git a/cli/src/response.rs b/cli/src/response.rs index aaa272db..dd981511 100644 --- a/cli/src/response.rs +++ b/cli/src/response.rs @@ -2,12 +2,13 @@ use serde::Serialize; use crate::response::{ contract_build::ContractBuildResponse, contract_deploy::ContractDeployResponse, - enclave_build::EnclaveBuildResponse, enclave_start::EnclaveStartResponse, - handshake::HandshakeResponse, init::InitResponse, + contract_tx::ContractTxResponse, enclave_build::EnclaveBuildResponse, + enclave_start::EnclaveStartResponse, handshake::HandshakeResponse, init::InitResponse, }; pub mod contract_build; pub mod contract_deploy; +pub mod contract_tx; pub mod enclave_build; pub mod enclave_start; pub mod handshake; @@ -19,6 +20,7 @@ pub enum Response { Handshake(HandshakeResponse), ContractBuild(ContractBuildResponse), ContractDeploy(ContractDeployResponse), + ContractTx(ContractTxResponse), EnclaveBuild(EnclaveBuildResponse), EnclaveStart(EnclaveStartResponse), } diff --git a/cli/src/response/contract_tx.rs b/cli/src/response/contract_tx.rs new file mode 100644 index 00000000..cd00910f --- /dev/null +++ b/cli/src/response/contract_tx.rs @@ -0,0 +1,14 @@ +use serde::Serialize; + +use crate::response::Response; + +#[derive(Clone, Debug, Serialize, Default)] +pub struct ContractTxResponse { + pub tx_hash: String, +} + +impl From for Response { + fn from(response: ContractTxResponse) -> Self { + Self::ContractTx(response) + } +} diff --git a/utils/cycles-sync/src/bin/submit.rs b/utils/cycles-sync/src/bin/submit.rs index a5f3d857..9624d805 100644 --- a/utils/cycles-sync/src/bin/submit.rs +++ b/utils/cycles-sync/src/bin/submit.rs @@ -105,7 +105,14 @@ async fn main() -> Result<(), anyhow::Error> { let wasmd_client = CliWasmdClient::new(node_url); - wasmd_client.tx_execute(&cli.mtcs, &chain_id, 3000000, &cli.admin.to_string(), msg)?; + wasmd_client.tx_execute( + &cli.mtcs, + &chain_id, + 3000000, + &cli.admin.to_string(), + msg, + None, + )?; Ok(()) } diff --git a/utils/cycles-sync/src/main.rs b/utils/cycles-sync/src/main.rs index d886a252..ddd3d88b 100644 --- a/utils/cycles-sync/src/main.rs +++ b/utils/cycles-sync/src/main.rs @@ -177,7 +177,7 @@ async fn sync_obligations( let msg = create_wasm_msg(intents_enc, liquidity_sources)?; let wasmd_client = CliWasmdClient::new(cli.node); - wasmd_client.tx_execute(&cli.contract, &cli.chain_id, 3000000, &cli.user, msg)?; + wasmd_client.tx_execute(&cli.contract, &cli.chain_id, 3000000, &cli.user, msg, None)?; Ok(()) } diff --git a/utils/cycles-sync/src/wasmd_client.rs b/utils/cycles-sync/src/wasmd_client.rs index d1c8b11a..cbc0df77 100644 --- a/utils/cycles-sync/src/wasmd_client.rs +++ b/utils/cycles-sync/src/wasmd_client.rs @@ -34,6 +34,7 @@ pub trait WasmdClient { gas: u64, sender: &str, msg: M, + amount: Option, ) -> Result; fn deploy( @@ -129,12 +130,23 @@ impl WasmdClient for CliWasmdClient { .arg(txhash) .args(["--output", "json"]); - let output = command.output()?; + let output = command + .output() + .map_err(|e| anyhow!("Failed to execute wasmd command: {}", e))?; + if !output.status.success() { - return Err(anyhow!("{:?}", output)); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(anyhow!( + "Error querying tx {}\nCommand failed with status {:?}\nStderr: {}", + txhash, + output.status, + stderr + )); } - let query_result: R = serde_json::from_slice(&output.stdout).unwrap_or_default(); + let query_result: R = serde_json::from_slice(&output.stdout) + .map_err(|e| anyhow!("Error parsing query result for tx {}: {}", txhash, e))?; + Ok(query_result) } @@ -145,9 +157,10 @@ impl WasmdClient for CliWasmdClient { gas: u64, sender: &str, msg: M, + amount: Option, ) -> Result { let mut wasmd = Command::new("wasmd"); - let command = wasmd + let mut command = wasmd .args(["--node", self.url.as_str()]) .args(["--chain-id", chain_id.as_ref()]) .args(["tx", "wasm"]) @@ -157,6 +170,11 @@ impl WasmdClient for CliWasmdClient { .args(["--output", "json"]) .arg("-y"); + // Add amount if provided + if let Some(amt) = amount { + command = command.arg("--amount").arg(amt); + } + let output = command.output()?; if !output.status.success() {