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