1use crate::{
44 crypto,
45 error::SorobanHelperError,
46 fs::{DefaultFileReader, FileReader},
47 operation::Operations,
48 transaction::TransactionBuilder,
49 Account, Env, ParseResult, Parser, ParserType, SorobanTransactionResponse,
50};
51use stellar_strkey::Contract as ContractId;
52use stellar_xdr::curr::{
53 ContractIdPreimage, ContractIdPreimageFromAddress, Hash, ScAddress, ScVal,
54};
55
56const CONSTRUCTOR_FUNCTION_NAME: &str = "__constructor";
58
59#[derive(Clone)]
64pub struct ClientContractConfigs {
65 pub contract_id: ContractId,
67 pub env: Env,
69 pub source_account: Account,
71}
72
73pub struct Contract {
79 wasm_bytes: Vec<u8>,
81 wasm_hash: Hash,
83 client_configs: Option<ClientContractConfigs>,
85}
86
87impl Clone for Contract {
88 fn clone(&self) -> Self {
89 Self {
90 wasm_bytes: self.wasm_bytes.clone(),
91 wasm_hash: self.wasm_hash.clone(),
92 client_configs: self.client_configs.clone(),
93 }
94 }
95}
96
97impl Contract {
98 pub fn new(
109 wasm_path: &str,
110 client_configs: Option<ClientContractConfigs>,
111 ) -> Result<Self, SorobanHelperError> {
112 Self::new_with_reader(wasm_path, client_configs, DefaultFileReader)
113 }
114
115 pub fn from_configs(client_configs: ClientContractConfigs) -> Self {
125 Self {
126 wasm_bytes: Vec::new(),
127 wasm_hash: crypto::sha256_hash(&[]),
128 client_configs: Some(client_configs),
129 }
130 }
131
132 pub fn new_with_reader<T: FileReader>(
144 wasm_path: &str,
145 client_configs: Option<ClientContractConfigs>,
146 file_reader: T,
147 ) -> Result<Self, SorobanHelperError> {
148 let wasm_bytes = file_reader.read(wasm_path)?;
149 let wasm_hash = crypto::sha256_hash(&wasm_bytes);
150
151 Ok(Self {
152 wasm_bytes,
153 wasm_hash,
154 client_configs,
155 })
156 }
157
158 pub async fn deploy(
177 mut self,
178 env: &Env,
179 account: &mut Account,
180 constructor_args: Option<Vec<ScVal>>,
181 ) -> Result<Self, SorobanHelperError> {
182 self.upload_wasm(account, env).await?;
183
184 let salt = crypto::generate_salt();
185
186 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
187 address: ScAddress::Account(account.account_id()),
188 salt,
189 });
190
191 let has_constructor =
192 String::from_utf8_lossy(&self.wasm_bytes).contains(CONSTRUCTOR_FUNCTION_NAME);
193 let create_operation = Operations::create_contract(
194 contract_id_preimage,
195 self.wasm_hash.clone(),
196 if has_constructor {
197 constructor_args
198 } else {
199 None
200 },
201 )?;
202
203 let builder = TransactionBuilder::new(account, env).add_operation(create_operation);
204
205 let deploy_tx = builder.simulate_and_build(env, account).await?;
206 let tx_envelope = account.sign_transaction(&deploy_tx, &env.network_id())?;
207 let tx_result = env.send_transaction(&tx_envelope).await?;
208
209 let parser = Parser::new(ParserType::Deploy);
210 let result = parser.parse(&tx_result.response)?;
211
212 let contract_id = match result {
213 ParseResult::Deploy(Some(contract_id)) => contract_id,
214 _ => return Err(SorobanHelperError::ContractDeployedConfigsNotSet),
215 };
216
217 self.set_client_configs(ClientContractConfigs {
218 contract_id,
219 env: env.clone(),
220 source_account: account.clone(),
221 });
222
223 Ok(self)
224 }
225
226 fn set_client_configs(&mut self, client_configs: ClientContractConfigs) {
232 self.client_configs = Some(client_configs);
233 }
234
235 pub fn contract_id(&self) -> Option<ContractId> {
241 self.client_configs.as_ref().map(|c| c.contract_id)
242 }
243
244 async fn upload_wasm(
255 &self,
256 account: &mut Account,
257 env: &Env,
258 ) -> Result<(), SorobanHelperError> {
259 let upload_operation = Operations::upload_wasm(self.wasm_bytes.clone())?;
260
261 let builder = TransactionBuilder::new(account, env).add_operation(upload_operation);
262
263 let upload_tx = builder.simulate_and_build(env, account).await?;
264 let tx_envelope = account.sign_transaction(&upload_tx, &env.network_id())?;
265
266 match env.send_transaction(&tx_envelope).await {
267 Ok(_) => Ok(()),
268 Err(e) => {
269 if let SorobanHelperError::ContractCodeAlreadyExists = e {
271 Ok(())
272 } else {
273 Err(e)
274 }
275 }
276 }
277 }
278
279 pub async fn invoke(
295 &mut self,
296 function_name: &str,
297 args: Vec<ScVal>,
298 ) -> Result<SorobanTransactionResponse, SorobanHelperError> {
299 let client_configs = self
300 .client_configs
301 .as_mut()
302 .ok_or(SorobanHelperError::ContractDeployedConfigsNotSet)?;
303
304 let contract_id = client_configs.contract_id;
305 let env = client_configs.env.clone();
306
307 let invoke_operation = Operations::invoke_contract(&contract_id, function_name, args)?;
308
309 let builder = TransactionBuilder::new(&client_configs.source_account, &env)
310 .add_operation(invoke_operation);
311
312 let invoke_tx = builder
313 .simulate_and_build(&env, &client_configs.source_account)
314 .await?;
315
316 let tx_envelope = client_configs
317 .source_account
318 .sign_transaction(&invoke_tx, &env.network_id())?;
319
320 env.send_transaction(&tx_envelope).await
321 }
322}
323
324#[cfg(test)]
325mod test {
326 use crate::{
327 crypto,
328 error::SorobanHelperError,
329 mock::{
330 fs::MockFileReader,
331 mock_account_entry, mock_contract_id, mock_env, mock_signer1,
332 mock_simulate_tx_response, mock_transaction_response,
333 transaction::{create_contract_id_val, mock_transaction_response_with_return_value},
334 },
335 Account, ClientContractConfigs, Contract,
336 };
337 use std::io::Write;
338 use tempfile::NamedTempFile;
339
340 #[test]
341 fn test_contract_clone() {
342 let wasm_bytes = b"mock wasm bytes".to_vec();
343 let wasm_hash = crypto::sha256_hash(&wasm_bytes);
344 let env = mock_env(None, None, None);
345 let account = Account::single(mock_signer1());
346
347 let client_configs = Some(ClientContractConfigs {
348 contract_id: mock_contract_id(account.clone(), &env),
349 env: env.clone(),
350 source_account: account.clone(),
351 });
352
353 let original_contract = Contract {
354 wasm_bytes: wasm_bytes.clone(),
355 wasm_hash,
356 client_configs: client_configs.clone(),
357 };
358
359 let cloned_contract = original_contract.clone();
360
361 assert_eq!(cloned_contract.wasm_bytes, original_contract.wasm_bytes);
362 assert_eq!(cloned_contract.wasm_hash.0, original_contract.wasm_hash.0);
363
364 assert!(cloned_contract.client_configs.is_some());
365 let cloned_configs = cloned_contract.client_configs.unwrap();
366 let original_configs = original_contract.client_configs.unwrap();
367
368 assert_eq!(cloned_configs.contract_id.0, original_configs.contract_id.0);
369 }
370
371 #[test]
372 fn test_contract_new() {
373 let mut temp_file = NamedTempFile::new().unwrap();
375 let wasm_bytes = b"test wasm bytes";
376 temp_file.write_all(wasm_bytes).unwrap();
377
378 let wasm_path = temp_file.path().to_str().unwrap();
379 let contract = Contract::new(wasm_path, None).unwrap();
380
381 assert_eq!(contract.wasm_bytes, wasm_bytes);
382 assert_eq!(contract.wasm_hash, crypto::sha256_hash(wasm_bytes));
383 assert!(contract.client_configs.is_none());
384 }
385
386 #[test]
387 fn test_contract_id() {
388 let wasm_bytes = b"mock wasm bytes".to_vec();
389 let contract_without_configs = Contract {
390 wasm_bytes: wasm_bytes.clone(),
391 wasm_hash: crypto::sha256_hash(&wasm_bytes),
392 client_configs: None,
393 };
394
395 assert!(contract_without_configs.contract_id().is_none());
396
397 let env = mock_env(None, None, None);
398 let account = Account::single(mock_signer1());
399 let contract_id = mock_contract_id(account.clone(), &env);
400
401 let contract_with_configs = Contract {
402 wasm_bytes: wasm_bytes.clone(),
403 wasm_hash: crypto::sha256_hash(&wasm_bytes),
404 client_configs: Some(ClientContractConfigs {
405 contract_id,
406 env: env.clone(),
407 source_account: account.clone(),
408 }),
409 };
410
411 let retrieved_id = contract_with_configs.contract_id();
412 assert!(retrieved_id.is_some());
413 assert_eq!(retrieved_id.unwrap().0, contract_id.0);
414 }
415
416 #[tokio::test]
417 async fn test_file_reader() {
418 let wasm_path = "path/to/wasm";
419 let client_configs = None;
420 let file_reader = MockFileReader::new(Ok(b"mock wasm bytes".to_vec()));
421 let contract = Contract::new_with_reader(wasm_path, client_configs, file_reader).unwrap();
422 assert_eq!(contract.wasm_bytes, b"mock wasm bytes".to_vec());
423 }
424
425 #[tokio::test]
426 async fn test_upload_wasm() {
427 let simulate_transaction_envelope_result = mock_simulate_tx_response(None);
428 let signer_1_account_id = mock_signer1().account_id().0.to_string();
429 let get_account_result = mock_account_entry(&signer_1_account_id);
430
431 let env = mock_env(
432 Some(Ok(get_account_result)),
433 Some(Ok(simulate_transaction_envelope_result)),
434 None,
435 );
436 let wasm_path = "path/to/wasm";
437 let mut account = Account::single(mock_signer1());
438 let client_configs = ClientContractConfigs {
439 contract_id: mock_contract_id(account.clone(), &env),
440 env: env.clone(),
441 source_account: account.clone(),
442 };
443 let file_reader = MockFileReader::new(Ok(b"mock wasm bytes".to_vec()));
444 let contract =
445 Contract::new_with_reader(wasm_path, Some(client_configs), file_reader).unwrap();
446
447 assert!(contract.upload_wasm(&mut account, &env).await.is_ok());
448 }
449
450 #[tokio::test]
451 async fn test_upload_wasm_contract_code_already_exists() {
452 let simulate_transaction_envelope_result = mock_simulate_tx_response(None);
453
454 let signer_1_account_id = mock_signer1().account_id().0.to_string();
455 let get_account_result = mock_account_entry(&signer_1_account_id);
456
457 let send_transaction_result = Err(SorobanHelperError::ContractCodeAlreadyExists);
458
459 let env = mock_env(
460 Some(Ok(get_account_result)),
461 Some(Ok(simulate_transaction_envelope_result)),
462 Some(send_transaction_result),
463 );
464 let wasm_path = "path/to/wasm";
465 let mut account = Account::single(mock_signer1());
466 let client_configs = ClientContractConfigs {
467 contract_id: mock_contract_id(account.clone(), &env),
468 env: env.clone(),
469 source_account: account.clone(),
470 };
471 let file_reader = MockFileReader::new(Ok(b"mock wasm bytes".to_vec()));
472 let contract =
473 Contract::new_with_reader(wasm_path, Some(client_configs), file_reader).unwrap();
474
475 let res = contract.upload_wasm(&mut account, &env).await;
476 assert!(res.is_ok());
478 }
479
480 #[tokio::test]
481 async fn test_contract_invoke() {
482 let simulate_transaction_envelope_result = mock_simulate_tx_response(None);
483
484 let signer_1_account_id = mock_signer1().account_id().0.to_string();
485 let get_account_result = mock_account_entry(&signer_1_account_id);
486 let send_transaction_result = mock_transaction_response();
487
488 let env = mock_env(
489 Some(Ok(get_account_result)),
490 Some(Ok(simulate_transaction_envelope_result)),
491 Some(Ok(send_transaction_result)),
492 );
493 let wasm_path = "path/to/wasm";
494 let account = Account::single(mock_signer1());
495 let client_configs = ClientContractConfigs {
496 contract_id: mock_contract_id(account.clone(), &env),
497 env: env.clone(),
498 source_account: account.clone(),
499 };
500 let file_reader = MockFileReader::new(Ok(b"mock wasm bytes".to_vec()));
501 let mut contract =
502 Contract::new_with_reader(wasm_path, Some(client_configs), file_reader).unwrap();
503
504 let res = contract.invoke("function_name", vec![]).await;
505 assert!(res.is_ok());
506 assert_eq!(
507 res.unwrap().response.result_meta,
508 mock_transaction_response().response.result_meta
509 );
510 }
511
512 #[tokio::test]
513 async fn test_contract_deploy() {
514 let simulate_transaction_envelope_result = mock_simulate_tx_response(None);
515 let signer_1_account_id = mock_signer1().account_id().0.to_string();
516 let get_account_result = mock_account_entry(&signer_1_account_id);
517
518 let contract_val = create_contract_id_val();
520 let send_transaction_result = Ok(mock_transaction_response_with_return_value(contract_val));
521
522 let env = mock_env(
523 Some(Ok(get_account_result)),
524 Some(Ok(simulate_transaction_envelope_result)),
525 Some(send_transaction_result),
526 );
527 let wasm_path = "path/to/wasm";
528 let mut account = Account::single(mock_signer1());
529 let client_configs = ClientContractConfigs {
530 contract_id: mock_contract_id(account.clone(), &env),
531 env: env.clone(),
532 source_account: account.clone(),
533 };
534 let file_reader = MockFileReader::new(Ok(b"mock wasm bytes".to_vec()));
535
536 let wasm_hash = crypto::sha256_hash(b"mock wasm bytes");
537 let contract =
538 Contract::new_with_reader(wasm_path, Some(client_configs), file_reader).unwrap();
539 let res = contract.deploy(&env, &mut account, None).await;
540 assert!(res.is_ok());
541 assert_eq!(res.unwrap().wasm_hash, wasm_hash);
542 }
543
544 #[test]
545 fn test_set_client_configs() {
546 let wasm_bytes = b"mock wasm bytes".to_vec();
547 let mut contract = Contract {
548 wasm_bytes: wasm_bytes.clone(),
549 wasm_hash: crypto::sha256_hash(&wasm_bytes),
550 client_configs: None,
551 };
552
553 let env = mock_env(None, None, None);
554 let account = Account::single(mock_signer1());
555 let contract_id = mock_contract_id(account.clone(), &env);
556
557 let configs = ClientContractConfigs {
558 contract_id,
559 env: env.clone(),
560 source_account: account.clone(),
561 };
562
563 contract.set_client_configs(configs.clone());
564
565 assert!(contract.client_configs.is_some());
566 let set_configs = contract.client_configs.unwrap();
567 assert_eq!(set_configs.contract_id.0, contract_id.0);
568 }
569
570 #[test]
571 fn test_from_configs() {
572 let env = mock_env(None, None, None);
573 let account = Account::single(mock_signer1());
574 let contract_id = mock_contract_id(account.clone(), &env);
575
576 let client_configs = ClientContractConfigs {
577 contract_id,
578 env: env.clone(),
579 source_account: account.clone(),
580 };
581 let contract = Contract::from_configs(client_configs.clone());
582
583 assert!(contract.client_configs.is_some());
584 let stored_configs = contract.client_configs.unwrap();
585 assert_eq!(stored_configs.contract_id.0, contract_id.0);
586
587 assert!(contract.wasm_bytes.is_empty());
589 assert_eq!(contract.wasm_hash, crypto::sha256_hash(&[]));
590 }
591}