use std::array::TryFromSliceError;
use std::ffi::OsString;
use std::fmt::Debug;
use std::num::ParseIntError;
use clap::Parser;
use rand::Rng;
use soroban_spec_tools::contract as contract_spec;
use crate::config::address::AliasName;
use crate::resources;
use crate::tx::sim_sign_and_send_tx;
use crate::xdr::{
AccountId, ContractExecutable, ContractIdPreimage, ContractIdPreimageFromAddress,
CreateContractArgs, CreateContractArgsV2, Error as XdrError, Hash, HostFunction,
InvokeContractArgs, InvokeHostFunctionOp, Limits, Memo, MuxedAccount, Operation, OperationBody,
Preconditions, PublicKey, ScAddress, SequenceNumber, Transaction, TransactionExt, Uint256,
VecM, WriteXdr,
};
use crate::commands::tx::fetch;
use crate::{
commands::{
contract::{self, arg_parsing, build, id::wasm::get_contract_id, upload},
global,
txn_result::{TxnEnvelopeResult, TxnResult},
},
config::{self, data, locator, network},
print::Print,
rpc,
utils::{self, rpc::get_remote_wasm_from_hash},
wasm,
};
pub const CONSTRUCTOR_FUNCTION_NAME: &str = "__constructor";
#[derive(Parser, Debug, Clone)]
#[command(group(
clap::ArgGroup::new("wasm_src")
.required(false)
.args(&["wasm", "wasm_hash"]),
))]
#[group(skip)]
pub struct Cmd {
#[arg(long, group = "wasm_src")]
pub wasm: Option<std::path::PathBuf>,
#[arg(long = "wasm-hash", conflicts_with = "wasm", group = "wasm_src")]
pub wasm_hash: Option<String>,
#[arg(long)]
pub salt: Option<String>,
#[command(flatten)]
pub config: config::Args,
#[arg(long, short = 'i', default_value = "false")]
pub ignore_checks: bool,
#[arg(long)]
pub alias: Option<AliasName>,
#[command(flatten)]
pub resources: resources::Args,
#[arg(long)]
pub build_only: bool,
#[arg(last = true, id = "CONTRACT_CONSTRUCTOR_ARGS")]
pub slop: Vec<OsString>,
#[arg(long, help_heading = "Build Options", conflicts_with = "wasm_src")]
pub package: Option<String>,
#[command(flatten)]
pub build_args: build::BuildArgs,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Install(#[from] upload::Error),
#[error("error parsing int: {0}")]
ParseIntError(#[from] ParseIntError),
#[error("internal conversion error: {0}")]
TryFromSliceError(#[from] TryFromSliceError),
#[error("xdr processing error: {0}")]
Xdr(#[from] XdrError),
#[error("jsonrpc error: {0}")]
JsonRpc(#[from] jsonrpsee_core::Error),
#[error("cannot parse salt: {salt}")]
CannotParseSalt { salt: String },
#[error("cannot parse contract ID {contract_id}: {error}")]
CannotParseContractId {
contract_id: String,
error: stellar_strkey::DecodeError,
},
#[error("cannot parse WASM hash {wasm_hash}: {error}")]
CannotParseWasmHash {
wasm_hash: String,
error: stellar_strkey::DecodeError,
},
#[error("Must provide either --wasm or --wasm-hash")]
WasmNotProvided,
#[error(transparent)]
Rpc(#[from] rpc::Error),
#[error(transparent)]
Config(#[from] config::Error),
#[error(transparent)]
StrKey(#[from] stellar_strkey::DecodeError),
#[error(transparent)]
Infallible(#[from] std::convert::Infallible),
#[error(transparent)]
WasmId(#[from] contract::id::wasm::Error),
#[error(transparent)]
Data(#[from] data::Error),
#[error(transparent)]
Network(#[from] network::Error),
#[error(transparent)]
Wasm(#[from] wasm::Error),
#[error(transparent)]
Locator(#[from] locator::Error),
#[error(transparent)]
ContractSpec(#[from] contract_spec::Error),
#[error(transparent)]
ArgParse(#[from] arg_parsing::Error),
#[error("Only ed25519 accounts are allowed")]
OnlyEd25519AccountsAllowed,
#[error(transparent)]
Fee(#[from] fetch::fee::Error),
#[error(transparent)]
Fetch(#[from] fetch::Error),
#[error(transparent)]
Build(#[from] build::Error),
#[error("no buildable contracts found in workspace (no packages with crate-type cdylib)")]
NoBuildableContracts,
#[error("--alias is not supported when deploying multiple contracts; aliases are derived from package names automatically")]
AliasNotSupported,
#[error("--salt is not supported when deploying multiple contracts")]
SaltNotSupported,
#[error("constructor arguments are not supported when deploying multiple contracts")]
ConstructorArgsNotSupported,
#[error("--build-only is not supported without --wasm or --wasm-hash")]
BuildOnlyNotSupported,
#[error(
"--wasm or --wasm-hash is required when not in a Cargo workspace; no Cargo.toml found"
)]
NotInCargoProject,
}
impl Cmd {
pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
if self.build_only && self.wasm.is_none() && self.wasm_hash.is_none() {
return Err(Error::BuildOnlyNotSupported);
}
let built_contracts = self.resolve_contracts(global_args)?;
if built_contracts.is_empty() {
Self::run_single(self, global_args).await?;
} else {
if built_contracts.len() > 1 {
if self.alias.is_some() {
return Err(Error::AliasNotSupported);
}
if self.salt.is_some() {
return Err(Error::SaltNotSupported);
}
if !self.slop.is_empty() {
return Err(Error::ConstructorArgsNotSupported);
}
}
for contract in &built_contracts {
let mut cmd = self.clone();
cmd.wasm = Some(contract.path.clone());
if cmd.alias.is_none() && !contract.name.is_empty() {
if let Ok(alias) = contract.name.parse::<AliasName>() {
cmd.alias = Some(alias);
}
}
Self::run_single(&cmd, global_args).await?;
}
}
Ok(())
}
async fn run_single(cmd: &Cmd, global_args: &global::Args) -> Result<(), Error> {
let res = cmd
.execute(&cmd.config, global_args.quiet, global_args.no_cache)
.await?
.to_envelope();
match res {
TxnEnvelopeResult::TxnEnvelope(tx) => {
println!("{}", tx.to_xdr_base64(Limits::none())?);
}
TxnEnvelopeResult::Res(contract) => {
let network = cmd.config.get_network()?;
if let Some(alias) = cmd.alias.clone() {
if let Some(existing_contract) = cmd
.config
.locator
.get_contract_id(&alias, &network.network_passphrase)?
{
let print = Print::new(global_args.quiet);
print.warnln(format!(
"Overwriting existing alias {alias:?} that currently links to contract ID: {existing_contract}"
));
}
cmd.config.locator.save_contract_id(
&network.network_passphrase,
&contract,
&alias,
)?;
}
println!("{contract}");
}
}
Ok(())
}
fn resolve_contracts(
&self,
global_args: &global::Args,
) -> Result<Vec<build::BuiltContract>, Error> {
if let Some(wasm) = &self.wasm {
return Ok(vec![build::BuiltContract {
name: String::new(),
path: wasm.clone(),
}]);
}
if self.wasm_hash.is_some() {
return Ok(vec![]);
}
let build_cmd = build::Cmd {
package: self.package.clone(),
build_args: self.build_args.clone(),
..build::Cmd::default()
};
let contracts = build_cmd.run(global_args).map_err(|e| match e {
build::Error::Metadata(_) => Error::NotInCargoProject,
other => other.into(),
})?;
if contracts.is_empty() {
return Err(Error::NoBuildableContracts);
}
Ok(contracts)
}
#[allow(clippy::too_many_lines)]
#[allow(unused_variables)]
pub async fn execute(
&self,
config: &config::Args,
quiet: bool,
no_cache: bool,
) -> Result<TxnResult<stellar_strkey::Contract>, Error> {
let print = Print::new(quiet);
let wasm_hash = if let Some(wasm) = &self.wasm {
let is_build = self.build_only;
let hash = if is_build {
wasm::Args { wasm: wasm.clone() }.hash()?
} else {
print.infoln("Uploading contract WASM…");
upload::Cmd {
wasm: Some(wasm.clone()),
config: config.clone(),
resources: self.resources.clone(),
ignore_checks: self.ignore_checks,
build_only: is_build,
package: None,
build_args: build::BuildArgs::default(),
}
.execute(config, quiet, no_cache)
.await?
.into_result()
.expect("the value (hash) is expected because it should always be available since build-only is a shared parameter")
};
hex::encode(hash)
} else {
self.wasm_hash
.as_ref()
.ok_or(Error::WasmNotProvided)?
.clone()
};
let wasm_hash = Hash(
utils::contract_id_from_str(&wasm_hash)
.map_err(|e| Error::CannotParseWasmHash {
wasm_hash: wasm_hash.clone(),
error: e,
})?
.0,
);
print.infoln(format!("Deploying contract using wasm hash {wasm_hash}").as_str());
let network = config.get_network()?;
let salt: [u8; 32] = match &self.salt {
Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32)
.map_err(|_| Error::CannotParseSalt { salt: h.clone() })?
.try_into()
.map_err(|_| Error::CannotParseSalt { salt: h.clone() })?,
None => rand::thread_rng().gen::<[u8; 32]>(),
};
let client = network.rpc_client()?;
let MuxedAccount::Ed25519(bytes) = config.source_account().await? else {
return Err(Error::OnlyEd25519AccountsAllowed);
};
let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(bytes));
let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
address: ScAddress::Account(source_account.clone()),
salt: Uint256(salt),
});
let contract_id =
get_contract_id(contract_id_preimage.clone(), &network.network_passphrase)?;
let raw_wasm = if let Some(wasm) = self.wasm.as_ref() {
wasm::Args { wasm: wasm.clone() }.read()?
} else {
if self.build_only {
return Err(Error::WasmNotProvided);
}
get_remote_wasm_from_hash(&client, &wasm_hash).await?
};
let entries = soroban_spec_tools::contract::Spec::new(&raw_wasm)?.spec;
let res = soroban_spec_tools::Spec::new(entries.clone().as_slice());
let constructor_params = if let Ok(func) = res.find_function(CONSTRUCTOR_FUNCTION_NAME) {
if func.inputs.is_empty() {
None
} else {
let mut slop = vec![OsString::from(CONSTRUCTOR_FUNCTION_NAME)];
slop.extend_from_slice(&self.slop);
Some(
arg_parsing::build_constructor_parameters(
&stellar_strkey::Contract(contract_id.0),
&slop,
&entries,
config,
)
.await?
.2,
)
}
} else {
None
};
client
.verify_network_passphrase(Some(&network.network_passphrase))
.await?;
let account_details = client.get_account(&source_account.to_string()).await?;
let sequence: i64 = account_details.seq_num.into();
let txn = Box::new(build_create_contract_tx(
wasm_hash,
sequence + 1,
config.get_inclusion_fee()?,
source_account,
contract_id_preimage,
constructor_params.as_ref(),
)?);
if self.build_only {
print.checkln("Transaction built!");
return Ok(TxnResult::Txn(txn));
}
sim_sign_and_send_tx::<Error>(&client, &txn, config, &self.resources, &[], quiet, no_cache)
.await?;
if let Some(url) = utils::lab_url_for_contract(&network, &contract_id) {
print.linkln(url);
}
print.checkln("Deployed!");
Ok(TxnResult::Res(contract_id))
}
}
fn build_create_contract_tx(
wasm_hash: Hash,
sequence: i64,
fee: u32,
key: AccountId,
contract_id_preimage: ContractIdPreimage,
constructor_params: Option<&InvokeContractArgs>,
) -> Result<Transaction, Error> {
let op = if let Some(InvokeContractArgs { args, .. }) = constructor_params {
Operation {
source_account: None,
body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
contract_id_preimage,
executable: ContractExecutable::Wasm(wasm_hash),
constructor_args: args.clone(),
}),
auth: VecM::default(),
}),
}
} else {
Operation {
source_account: None,
body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
host_function: HostFunction::CreateContract(CreateContractArgs {
contract_id_preimage,
executable: ContractExecutable::Wasm(wasm_hash),
}),
auth: VecM::default(),
}),
}
};
let tx = Transaction {
source_account: key.into(),
fee,
seq_num: SequenceNumber(sequence),
cond: Preconditions::None,
memo: Memo::None,
operations: vec![op].try_into()?,
ext: TransactionExt::V0,
};
Ok(tx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_create_contract() {
let hash = hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
.unwrap()
.try_into()
.unwrap();
let salt = [0u8; 32];
let key =
&utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP")
.unwrap();
let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
key.verifying_key().to_bytes(),
)));
let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
address: ScAddress::Account(source_account.clone()),
salt: Uint256(salt),
});
let result = build_create_contract_tx(
Hash(hash),
300,
1,
source_account,
contract_id_preimage,
None,
);
assert!(result.is_ok());
}
}