1use crate::error::RpcError;
2use crate::simulate::simulate_transaction;
3use crate::submission::{submit_and_wait, PollConfig, TransactionStatus};
4use crate::SorobanRpcClient;
5use sdkt_xdr::sign::{Ed25519Signer, Network, SigningOptions};
6use sdkt_xdr::sign_transaction;
7use stellar_xdr::{
8 LedgerFootprint, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt, VecM,
9};
10
11#[derive(Debug, Clone, PartialEq)]
13pub struct DeployResult {
14 pub wasm_hash: String,
15 pub contract_id: String,
16 pub upload_hash: String,
17 pub create_hash: String,
18 pub status: String,
19 pub salt: String,
20 pub upload_fee: u32,
21 pub create_fee: u32,
22 pub total_fee: u64,
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct PartialDeployResult {
28 pub wasm_hash: String,
29 pub upload_hash: String,
30 pub error: String,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub enum DeployOutcome {
36 Success(DeployResult),
37 Partial(PartialDeployResult),
38 Failure(String),
39}
40
41impl std::fmt::Display for DeployOutcome {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 DeployOutcome::Success(r) => {
45 write!(
46 f,
47 "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: {}",
48 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
49 )
50 }
51 DeployOutcome::Partial(p) => {
52 write!(
53 f,
54 "Partial deployment: upload succeeded but create failed.\n WASM Hash: {}\n Upload TX: {}\n Error: {}",
55 p.wasm_hash, p.upload_hash, p.error
56 )
57 }
58 DeployOutcome::Failure(e) => {
59 write!(f, "Deployment failed: {}", e)
60 }
61 }
62 }
63}
64
65pub fn parse_wasm_hash(hash_str: &str) -> Result<[u8; 32], RpcError> {
67 let bytes = hex::decode(hash_str)
68 .map_err(|e| RpcError::Rpc(format!("Invalid WASM hash hex: {}", e)))?;
69 if bytes.len() != 32 {
70 return Err(RpcError::Rpc("WASM hash must be 32 bytes".into()));
71 }
72 let mut result = [0u8; 32];
73 result.copy_from_slice(&bytes);
74 Ok(result)
75}
76
77fn parse_min_resource_fee(raw: &str) -> Result<u32, RpcError> {
86 let trimmed = raw.trim();
87 let value: u64 = trimmed.parse().map_err(|_| {
88 RpcError::Rpc(format!(
89 "Simulation returned an invalid min_resource_fee: {trimmed:?} \
90 (expected a numeric stroop value)"
91 ))
92 })?;
93 u32::try_from(value).map_err(|_| {
94 RpcError::Rpc(format!(
95 "Simulation min_resource_fee {value} exceeds the supported u32 fee range"
96 ))
97 })
98}
99
100pub fn generate_salt() -> [u8; 20] {
102 let mut salt = [0u8; 20];
103 getrandom::fill(&mut salt).expect("Failed to generate random salt");
104 salt
105}
106
107pub async fn upload_wasm(
109 client: &SorobanRpcClient,
110 wasm_bytes: &[u8],
111 source_account: &str,
112 sequence: i64,
113 fee: u32,
114 network: Network,
115 signer: &Ed25519Signer,
116) -> Result<(String, String, u32), RpcError> {
117 use sdkt_xdr::builder::UploadWasmParams;
118
119 if wasm_bytes.is_empty() {
120 return Err(RpcError::Rpc("WASM bytes are empty".into()));
121 }
122
123 let meta = sdkt_wasm::parse_metadata(wasm_bytes)
125 .map_err(|e| RpcError::Rpc(format!("Failed to parse WASM metadata: {}", e)))?;
126 let wasm_hash = meta.hash.clone();
127
128 let initial_soroban_data = SorobanTransactionData {
130 ext: SorobanTransactionDataExt::V0,
131 resources: SorobanResources {
132 footprint: LedgerFootprint {
133 read_only: VecM::default(),
134 read_write: VecM::default(),
135 },
136 instructions: 0,
137 disk_read_bytes: 0,
138 write_bytes: 0,
139 },
140 resource_fee: 0,
141 };
142
143 let initial_envelope = sdkt_xdr::builder::build_upload_wasm_tx_with_data(
144 &UploadWasmParams {
145 source_account: source_account.to_string(),
146 sequence,
147 fee,
148 wasm_bytes: wasm_bytes.to_vec(),
149 },
150 initial_soroban_data,
151 )
152 .map_err(|e| RpcError::Rpc(format!("Failed to build upload transaction: {}", e)))?;
153
154 let simulation = simulate_transaction(client, &initial_envelope)
156 .await
157 .map_err(|e| RpcError::Rpc(format!("Upload simulation failed: {}", e)))?;
158
159 if let Some(err) = &simulation.error {
161 return Err(RpcError::Rpc(format!("Upload simulation error: {}", err)));
162 }
163
164 let soroban_data = if simulation.transaction_data.is_empty() {
166 return Err(RpcError::Rpc(
167 "Simulation did not return SorobanTransactionData".into(),
168 ));
169 } else {
170 sdkt_xdr::builder::parse_soroban_transaction_data(&simulation.transaction_data)
171 .map_err(|e| RpcError::Rpc(format!("Failed to parse SorobanTransactionData: {}", e)))?
172 };
173
174 let min_resource_fee: u32 = parse_min_resource_fee(&simulation.min_resource_fee)?;
176
177 let inclusion_fee: u32 = 100;
178 let total_fee = inclusion_fee + min_resource_fee;
179
180 let auth_entries = if simulation.results.is_empty() {
182 Vec::new()
183 } else {
184 sdkt_xdr::builder::parse_soroban_authorization_entries(&simulation.results[0].auth)
185 .map_err(|e| RpcError::Rpc(format!("Failed to parse auth entries: {}", e)))?
186 };
187
188 let final_envelope = sdkt_xdr::builder::build_upload_wasm_tx_with_data_and_auth(
190 &UploadWasmParams {
191 source_account: source_account.to_string(),
192 sequence,
193 fee: total_fee,
194 wasm_bytes: wasm_bytes.to_vec(),
195 },
196 soroban_data,
197 auth_entries,
198 )
199 .map_err(|e| RpcError::Rpc(format!("Failed to build final upload transaction: {}", e)))?;
200
201 let signing_opts = SigningOptions::with(network.clone());
203 let signed_envelope = sign_transaction(&final_envelope, signer, &signing_opts)
204 .map_err(|e| RpcError::Rpc(format!("Failed to sign upload transaction: {}", e)))?;
205
206 let submission_result =
210 submit_and_wait(client, &signed_envelope, true, &PollConfig::default()).await?;
211
212 if submission_result.status != TransactionStatus::Success {
213 let diag = if let Some(ref xdr) = submission_result.error_result_xdr {
214 format!(" | error_result_xdr={}", xdr)
215 } else {
216 String::new()
217 };
218 let events = if !submission_result.diagnostic_events.is_empty() {
219 format!(
220 " | diagnostic_events={:?}",
221 submission_result.diagnostic_events
222 )
223 } else {
224 String::new()
225 };
226 let code = submission_result.error_code.as_deref().unwrap_or("unknown");
227 return Err(RpcError::Rpc(format!(
228 "Upload transaction failed: code={}{}{}",
229 code, diag, events
230 )));
231 }
232
233 Ok((wasm_hash, submission_result.hash, total_fee))
234}
235
236pub async fn create_contract(
238 client: &SorobanRpcClient,
239 args: &CreateContractArgs,
240 sequence: i64,
241 fee: u32,
242 network: Network,
243 signer: &Ed25519Signer,
244) -> Result<(String, String, u32), RpcError> {
245 use sdkt_xdr::builder::{
246 build_create_contract_tx_with_data, build_create_contract_tx_with_data_and_auth,
247 build_create_contract_v2_tx_with_data, build_create_contract_v2_tx_with_data_and_auth,
248 CreateContractParams, CreateContractV2Params,
249 };
250
251 let initial_soroban_data = SorobanTransactionData {
253 ext: SorobanTransactionDataExt::V0,
254 resources: SorobanResources {
255 footprint: LedgerFootprint {
256 read_only: VecM::default(),
257 read_write: VecM::default(),
258 },
259 instructions: 0,
260 disk_read_bytes: 0,
261 write_bytes: 0,
262 },
263 resource_fee: 0,
264 };
265
266 let initial_envelope = if args.constructor_args.is_empty() {
267 build_create_contract_tx_with_data(
268 &CreateContractParams {
269 source_account: args.deployer_address.clone(),
270 sequence,
271 fee,
272 wasm_hash: args.wasm_hash,
273 deployer_address: args.deployer_address.clone(),
274 salt: args.salt,
275 },
276 initial_soroban_data,
277 )
278 .map_err(|e| RpcError::Rpc(format!("Failed to build create transaction: {}", e)))?
279 } else {
280 build_create_contract_v2_tx_with_data(
281 &CreateContractV2Params {
282 source_account: args.deployer_address.clone(),
283 sequence,
284 fee,
285 wasm_hash: args.wasm_hash,
286 deployer_address: args.deployer_address.clone(),
287 salt: args.salt,
288 constructor_args: args.constructor_args.clone(),
289 },
290 initial_soroban_data,
291 )
292 .map_err(|e| RpcError::Rpc(format!("Failed to build create v2 transaction: {}", e)))?
293 };
294
295 let simulation = simulate_transaction(client, &initial_envelope)
297 .await
298 .map_err(|e| RpcError::Rpc(format!("Create simulation failed: {}", e)))?;
299
300 if let Some(err) = &simulation.error {
302 return Err(RpcError::Rpc(format!("Create simulation error: {}", err)));
303 }
304
305 let soroban_data = if simulation.transaction_data.is_empty() {
307 return Err(RpcError::Rpc(
308 "Simulation did not return SorobanTransactionData".into(),
309 ));
310 } else {
311 sdkt_xdr::builder::parse_soroban_transaction_data(&simulation.transaction_data)
312 .map_err(|e| RpcError::Rpc(format!("Failed to parse SorobanTransactionData: {}", e)))?
313 };
314
315 let min_resource_fee: u32 = parse_min_resource_fee(&simulation.min_resource_fee)?;
317
318 let inclusion_fee: u32 = 100;
319 let total_fee = inclusion_fee + min_resource_fee;
320
321 let auth_entries = if simulation.results.is_empty() {
323 Vec::new()
324 } else {
325 sdkt_xdr::builder::parse_soroban_authorization_entries(&simulation.results[0].auth)
326 .map_err(|e| RpcError::Rpc(format!("Failed to parse auth entries: {}", e)))?
327 };
328
329 let final_envelope = if args.constructor_args.is_empty() {
331 build_create_contract_tx_with_data_and_auth(
332 &CreateContractParams {
333 source_account: args.deployer_address.clone(),
334 sequence,
335 fee: total_fee,
336 wasm_hash: args.wasm_hash,
337 deployer_address: args.deployer_address.clone(),
338 salt: args.salt,
339 },
340 soroban_data,
341 auth_entries,
342 )
343 .map_err(|e| RpcError::Rpc(format!("Failed to build final create transaction: {}", e)))?
344 } else {
345 build_create_contract_v2_tx_with_data_and_auth(
346 &CreateContractV2Params {
347 source_account: args.deployer_address.clone(),
348 sequence,
349 fee: total_fee,
350 wasm_hash: args.wasm_hash,
351 deployer_address: args.deployer_address.clone(),
352 salt: args.salt,
353 constructor_args: args.constructor_args.clone(),
354 },
355 soroban_data,
356 auth_entries,
357 )
358 .map_err(|e| {
359 RpcError::Rpc(format!(
360 "Failed to build final create v2 transaction: {}",
361 e
362 ))
363 })?
364 };
365
366 let signing_opts = SigningOptions::with(network.clone());
368 let signed_envelope = sign_transaction(&final_envelope, signer, &signing_opts)
369 .map_err(|e| RpcError::Rpc(format!("Failed to sign create transaction: {}", e)))?;
370
371 let submission_result =
375 submit_and_wait(client, &signed_envelope, true, &PollConfig::default()).await?;
376
377 if submission_result.status != TransactionStatus::Success {
378 let diag = if let Some(ref xdr) = submission_result.error_result_xdr {
379 format!(" | error_result_xdr={}", xdr)
380 } else {
381 String::new()
382 };
383 let events = if !submission_result.diagnostic_events.is_empty() {
384 format!(
385 " | diagnostic_events={:?}",
386 submission_result.diagnostic_events
387 )
388 } else {
389 String::new()
390 };
391 let code = submission_result.error_code.as_deref().unwrap_or("unknown");
392 return Err(RpcError::Rpc(format!(
393 "Create transaction failed: code={}{}{}",
394 code, diag, events
395 )));
396 }
397
398 let network_id = network.network_id();
400 let contract_id = sdkt_xdr::builder::derive_contract_id(
401 &network_id,
402 &args.deployer_address,
403 &args.salt,
404 &args.wasm_hash,
405 )
406 .map_err(|e| RpcError::Rpc(format!("Failed to derive contract ID: {}", e)))?;
407
408 Ok((contract_id, submission_result.hash, total_fee))
409}
410
411pub async fn deploy_contract(
413 client: &SorobanRpcClient,
414 wasm_bytes: &[u8],
415 source_account: &str,
416 signer: &Ed25519Signer,
417 network: Network,
418 user_salt: Option<[u8; 20]>,
419) -> Result<DeployOutcome, RpcError> {
420 deploy_contract_with_args(
421 client,
422 wasm_bytes,
423 source_account,
424 signer,
425 network,
426 user_salt,
427 Vec::new(),
428 )
429 .await
430}
431
432pub async fn deploy_contract_with_args(
434 client: &SorobanRpcClient,
435 wasm_bytes: &[u8],
436 source_account: &str,
437 signer: &Ed25519Signer,
438 network: Network,
439 user_salt: Option<[u8; 20]>,
440 constructor_args: Vec<String>,
441) -> Result<DeployOutcome, RpcError> {
442 use crate::account::get_next_sequence;
443
444 if wasm_bytes.is_empty() {
445 return Err(RpcError::Rpc("WASM bytes are empty".into()));
446 }
447
448 sdkt_xdr::parse_scval_args(&constructor_args)
450 .map_err(|e| RpcError::Rpc(format!("Invalid constructor argument: {}", e)))?;
451
452 let meta = sdkt_wasm::parse_metadata(wasm_bytes)
454 .map_err(|e| RpcError::Rpc(format!("Failed to parse WASM metadata: {}", e)))?;
455 let wasm_hash_str = meta.hash.clone();
456 let wasm_hash = parse_wasm_hash(&wasm_hash_str)?;
457
458 let salt = match user_salt {
460 Some(s) => s,
461 None => generate_salt(),
462 };
463
464 let mut sequence = get_next_sequence(client, source_account).await?;
466
467 let (_uploaded_wasm_hash, upload_hash, upload_fee) = match upload_wasm(
469 client,
470 wasm_bytes,
471 source_account,
472 sequence,
473 100,
474 network.clone(),
475 signer,
476 )
477 .await
478 {
479 Ok(result) => result,
480 Err(e) => return Ok(DeployOutcome::Failure(format!("Upload failed: {}", e))),
481 };
482
483 sequence = get_next_sequence(client, source_account).await?;
485
486 let create_args = CreateContractArgs {
488 wasm_hash,
489 deployer_address: source_account.to_string(),
490 salt,
491 constructor_args,
492 };
493
494 let (contract_id, create_hash, create_fee) =
495 match create_contract(client, &create_args, sequence, 100, network, signer).await {
496 Ok(result) => result,
497 Err(e) => {
498 return Ok(DeployOutcome::Partial(PartialDeployResult {
499 wasm_hash: wasm_hash_str,
500 upload_hash,
501 error: e.to_string(),
502 }));
503 }
504 };
505
506 let total_fee = upload_fee as u64 + create_fee as u64;
507
508 Ok(DeployOutcome::Success(DeployResult {
509 wasm_hash: wasm_hash_str,
510 contract_id,
511 upload_hash,
512 create_hash,
513 status: "SUCCESS".into(),
514 salt: hex::encode(salt),
515 upload_fee,
516 create_fee,
517 total_fee,
518 }))
519}
520
521pub async fn deploy_contract_from_hash(
534 client: &SorobanRpcClient,
535 wasm_hash_hex: &str,
536 source_account: &str,
537 signer: &Ed25519Signer,
538 network: Network,
539 user_salt: Option<[u8; 20]>,
540 constructor_args: Vec<String>,
541) -> Result<DeployOutcome, RpcError> {
542 use crate::account::get_next_sequence;
543
544 let wasm_hash = parse_wasm_hash(wasm_hash_hex)?;
546 sdkt_xdr::parse_scval_args(&constructor_args)
547 .map_err(|e| RpcError::Rpc(format!("Invalid constructor argument: {}", e)))?;
548
549 let salt = match user_salt {
551 Some(s) => s,
552 None => generate_salt(),
553 };
554
555 let sequence = get_next_sequence(client, source_account).await?;
556
557 let create_args = CreateContractArgs {
558 wasm_hash,
559 deployer_address: source_account.to_string(),
560 salt,
561 constructor_args,
562 };
563
564 let (contract_id, create_hash, create_fee) =
565 match create_contract(client, &create_args, sequence, 100, network, signer).await {
566 Ok(result) => result,
567 Err(e) => return Ok(DeployOutcome::Failure(format!("Create failed: {}", e))),
568 };
569
570 Ok(DeployOutcome::Success(DeployResult {
571 wasm_hash: hex::encode(wasm_hash),
572 contract_id,
573 upload_hash: String::new(),
574 create_hash,
575 status: "SUCCESS".into(),
576 salt: hex::encode(salt),
577 upload_fee: 0,
578 create_fee,
579 total_fee: create_fee as u64,
580 }))
581}
582
583pub fn format_pretty(res: &DeployResult) -> String {
585 format!(
586 "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: {}",
587 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
588 )
589}
590
591pub fn format_json(res: &DeployResult) -> String {
593 serde_json::json!({
594 "wasmHash": res.wasm_hash,
595 "contractId": res.contract_id,
596 "uploadHash": res.upload_hash,
597 "createHash": res.create_hash,
598 "salt": res.salt,
599 "status": res.status,
600 "uploadFee": res.upload_fee,
601 "createFee": res.create_fee,
602 "totalFee": res.total_fee,
603 })
604 .to_string()
605}
606
607#[derive(Debug, Clone, Default)]
609pub struct CreateContractArgs {
610 pub wasm_hash: [u8; 32],
611 pub deployer_address: String,
612 pub salt: [u8; 20],
613 pub constructor_args: Vec<String>,
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619
620 #[test]
621 fn test_parse_wasm_hash_valid() {
622 let hash_str = "0101010101010101010101010101010101010101010101010101010101010101";
623 let result = parse_wasm_hash(hash_str).unwrap();
624 assert_eq!(result.len(), 32);
625 assert_eq!(result[0], 1);
626 }
627
628 #[test]
629 fn test_parse_wasm_hash_invalid_length() {
630 let hash_str = "0101";
631 assert!(parse_wasm_hash(hash_str).is_err());
632 }
633
634 #[test]
635 fn test_parse_wasm_hash_invalid_hex() {
636 let hash_str = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";
637 assert!(parse_wasm_hash(hash_str).is_err());
638 }
639
640 #[test]
641 fn test_parse_min_resource_fee_valid() {
642 assert_eq!(parse_min_resource_fee("0").unwrap(), 0);
643 assert_eq!(parse_min_resource_fee("1000").unwrap(), 1000);
644 assert_eq!(parse_min_resource_fee(" 1234 ").unwrap(), 1234);
645 assert_eq!(
646 parse_min_resource_fee(&u32::MAX.to_string()).unwrap(),
647 u32::MAX
648 );
649 }
650
651 #[test]
652 fn test_parse_min_resource_fee_empty_is_error() {
653 let err = parse_min_resource_fee("").unwrap_err();
654 assert!(err.to_string().contains("invalid min_resource_fee"));
655 }
656
657 #[test]
658 fn test_parse_min_resource_fee_non_numeric_is_error() {
659 for raw in ["N/A", "unavailable", "12.5", "abc", "-1"] {
660 assert!(
661 parse_min_resource_fee(raw).is_err(),
662 "expected error for {raw:?}"
663 );
664 }
665 }
666
667 #[test]
668 fn test_parse_min_resource_fee_overflow_is_error() {
669 let raw = (u32::MAX as u64 + 1).to_string();
671 let err = parse_min_resource_fee(&raw).unwrap_err();
672 assert!(err
673 .to_string()
674 .contains("exceeds the supported u32 fee range"));
675 }
676
677 #[test]
678 fn test_generate_salt_produces_unique_values() {
679 let salt1 = generate_salt();
680 let salt2 = generate_salt();
681 assert_ne!(salt1, salt2);
682 assert_eq!(salt1.len(), 20);
683 }
684
685 #[test]
686 fn test_salt_selection_user_provided() {
687 let user = [7u8; 20];
689 let selected = match Some(user) {
690 Some(s) => s,
691 None => generate_salt(),
692 };
693 assert_eq!(selected, user);
694 }
695
696 #[test]
697 fn test_salt_selection_auto_generate() {
698 let selected: [u8; 20] = match None {
700 Some(s) => s,
701 None => generate_salt(),
702 };
703 assert_eq!(selected.len(), 20);
704 assert!(selected.iter().any(|&b| b != 0));
706 }
707
708 #[test]
709 fn test_salt_deterministic_contract_id_inputs() {
710 let salt = [5u8; 20];
712 let mut padded = [0u8; 32];
713 padded[..20].copy_from_slice(&salt);
714 let mut padded2 = [0u8; 32];
715 padded2[..20].copy_from_slice(&salt);
716 assert_eq!(padded, padded2);
717 }
718
719 #[test]
720 fn test_deploy_result_contains_salt_field() {
721 let result = DeployResult {
723 wasm_hash: "abc123".into(),
724 contract_id: "C...".into(),
725 upload_hash: "u1".into(),
726 create_hash: "c1".into(),
727 status: "SUCCESS".into(),
728 salt: "00112233445566778899aabbccddeeff00112233".into(),
729 upload_fee: 100,
730 create_fee: 200,
731 total_fee: 300,
732 };
733 assert_eq!(result.salt, "00112233445566778899aabbccddeeff00112233");
734 }
735
736 #[test]
737 fn test_deploy_result_fee_fields_and_total() {
738 let result = DeployResult {
739 wasm_hash: "abc123".into(),
740 contract_id: "C...".into(),
741 upload_hash: "u1".into(),
742 create_hash: "c1".into(),
743 status: "SUCCESS".into(),
744 salt: "00112233445566778899aabbccddeeff00112233".into(),
745 upload_fee: 1500,
746 create_fee: 2500,
747 total_fee: 4000,
748 };
749 assert_eq!(result.upload_fee, 1500);
750 assert_eq!(result.create_fee, 2500);
751 assert_eq!(result.total_fee, 4000);
752 assert_eq!(
753 result.total_fee,
754 result.upload_fee as u64 + result.create_fee as u64
755 );
756 }
757
758 #[test]
759 fn test_deploy_result_fee_overflow_safety() {
760 let upload_fee = u32::MAX;
761 let create_fee = u32::MAX;
762 let total_fee = upload_fee as u64 + create_fee as u64;
763 let result = DeployResult {
764 wasm_hash: "abc123".into(),
765 contract_id: "C...".into(),
766 upload_hash: "u1".into(),
767 create_hash: "c1".into(),
768 status: "SUCCESS".into(),
769 salt: "00112233445566778899aabbccddeeff00112233".into(),
770 upload_fee,
771 create_fee,
772 total_fee,
773 };
774 assert_eq!(result.upload_fee, u32::MAX);
775 assert_eq!(result.create_fee, u32::MAX);
776 assert_eq!(result.total_fee, 8589934590);
777 assert!(result.total_fee > u32::MAX as u64);
778 assert_eq!(
779 result.total_fee,
780 result.upload_fee as u64 + result.create_fee as u64
781 );
782 }
783
784 #[test]
785 fn test_explicit_salt_returned_in_result() {
786 let explicit = [7u8; 20];
788 let selected = match Some(explicit) {
789 Some(s) => s,
790 None => generate_salt(),
791 };
792 assert_eq!(selected, explicit);
793 let hex_salt = hex::encode(selected);
794 assert_eq!(hex_salt.len(), 40);
795 assert_eq!(hex_salt, "0707070707070707070707070707070707070707");
796 }
797
798 #[test]
799 fn test_auto_salt_is_20_bytes_and_hex_encoded() {
800 let selected: [u8; 20] = match None {
802 Some(s) => s,
803 None => generate_salt(),
804 };
805 let hex_salt = hex::encode(selected);
806 assert_eq!(hex_salt.len(), 40);
807 assert!(hex_salt.chars().all(|c| c.is_ascii_hexdigit()));
809 }
810
811 #[test]
812 fn test_salt_in_contract_id_derivation_matches_result() {
813 let salt = [3u8; 20];
815 let hex_before = hex::encode(salt);
816
817 let mut salt_bytes = [0u8; 32];
819 salt_bytes[..20].copy_from_slice(&salt);
820
821 let hex_after = hex::encode(salt);
822 assert_eq!(hex_before, hex_after);
823 assert_eq!(hex_after, "0303030303030303030303030303030303030303");
824 }
825
826 #[test]
827 fn test_format_pretty_includes_salt_and_fees() {
828 let result = DeployResult {
829 wasm_hash: "abc".into(),
830 contract_id: "C123".into(),
831 upload_hash: "u1".into(),
832 create_hash: "c1".into(),
833 status: "SUCCESS".into(),
834 salt: "00112233445566778899aabbccddeeff00112233".into(),
835 upload_fee: 150,
836 create_fee: 250,
837 total_fee: 400,
838 };
839 let pretty = format_pretty(&result);
840 assert!(pretty.contains("Salt:"));
841 assert!(pretty.contains("00112233445566778899aabbccddeeff00112233"));
842 assert!(pretty.contains("Upload Fee: 150"));
843 assert!(pretty.contains("Create Fee: 250"));
844 assert!(pretty.contains("Total Fee: 400"));
845 }
846
847 #[test]
848 fn test_format_json_includes_salt_and_fees() {
849 let result = DeployResult {
850 wasm_hash: "abc".into(),
851 contract_id: "C123".into(),
852 upload_hash: "u1".into(),
853 create_hash: "c1".into(),
854 status: "SUCCESS".into(),
855 salt: "00112233445566778899aabbccddeeff00112233".into(),
856 upload_fee: 150,
857 create_fee: 250,
858 total_fee: 400,
859 };
860 let json = format_json(&result);
861 assert!(json.contains("\"salt\""));
862 assert!(json.contains("00112233445566778899aabbccddeeff00112233"));
863 assert!(json.contains("\"uploadFee\":150"));
864 assert!(json.contains("\"createFee\":250"));
865 assert!(json.contains("\"totalFee\":400"));
866 }
867
868 #[test]
869 fn test_deploy_outcome_display_includes_fees() {
870 let result = DeployResult {
871 wasm_hash: "abc".into(),
872 contract_id: "C123".into(),
873 upload_hash: "u1".into(),
874 create_hash: "c1".into(),
875 status: "SUCCESS".into(),
876 salt: "00112233445566778899aabbccddeeff00112233".into(),
877 upload_fee: 150,
878 create_fee: 250,
879 total_fee: 400,
880 };
881 let outcome = DeployOutcome::Success(result);
882 let display = format!("{}", outcome);
883 assert!(display.contains("Upload Fee: 150"));
884 assert!(display.contains("Create Fee: 250"));
885 assert!(display.contains("Total Fee: 400"));
886 }
887
888 #[test]
889 fn test_create_contract_args_constructor_args() {
890 let args = CreateContractArgs {
891 wasm_hash: [1u8; 32],
892 deployer_address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
893 salt: [2u8; 20],
894 constructor_args: vec!["AAAAAQAAAAoAAAAA".into()],
895 };
896 assert_eq!(args.constructor_args.len(), 1);
897 assert_eq!(args.constructor_args[0], "AAAAAQAAAAoAAAAA");
898
899 let default_args = CreateContractArgs::default();
900 assert!(default_args.constructor_args.is_empty());
901 }
902
903 #[tokio::test]
904 async fn test_deploy_contract_with_invalid_constructor_args_fails_before_upload() {
905 let client = SorobanRpcClient::new("http://127.0.0.1:9999");
906 let signer = Ed25519Signer::from_seed(&[1u8; 32]);
907 let wasm = b"\0asm\x01\0\0\0";
908 let res = deploy_contract_with_args(
909 &client,
910 wasm,
911 "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
912 &signer,
913 Network::Testnet,
914 None,
915 vec!["not_valid_base64_scval".into()],
916 )
917 .await;
918
919 let err = res.unwrap_err();
920 match err {
921 RpcError::Rpc(msg) => {
922 assert!(msg.contains("Invalid constructor argument"));
923 }
924 other => panic!("Unexpected error variant: {other:?}"),
925 }
926 }
927}