From 91fed12b45ad6e8ee6f85e64c255c13f32b26b24 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 19 Mar 2024 15:54:20 -0400 Subject: [PATCH] WIP: transaction builder --- .../src/commands/contract/install.rs | 22 +- cmd/soroban-cli/src/commands/txn/mod.rs | 31 +++ cmd/soroban-cli/src/commands/txn/token/mod.rs | 38 ++++ cmd/soroban-cli/src/lib.rs | 1 + cmd/soroban-cli/src/txn.rs | 200 ++++++++++++++++++ 5 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 cmd/soroban-cli/src/commands/txn/mod.rs create mode 100644 cmd/soroban-cli/src/commands/txn/token/mod.rs create mode 100644 cmd/soroban-cli/src/txn.rs diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 47a47e1d2f..f6986e3309 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -2,6 +2,7 @@ use std::array::TryFromSliceError; use std::fmt::Debug; use std::num::ParseIntError; +use cargo_metadata::semver::Op; use clap::{command, Parser}; use soroban_env_host::xdr::{ self, Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp, Memo, MuxedAccount, @@ -11,9 +12,10 @@ use soroban_env_host::xdr::{ use super::restore; use crate::commands::{global, NetworkRunnable}; -use crate::key; use crate::rpc::{self, Client}; +use crate::txn::{InvokeHostFunctionOpBuilder, OperationBuilder, TransactionBuilder}; use crate::{commands::config, utils, wasm}; +use crate::{key, txn}; const CONTRACT_META_SDK_KEY: &str = "rssdkver"; const PUBLIC_NETWORK_PASSPHRASE: &str = "Public Global Stellar Network ; September 2015"; @@ -45,6 +47,8 @@ pub enum Error { #[error(transparent)] Rpc(#[from] rpc::Error), #[error(transparent)] + TxnBuilder(#[from] txn::Error), + #[error(transparent)] Config(#[from] config::Error), #[error(transparent)] Wasm(#[from] wasm::Error), @@ -190,6 +194,22 @@ pub(crate) fn build_install_contract_code_tx( fee: u32, key: &ed25519_dalek::SigningKey, ) -> Result<(Transaction, Hash), XdrError> { + let source_account = stellar_strkey::Strkey::PublicKeyEd25519( + stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes().try_into()?), + ); + let mut txn = TransactionBuilder::new(source_account.clone())?; + let op = OperationBuilder::new() + .set_source_account(&source_account) + .set_host_function(HostFunction::UploadContractWasm(source_code.try_into()?)) + .build(); + + let op = InvokeHostFunctionOpBuilder::upload(source_code)?.build()? + let op_body = OperationBuilder::new() + .set_source_account(&txn.txn.source_account) + .set_body(op) + .build(); + + let hash = utils::contract_hash(source_code)?; let op = Operation { diff --git a/cmd/soroban-cli/src/commands/txn/mod.rs b/cmd/soroban-cli/src/commands/txn/mod.rs new file mode 100644 index 0000000000..f405efe6d7 --- /dev/null +++ b/cmd/soroban-cli/src/commands/txn/mod.rs @@ -0,0 +1,31 @@ +use clap::Subcommand; +use stellar_xdr::cli as xdr; + +pub mod token; + +#[derive(Debug, Subcommand)] +pub enum Cmd { + /// Wrap, create, and manage token contracts + Token(token::Root), + + /// Decode xdr + Xdr(xdr::Root), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Token(#[from] token::Error), + #[error(transparent)] + Xdr(#[from] xdr::Error), +} + +impl Cmd { + pub async fn run(&self) -> Result<(), Error> { + match &self { + Cmd::Token(token) => token.run().await?, + Cmd::Xdr(xdr) => xdr.run()?, + } + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/txn/token/mod.rs b/cmd/soroban-cli/src/commands/txn/token/mod.rs new file mode 100644 index 0000000000..bd7eacf36e --- /dev/null +++ b/cmd/soroban-cli/src/commands/txn/token/mod.rs @@ -0,0 +1,38 @@ +use std::fmt::Debug; + +use crate::commands::contract::{deploy, id}; +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug)] +pub struct Root { + #[clap(subcommand)] + cmd: Cmd, +} + +#[derive(Subcommand, Debug)] +enum Cmd { + /// Deploy a token contract to wrap an existing Stellar classic asset for smart contract usage + /// Deprecated, use `soroban contract deploy asset` instead + Wrap(deploy::asset::Cmd), + /// Compute the expected contract id for the given asset + /// Deprecated, use `soroban contract id asset` instead + Id(id::asset::Cmd), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Wrap(#[from] deploy::asset::Error), + #[error(transparent)] + Id(#[from] id::asset::Error), +} + +impl Root { + pub async fn run(&self) -> Result<(), Error> { + match &self.cmd { + Cmd::Wrap(wrap) => wrap.run().await?, + Cmd::Id(id) => id.run()?, + } + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index ef443853bc..01b8957e83 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -11,6 +11,7 @@ pub mod fee; pub mod key; pub mod log; pub mod toid; +pub mod txn; pub mod utils; pub mod wasm; diff --git a/cmd/soroban-cli/src/txn.rs b/cmd/soroban-cli/src/txn.rs new file mode 100644 index 0000000000..d7d1808dcd --- /dev/null +++ b/cmd/soroban-cli/src/txn.rs @@ -0,0 +1,200 @@ +//! This module contains a transaction builder for Stellar. +//! +use soroban_env_host::xdr::{self, Operation, Transaction, Uint256, VecM}; + + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("")] + InvokeHostFunctionOpMustBeOnlyOperation, + #[error(transparent)] + Xdr(#[from] xdr::Error), + #[error("invalid source account strkey type")] + InvalidSourceAccountStrkeyType, +} + +fn to_muxed_account(source_account: stellar_strkey::Strkey) -> Result { + let raw_bytes = match source_account { + stellar_strkey::Strkey::PublicKeyEd25519(x) => x.0, + stellar_strkey::Strkey::MuxedAccountEd25519(x) => x.ed25519, + _ => return Err(Error::InvalidSourceAccountStrkeyType), + }; + Ok(xdr::MuxedAccount::Ed25519(xdr::Uint256(raw_bytes))) +} + +pub struct TransactionBuilder { + pub txn: Transaction, +} + +impl TransactionBuilder { + pub fn new(source_account: stellar_strkey::Strkey) -> Result { + let source_account = to_muxed_account(source_account)?; + Ok(Self { + txn: Transaction { + source_account, + fee: 100, + operations: VecM::default(), + seq_num: xdr::SequenceNumber(0), + cond: xdr::Preconditions::None, + memo: xdr::Memo::None, + ext: xdr::TransactionExt::V0, + }, + }) + } + + pub fn set_source_account(&mut self, source_account: stellar_strkey::Strkey) -> Result<&mut Self, Error> { + self.txn.source_account = to_muxed_account(source_account)?; + Ok(self) + } + + pub fn set_fee(&mut self, fee: u32) -> &mut Self { + self.txn.fee = fee; + self + } + + pub fn set_sequence_number(&mut self, sequence_number: i64) -> &mut Self { + self.txn.seq_num = xdr::SequenceNumber(sequence_number); + self + } + + pub fn add_operation(&mut self, operation: Operation) -> Result<&mut Self, Error> { + if !self.txn.operations.is_empty() + && matches!( + operation, + Operation { + body: xdr::OperationBody::InvokeHostFunction(_), + .. + } + ) + { + return Err(Error::InvokeHostFunctionOpMustBeOnlyOperation); + } + self.txn.operations.push(operation); + Ok(self) + } + + pub fn cond(&mut self, cond: xdr::Preconditions) -> &mut Self { + self.txn.cond = cond; + self + } + + pub fn build(&self) -> Transaction { + self.txn.clone() + } +} + +pub struct OperationBuilder { + op: Operation, +} + +impl OperationBuilder { + pub fn new() -> Self { + Self { + op: Operation { + source_account: None, + body: xdr::OperationBody::Inflation, + }, + } + } + + pub fn set_source_account(&mut self, source_account: stellar_strkey::Strkey) -> Result<&mut Self, Error> { + self.op.source_account = Some(to_muxed_account(source_account)?); + Ok(self) + } + + pub fn set_body(&mut self, body: xdr::OperationBody) -> &mut Self { + self.op.body = body; + self + } + + pub fn set_host_function(&mut self, host_function: xdr::HostFunction) -> &mut Self { + if let xdr::OperationBody::InvokeHostFunction(ref mut op) = self.op.body { + op.host_function = host_function; + } + self + } + + pub fn set_auth(&mut self, auth: VecM) -> &mut Self { + if let xdr::OperationBody::InvokeHostFunction(ref mut op) = self.op.body { + op.auth = auth; + } + self + } + + pub fn build(&self) -> Operation { + self.op.clone() + } +} + +pub struct OperationBodyBuilder { + body: xdr::OperationBody, +} + +impl OperationBodyBuilder { + pub fn new() -> Self { + Self { + body: xdr::OperationBody::Inflation, + } + } + + pub fn set_invoke_host_function(&mut self, invoke_host_function: xdr::InvokeHostFunctionOp) -> &mut Self { + self.body = xdr::OperationBody::InvokeHostFunction(invoke_host_function); + self + } + + pub fn build(&self) -> xdr::OperationBody { + self.body.clone() + } +} + +pub struct InvokeHostFunctionOpBuilder(xdr::HostFunction, Vec); + +impl InvokeHostFunctionOpBuilder { + fn new(host_function: xdr::HostFunction) -> Self { + Self(host_function, vec![]) + } + pub fn upload(wasm: &[u8]) -> Result { + Ok(Self::new(xdr::HostFunction::UploadContractWasm( + wasm.try_into()?, + ))) + } + + pub fn create_contract( + source_account: stellar_strkey::Strkey, + salt: [u8; 32], + wasm_hash: xdr::Hash, + ) -> Result { + let stellar_strkey::Strkey::PublicKeyEd25519(bytes) = source_account else { + panic!("Invalid public key"); + }; + + let contract_id_preimage = + xdr::ContractIdPreimage::Address(xdr::ContractIdPreimageFromAddress { + address: xdr::ScAddress::Account(xdr::AccountId( + xdr::PublicKey::PublicKeyTypeEd25519(bytes.0.into()), + )), + salt: Uint256(salt), + }); + + Ok(Self::new(xdr::HostFunction::CreateContract( + xdr::CreateContractArgs { + contract_id_preimage, + executable: xdr::ContractExecutable::Wasm(wasm_hash), + }, + ))) + } + + pub fn add_auth(&mut self, auth: xdr::SorobanAuthorizationEntry) -> &mut Self { + self.1.push(auth); + self + } + + pub fn build(self) -> Result { + Ok(xdr::OperationBody::InvokeHostFunction( + xdr::InvokeHostFunctionOp { + host_function: self.0, + auth: self.1.try_into()?, + }, + )) + } +}