use crate::error::RpcError;
use crate::simulate::simulate_transaction;
use crate::submission::{submit_and_wait, PollConfig, TransactionStatus};
use crate::SorobanRpcClient;
use sdkt_xdr::sign::{Ed25519Signer, Network, SigningOptions};
use sdkt_xdr::sign_transaction;
use stellar_xdr::{
LedgerFootprint, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt, VecM,
};
#[derive(Debug, Clone, PartialEq)]
pub struct DeployResult {
pub wasm_hash: String,
pub contract_id: String,
pub upload_hash: String,
pub create_hash: String,
pub status: String,
pub salt: String,
pub upload_fee: u32,
pub create_fee: u32,
pub total_fee: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PartialDeployResult {
pub wasm_hash: String,
pub upload_hash: String,
pub error: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DeployOutcome {
Success(DeployResult),
Partial(PartialDeployResult),
Failure(String),
}
impl std::fmt::Display for DeployOutcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DeployOutcome::Success(r) => {
write!(
f,
"Deployment successful!\n WASM Hash: {}\n Contract ID: {}\n Upload TX: {}\n Create TX: {}\n Salt: {}\n Status: {}\n Upload Fee: {}\n Create Fee: {}\n Total Fee: {}",
r.wasm_hash, r.contract_id, r.upload_hash, r.create_hash, r.salt, r.status, r.upload_fee, r.create_fee, r.total_fee
)
}
DeployOutcome::Partial(p) => {
write!(
f,
"Partial deployment: upload succeeded but create failed.\n WASM Hash: {}\n Upload TX: {}\n Error: {}",
p.wasm_hash, p.upload_hash, p.error
)
}
DeployOutcome::Failure(e) => {
write!(f, "Deployment failed: {}", e)
}
}
}
}
pub fn parse_wasm_hash(hash_str: &str) -> Result<[u8; 32], RpcError> {
let bytes = hex::decode(hash_str)
.map_err(|e| RpcError::Rpc(format!("Invalid WASM hash hex: {}", e)))?;
if bytes.len() != 32 {
return Err(RpcError::Rpc("WASM hash must be 32 bytes".into()));
}
let mut result = [0u8; 32];
result.copy_from_slice(&bytes);
Ok(result)
}
fn parse_min_resource_fee(raw: &str) -> Result<u32, RpcError> {
let trimmed = raw.trim();
let value: u64 = trimmed.parse().map_err(|_| {
RpcError::Rpc(format!(
"Simulation returned an invalid min_resource_fee: {trimmed:?} \
(expected a numeric stroop value)"
))
})?;
u32::try_from(value).map_err(|_| {
RpcError::Rpc(format!(
"Simulation min_resource_fee {value} exceeds the supported u32 fee range"
))
})
}
pub fn generate_salt() -> [u8; 20] {
let mut salt = [0u8; 20];
getrandom::fill(&mut salt).expect("Failed to generate random salt");
salt
}
pub async fn upload_wasm(
client: &SorobanRpcClient,
wasm_bytes: &[u8],
source_account: &str,
sequence: i64,
fee: u32,
network: Network,
signer: &Ed25519Signer,
) -> Result<(String, String, u32), RpcError> {
use sdkt_xdr::builder::UploadWasmParams;
if wasm_bytes.is_empty() {
return Err(RpcError::Rpc("WASM bytes are empty".into()));
}
let meta = sdkt_wasm::parse_metadata(wasm_bytes)
.map_err(|e| RpcError::Rpc(format!("Failed to parse WASM metadata: {}", e)))?;
let wasm_hash = meta.hash.clone();
let initial_soroban_data = SorobanTransactionData {
ext: SorobanTransactionDataExt::V0,
resources: SorobanResources {
footprint: LedgerFootprint {
read_only: VecM::default(),
read_write: VecM::default(),
},
instructions: 0,
disk_read_bytes: 0,
write_bytes: 0,
},
resource_fee: 0,
};
let initial_envelope = sdkt_xdr::builder::build_upload_wasm_tx_with_data(
&UploadWasmParams {
source_account: source_account.to_string(),
sequence,
fee,
wasm_bytes: wasm_bytes.to_vec(),
},
initial_soroban_data,
)
.map_err(|e| RpcError::Rpc(format!("Failed to build upload transaction: {}", e)))?;
let simulation = simulate_transaction(client, &initial_envelope)
.await
.map_err(|e| RpcError::Rpc(format!("Upload simulation failed: {}", e)))?;
if let Some(err) = &simulation.error {
return Err(RpcError::Rpc(format!("Upload simulation error: {}", err)));
}
let soroban_data = if simulation.transaction_data.is_empty() {
return Err(RpcError::Rpc(
"Simulation did not return SorobanTransactionData".into(),
));
} else {
sdkt_xdr::builder::parse_soroban_transaction_data(&simulation.transaction_data)
.map_err(|e| RpcError::Rpc(format!("Failed to parse SorobanTransactionData: {}", e)))?
};
let min_resource_fee: u32 = parse_min_resource_fee(&simulation.min_resource_fee)?;
let inclusion_fee: u32 = 100;
let total_fee = inclusion_fee + min_resource_fee;
let auth_entries = if simulation.results.is_empty() {
Vec::new()
} else {
sdkt_xdr::builder::parse_soroban_authorization_entries(&simulation.results[0].auth)
.map_err(|e| RpcError::Rpc(format!("Failed to parse auth entries: {}", e)))?
};
let final_envelope = sdkt_xdr::builder::build_upload_wasm_tx_with_data_and_auth(
&UploadWasmParams {
source_account: source_account.to_string(),
sequence,
fee: total_fee,
wasm_bytes: wasm_bytes.to_vec(),
},
soroban_data,
auth_entries,
)
.map_err(|e| RpcError::Rpc(format!("Failed to build final upload transaction: {}", e)))?;
let signing_opts = SigningOptions::with(network.clone());
let signed_envelope = sign_transaction(&final_envelope, signer, &signing_opts)
.map_err(|e| RpcError::Rpc(format!("Failed to sign upload transaction: {}", e)))?;
let submission_result =
submit_and_wait(client, &signed_envelope, true, &PollConfig::default()).await?;
if submission_result.status != TransactionStatus::Success {
let diag = if let Some(ref xdr) = submission_result.error_result_xdr {
format!(" | error_result_xdr={}", xdr)
} else {
String::new()
};
let events = if !submission_result.diagnostic_events.is_empty() {
format!(
" | diagnostic_events={:?}",
submission_result.diagnostic_events
)
} else {
String::new()
};
let code = submission_result.error_code.as_deref().unwrap_or("unknown");
return Err(RpcError::Rpc(format!(
"Upload transaction failed: code={}{}{}",
code, diag, events
)));
}
Ok((wasm_hash, submission_result.hash, total_fee))
}
pub async fn create_contract(
client: &SorobanRpcClient,
args: &CreateContractArgs,
sequence: i64,
fee: u32,
network: Network,
signer: &Ed25519Signer,
) -> Result<(String, String, u32), RpcError> {
use sdkt_xdr::builder::{
build_create_contract_tx_with_data, build_create_contract_tx_with_data_and_auth,
build_create_contract_v2_tx_with_data, build_create_contract_v2_tx_with_data_and_auth,
CreateContractParams, CreateContractV2Params,
};
let initial_soroban_data = SorobanTransactionData {
ext: SorobanTransactionDataExt::V0,
resources: SorobanResources {
footprint: LedgerFootprint {
read_only: VecM::default(),
read_write: VecM::default(),
},
instructions: 0,
disk_read_bytes: 0,
write_bytes: 0,
},
resource_fee: 0,
};
let initial_envelope = if args.constructor_args.is_empty() {
build_create_contract_tx_with_data(
&CreateContractParams {
source_account: args.deployer_address.clone(),
sequence,
fee,
wasm_hash: args.wasm_hash,
deployer_address: args.deployer_address.clone(),
salt: args.salt,
},
initial_soroban_data,
)
.map_err(|e| RpcError::Rpc(format!("Failed to build create transaction: {}", e)))?
} else {
build_create_contract_v2_tx_with_data(
&CreateContractV2Params {
source_account: args.deployer_address.clone(),
sequence,
fee,
wasm_hash: args.wasm_hash,
deployer_address: args.deployer_address.clone(),
salt: args.salt,
constructor_args: args.constructor_args.clone(),
},
initial_soroban_data,
)
.map_err(|e| RpcError::Rpc(format!("Failed to build create v2 transaction: {}", e)))?
};
let simulation = simulate_transaction(client, &initial_envelope)
.await
.map_err(|e| RpcError::Rpc(format!("Create simulation failed: {}", e)))?;
if let Some(err) = &simulation.error {
return Err(RpcError::Rpc(format!("Create simulation error: {}", err)));
}
let soroban_data = if simulation.transaction_data.is_empty() {
return Err(RpcError::Rpc(
"Simulation did not return SorobanTransactionData".into(),
));
} else {
sdkt_xdr::builder::parse_soroban_transaction_data(&simulation.transaction_data)
.map_err(|e| RpcError::Rpc(format!("Failed to parse SorobanTransactionData: {}", e)))?
};
let min_resource_fee: u32 = parse_min_resource_fee(&simulation.min_resource_fee)?;
let inclusion_fee: u32 = 100;
let total_fee = inclusion_fee + min_resource_fee;
let auth_entries = if simulation.results.is_empty() {
Vec::new()
} else {
sdkt_xdr::builder::parse_soroban_authorization_entries(&simulation.results[0].auth)
.map_err(|e| RpcError::Rpc(format!("Failed to parse auth entries: {}", e)))?
};
let final_envelope = if args.constructor_args.is_empty() {
build_create_contract_tx_with_data_and_auth(
&CreateContractParams {
source_account: args.deployer_address.clone(),
sequence,
fee: total_fee,
wasm_hash: args.wasm_hash,
deployer_address: args.deployer_address.clone(),
salt: args.salt,
},
soroban_data,
auth_entries,
)
.map_err(|e| RpcError::Rpc(format!("Failed to build final create transaction: {}", e)))?
} else {
build_create_contract_v2_tx_with_data_and_auth(
&CreateContractV2Params {
source_account: args.deployer_address.clone(),
sequence,
fee: total_fee,
wasm_hash: args.wasm_hash,
deployer_address: args.deployer_address.clone(),
salt: args.salt,
constructor_args: args.constructor_args.clone(),
},
soroban_data,
auth_entries,
)
.map_err(|e| {
RpcError::Rpc(format!(
"Failed to build final create v2 transaction: {}",
e
))
})?
};
let signing_opts = SigningOptions::with(network.clone());
let signed_envelope = sign_transaction(&final_envelope, signer, &signing_opts)
.map_err(|e| RpcError::Rpc(format!("Failed to sign create transaction: {}", e)))?;
let submission_result =
submit_and_wait(client, &signed_envelope, true, &PollConfig::default()).await?;
if submission_result.status != TransactionStatus::Success {
let diag = if let Some(ref xdr) = submission_result.error_result_xdr {
format!(" | error_result_xdr={}", xdr)
} else {
String::new()
};
let events = if !submission_result.diagnostic_events.is_empty() {
format!(
" | diagnostic_events={:?}",
submission_result.diagnostic_events
)
} else {
String::new()
};
let code = submission_result.error_code.as_deref().unwrap_or("unknown");
return Err(RpcError::Rpc(format!(
"Create transaction failed: code={}{}{}",
code, diag, events
)));
}
let network_id = network.network_id();
let contract_id = sdkt_xdr::builder::derive_contract_id(
&network_id,
&args.deployer_address,
&args.salt,
&args.wasm_hash,
)
.map_err(|e| RpcError::Rpc(format!("Failed to derive contract ID: {}", e)))?;
Ok((contract_id, submission_result.hash, total_fee))
}
pub async fn deploy_contract(
client: &SorobanRpcClient,
wasm_bytes: &[u8],
source_account: &str,
signer: &Ed25519Signer,
network: Network,
user_salt: Option<[u8; 20]>,
) -> Result<DeployOutcome, RpcError> {
deploy_contract_with_args(
client,
wasm_bytes,
source_account,
signer,
network,
user_salt,
Vec::new(),
)
.await
}
pub async fn deploy_contract_with_args(
client: &SorobanRpcClient,
wasm_bytes: &[u8],
source_account: &str,
signer: &Ed25519Signer,
network: Network,
user_salt: Option<[u8; 20]>,
constructor_args: Vec<String>,
) -> Result<DeployOutcome, RpcError> {
use crate::account::get_next_sequence;
if wasm_bytes.is_empty() {
return Err(RpcError::Rpc("WASM bytes are empty".into()));
}
sdkt_xdr::parse_scval_args(&constructor_args)
.map_err(|e| RpcError::Rpc(format!("Invalid constructor argument: {}", e)))?;
let meta = sdkt_wasm::parse_metadata(wasm_bytes)
.map_err(|e| RpcError::Rpc(format!("Failed to parse WASM metadata: {}", e)))?;
let wasm_hash_str = meta.hash.clone();
let wasm_hash = parse_wasm_hash(&wasm_hash_str)?;
let salt = match user_salt {
Some(s) => s,
None => generate_salt(),
};
let mut sequence = get_next_sequence(client, source_account).await?;
let (_uploaded_wasm_hash, upload_hash, upload_fee) = match upload_wasm(
client,
wasm_bytes,
source_account,
sequence,
100,
network.clone(),
signer,
)
.await
{
Ok(result) => result,
Err(e) => return Ok(DeployOutcome::Failure(format!("Upload failed: {}", e))),
};
sequence = get_next_sequence(client, source_account).await?;
let create_args = CreateContractArgs {
wasm_hash,
deployer_address: source_account.to_string(),
salt,
constructor_args,
};
let (contract_id, create_hash, create_fee) =
match create_contract(client, &create_args, sequence, 100, network, signer).await {
Ok(result) => result,
Err(e) => {
return Ok(DeployOutcome::Partial(PartialDeployResult {
wasm_hash: wasm_hash_str,
upload_hash,
error: e.to_string(),
}));
}
};
let total_fee = upload_fee as u64 + create_fee as u64;
Ok(DeployOutcome::Success(DeployResult {
wasm_hash: wasm_hash_str,
contract_id,
upload_hash,
create_hash,
status: "SUCCESS".into(),
salt: hex::encode(salt),
upload_fee,
create_fee,
total_fee,
}))
}
pub async fn deploy_contract_from_hash(
client: &SorobanRpcClient,
wasm_hash_hex: &str,
source_account: &str,
signer: &Ed25519Signer,
network: Network,
user_salt: Option<[u8; 20]>,
constructor_args: Vec<String>,
) -> Result<DeployOutcome, RpcError> {
use crate::account::get_next_sequence;
let wasm_hash = parse_wasm_hash(wasm_hash_hex)?;
sdkt_xdr::parse_scval_args(&constructor_args)
.map_err(|e| RpcError::Rpc(format!("Invalid constructor argument: {}", e)))?;
let salt = match user_salt {
Some(s) => s,
None => generate_salt(),
};
let sequence = get_next_sequence(client, source_account).await?;
let create_args = CreateContractArgs {
wasm_hash,
deployer_address: source_account.to_string(),
salt,
constructor_args,
};
let (contract_id, create_hash, create_fee) =
match create_contract(client, &create_args, sequence, 100, network, signer).await {
Ok(result) => result,
Err(e) => return Ok(DeployOutcome::Failure(format!("Create failed: {}", e))),
};
Ok(DeployOutcome::Success(DeployResult {
wasm_hash: hex::encode(wasm_hash),
contract_id,
upload_hash: String::new(),
create_hash,
status: "SUCCESS".into(),
salt: hex::encode(salt),
upload_fee: 0,
create_fee,
total_fee: create_fee as u64,
}))
}
pub fn format_pretty(res: &DeployResult) -> String {
format!(
"Deployment Result:\n WASM Hash: {}\n Contract ID: {}\n Upload Hash: {}\n Create Hash: {}\n Salt: {}\n Status: {}\n Upload Fee: {}\n Create Fee: {}\n Total Fee: {}",
res.wasm_hash, res.contract_id, res.upload_hash, res.create_hash, res.salt, res.status, res.upload_fee, res.create_fee, res.total_fee
)
}
pub fn format_json(res: &DeployResult) -> String {
serde_json::json!({
"wasmHash": res.wasm_hash,
"contractId": res.contract_id,
"uploadHash": res.upload_hash,
"createHash": res.create_hash,
"salt": res.salt,
"status": res.status,
"uploadFee": res.upload_fee,
"createFee": res.create_fee,
"totalFee": res.total_fee,
})
.to_string()
}
#[derive(Debug, Clone, Default)]
pub struct CreateContractArgs {
pub wasm_hash: [u8; 32],
pub deployer_address: String,
pub salt: [u8; 20],
pub constructor_args: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_wasm_hash_valid() {
let hash_str = "0101010101010101010101010101010101010101010101010101010101010101";
let result = parse_wasm_hash(hash_str).unwrap();
assert_eq!(result.len(), 32);
assert_eq!(result[0], 1);
}
#[test]
fn test_parse_wasm_hash_invalid_length() {
let hash_str = "0101";
assert!(parse_wasm_hash(hash_str).is_err());
}
#[test]
fn test_parse_wasm_hash_invalid_hex() {
let hash_str = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";
assert!(parse_wasm_hash(hash_str).is_err());
}
#[test]
fn test_parse_min_resource_fee_valid() {
assert_eq!(parse_min_resource_fee("0").unwrap(), 0);
assert_eq!(parse_min_resource_fee("1000").unwrap(), 1000);
assert_eq!(parse_min_resource_fee(" 1234 ").unwrap(), 1234);
assert_eq!(
parse_min_resource_fee(&u32::MAX.to_string()).unwrap(),
u32::MAX
);
}
#[test]
fn test_parse_min_resource_fee_empty_is_error() {
let err = parse_min_resource_fee("").unwrap_err();
assert!(err.to_string().contains("invalid min_resource_fee"));
}
#[test]
fn test_parse_min_resource_fee_non_numeric_is_error() {
for raw in ["N/A", "unavailable", "12.5", "abc", "-1"] {
assert!(
parse_min_resource_fee(raw).is_err(),
"expected error for {raw:?}"
);
}
}
#[test]
fn test_parse_min_resource_fee_overflow_is_error() {
let raw = (u32::MAX as u64 + 1).to_string();
let err = parse_min_resource_fee(&raw).unwrap_err();
assert!(err
.to_string()
.contains("exceeds the supported u32 fee range"));
}
#[test]
fn test_generate_salt_produces_unique_values() {
let salt1 = generate_salt();
let salt2 = generate_salt();
assert_ne!(salt1, salt2);
assert_eq!(salt1.len(), 20);
}
#[test]
fn test_salt_selection_user_provided() {
let user = [7u8; 20];
let selected = match Some(user) {
Some(s) => s,
None => generate_salt(),
};
assert_eq!(selected, user);
}
#[test]
fn test_salt_selection_auto_generate() {
let selected: [u8; 20] = match None {
Some(s) => s,
None => generate_salt(),
};
assert_eq!(selected.len(), 20);
assert!(selected.iter().any(|&b| b != 0));
}
#[test]
fn test_salt_deterministic_contract_id_inputs() {
let salt = [5u8; 20];
let mut padded = [0u8; 32];
padded[..20].copy_from_slice(&salt);
let mut padded2 = [0u8; 32];
padded2[..20].copy_from_slice(&salt);
assert_eq!(padded, padded2);
}
#[test]
fn test_deploy_result_contains_salt_field() {
let result = DeployResult {
wasm_hash: "abc123".into(),
contract_id: "C...".into(),
upload_hash: "u1".into(),
create_hash: "c1".into(),
status: "SUCCESS".into(),
salt: "00112233445566778899aabbccddeeff00112233".into(),
upload_fee: 100,
create_fee: 200,
total_fee: 300,
};
assert_eq!(result.salt, "00112233445566778899aabbccddeeff00112233");
}
#[test]
fn test_deploy_result_fee_fields_and_total() {
let result = DeployResult {
wasm_hash: "abc123".into(),
contract_id: "C...".into(),
upload_hash: "u1".into(),
create_hash: "c1".into(),
status: "SUCCESS".into(),
salt: "00112233445566778899aabbccddeeff00112233".into(),
upload_fee: 1500,
create_fee: 2500,
total_fee: 4000,
};
assert_eq!(result.upload_fee, 1500);
assert_eq!(result.create_fee, 2500);
assert_eq!(result.total_fee, 4000);
assert_eq!(
result.total_fee,
result.upload_fee as u64 + result.create_fee as u64
);
}
#[test]
fn test_deploy_result_fee_overflow_safety() {
let upload_fee = u32::MAX;
let create_fee = u32::MAX;
let total_fee = upload_fee as u64 + create_fee as u64;
let result = DeployResult {
wasm_hash: "abc123".into(),
contract_id: "C...".into(),
upload_hash: "u1".into(),
create_hash: "c1".into(),
status: "SUCCESS".into(),
salt: "00112233445566778899aabbccddeeff00112233".into(),
upload_fee,
create_fee,
total_fee,
};
assert_eq!(result.upload_fee, u32::MAX);
assert_eq!(result.create_fee, u32::MAX);
assert_eq!(result.total_fee, 8589934590);
assert!(result.total_fee > u32::MAX as u64);
assert_eq!(
result.total_fee,
result.upload_fee as u64 + result.create_fee as u64
);
}
#[test]
fn test_explicit_salt_returned_in_result() {
let explicit = [7u8; 20];
let selected = match Some(explicit) {
Some(s) => s,
None => generate_salt(),
};
assert_eq!(selected, explicit);
let hex_salt = hex::encode(selected);
assert_eq!(hex_salt.len(), 40);
assert_eq!(hex_salt, "0707070707070707070707070707070707070707");
}
#[test]
fn test_auto_salt_is_20_bytes_and_hex_encoded() {
let selected: [u8; 20] = match None {
Some(s) => s,
None => generate_salt(),
};
let hex_salt = hex::encode(selected);
assert_eq!(hex_salt.len(), 40);
assert!(hex_salt.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_salt_in_contract_id_derivation_matches_result() {
let salt = [3u8; 20];
let hex_before = hex::encode(salt);
let mut salt_bytes = [0u8; 32];
salt_bytes[..20].copy_from_slice(&salt);
let hex_after = hex::encode(salt);
assert_eq!(hex_before, hex_after);
assert_eq!(hex_after, "0303030303030303030303030303030303030303");
}
#[test]
fn test_format_pretty_includes_salt_and_fees() {
let result = DeployResult {
wasm_hash: "abc".into(),
contract_id: "C123".into(),
upload_hash: "u1".into(),
create_hash: "c1".into(),
status: "SUCCESS".into(),
salt: "00112233445566778899aabbccddeeff00112233".into(),
upload_fee: 150,
create_fee: 250,
total_fee: 400,
};
let pretty = format_pretty(&result);
assert!(pretty.contains("Salt:"));
assert!(pretty.contains("00112233445566778899aabbccddeeff00112233"));
assert!(pretty.contains("Upload Fee: 150"));
assert!(pretty.contains("Create Fee: 250"));
assert!(pretty.contains("Total Fee: 400"));
}
#[test]
fn test_format_json_includes_salt_and_fees() {
let result = DeployResult {
wasm_hash: "abc".into(),
contract_id: "C123".into(),
upload_hash: "u1".into(),
create_hash: "c1".into(),
status: "SUCCESS".into(),
salt: "00112233445566778899aabbccddeeff00112233".into(),
upload_fee: 150,
create_fee: 250,
total_fee: 400,
};
let json = format_json(&result);
assert!(json.contains("\"salt\""));
assert!(json.contains("00112233445566778899aabbccddeeff00112233"));
assert!(json.contains("\"uploadFee\":150"));
assert!(json.contains("\"createFee\":250"));
assert!(json.contains("\"totalFee\":400"));
}
#[test]
fn test_deploy_outcome_display_includes_fees() {
let result = DeployResult {
wasm_hash: "abc".into(),
contract_id: "C123".into(),
upload_hash: "u1".into(),
create_hash: "c1".into(),
status: "SUCCESS".into(),
salt: "00112233445566778899aabbccddeeff00112233".into(),
upload_fee: 150,
create_fee: 250,
total_fee: 400,
};
let outcome = DeployOutcome::Success(result);
let display = format!("{}", outcome);
assert!(display.contains("Upload Fee: 150"));
assert!(display.contains("Create Fee: 250"));
assert!(display.contains("Total Fee: 400"));
}
#[test]
fn test_create_contract_args_constructor_args() {
let args = CreateContractArgs {
wasm_hash: [1u8; 32],
deployer_address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
salt: [2u8; 20],
constructor_args: vec!["AAAAAQAAAAoAAAAA".into()],
};
assert_eq!(args.constructor_args.len(), 1);
assert_eq!(args.constructor_args[0], "AAAAAQAAAAoAAAAA");
let default_args = CreateContractArgs::default();
assert!(default_args.constructor_args.is_empty());
}
#[tokio::test]
async fn test_deploy_contract_with_invalid_constructor_args_fails_before_upload() {
let client = SorobanRpcClient::new("http://127.0.0.1:9999");
let signer = Ed25519Signer::from_seed(&[1u8; 32]);
let wasm = b"\0asm\x01\0\0\0";
let res = deploy_contract_with_args(
&client,
wasm,
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
&signer,
Network::Testnet,
None,
vec!["not_valid_base64_scval".into()],
)
.await;
let err = res.unwrap_err();
match err {
RpcError::Rpc(msg) => {
assert!(msg.contains("Invalid constructor argument"));
}
other => panic!("Unexpected error variant: {other:?}"),
}
}
}