1use crate::{
44 Account, Env, ParseResult, Parser, ParserType, 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 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 new_with_reader<T: FileReader>(
126 wasm_path: &str,
127 client_configs: Option<ClientContractConfigs>,
128 file_reader: T,
129 ) -> Result<Self, SorobanHelperError> {
130 let wasm_bytes = file_reader.read(wasm_path)?;
131 let wasm_hash = crypto::sha256_hash(&wasm_bytes);
132
133 Ok(Self {
134 wasm_bytes,
135 wasm_hash,
136 client_configs,
137 })
138 }
139
140 pub async fn deploy(
159 mut self,
160 env: &Env,
161 account: &mut Account,
162 constructor_args: Option<Vec<ScVal>>,
163 ) -> Result<Self, SorobanHelperError> {
164 self.upload_wasm(account, env).await?;
165
166 let salt = crypto::generate_salt();
167
168 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
169 address: ScAddress::Account(account.account_id()),
170 salt,
171 });
172
173 let has_constructor =
174 String::from_utf8_lossy(&self.wasm_bytes).contains(CONSTRUCTOR_FUNCTION_NAME);
175 let create_operation = Operations::create_contract(
176 contract_id_preimage,
177 self.wasm_hash.clone(),
178 if has_constructor {
179 constructor_args
180 } else {
181 None
182 },
183 )?;
184
185 let builder = TransactionBuilder::new(account, env).add_operation(create_operation);
186
187 let deploy_tx = builder.simulate_and_build(env, account).await?;
188 let tx_envelope = account.sign_transaction(&deploy_tx, &env.network_id())?;
189 let tx_result = env.send_transaction(&tx_envelope).await?;
190
191 let parser = Parser::new(ParserType::Deploy);
192 let result = parser.parse(&tx_result)?;
193
194 let contract_id = match result {
195 ParseResult::Deploy(Some(contract_id)) => contract_id,
196 _ => return Err(SorobanHelperError::ContractDeployedConfigsNotSet),
197 };
198
199 self.set_client_configs(ClientContractConfigs {
200 contract_id,
201 env: env.clone(),
202 account: account.clone(),
203 });
204
205 Ok(self)
206 }
207
208 fn set_client_configs(&mut self, client_configs: ClientContractConfigs) {
214 self.client_configs = Some(client_configs);
215 }
216
217 pub fn contract_id(&self) -> Option<ContractId> {
223 self.client_configs.as_ref().map(|c| c.contract_id)
224 }
225
226 async fn upload_wasm(
237 &self,
238 account: &mut Account,
239 env: &Env,
240 ) -> Result<(), SorobanHelperError> {
241 let upload_operation = Operations::upload_wasm(self.wasm_bytes.clone())?;
242
243 let builder = TransactionBuilder::new(account, env).add_operation(upload_operation);
244
245 let upload_tx = builder.simulate_and_build(env, account).await?;
246 let tx_envelope = account.sign_transaction(&upload_tx, &env.network_id())?;
247
248 match env.send_transaction(&tx_envelope).await {
249 Ok(_) => Ok(()),
250 Err(e) => {
251 if let SorobanHelperError::ContractCodeAlreadyExists = e {
253 Ok(())
254 } else {
255 Err(e)
256 }
257 }
258 }
259 }
260
261 pub async fn invoke(
277 &mut self,
278 function_name: &str,
279 args: Vec<ScVal>,
280 ) -> Result<stellar_rpc_client::GetTransactionResponse, SorobanHelperError> {
281 let client_configs = self
282 .client_configs
283 .as_mut()
284 .ok_or(SorobanHelperError::ContractDeployedConfigsNotSet)?;
285
286 let contract_id = client_configs.contract_id;
287 let env = client_configs.env.clone();
288
289 let invoke_operation = Operations::invoke_contract(&contract_id, function_name, args)?;
290
291 let builder =
292 TransactionBuilder::new(&client_configs.account, &env).add_operation(invoke_operation);
293
294 let invoke_tx = builder
295 .simulate_and_build(&env, &client_configs.account)
296 .await?;
297 let tx_envelope = client_configs
298 .account
299 .sign_transaction(&invoke_tx, &env.network_id())?;
300
301 env.send_transaction(&tx_envelope).await
302 }
303}
304
305#[cfg(test)]
306mod test {
307 use crate::{
308 Account, ClientContractConfigs, Contract, crypto,
309 error::SorobanHelperError,
310 mock::{
311 fs::MockFileReader,
312 mock_account_entry, mock_contract_id, mock_env, mock_signer1,
313 mock_simulate_tx_response, mock_transaction_response,
314 transaction::{create_contract_id_val, mock_transaction_response_with_return_value},
315 },
316 };
317 use std::io::Write;
318 use tempfile::NamedTempFile;
319
320 #[test]
321 fn test_contract_clone() {
322 let wasm_bytes = b"mock wasm bytes".to_vec();
323 let wasm_hash = crypto::sha256_hash(&wasm_bytes);
324 let env = mock_env(None, None, None);
325 let account = Account::single(mock_signer1());
326
327 let client_configs = Some(ClientContractConfigs {
328 contract_id: mock_contract_id(account.clone(), &env),
329 env: env.clone(),
330 account: account.clone(),
331 });
332
333 let original_contract = Contract {
334 wasm_bytes: wasm_bytes.clone(),
335 wasm_hash,
336 client_configs: client_configs.clone(),
337 };
338
339 let cloned_contract = original_contract.clone();
340
341 assert_eq!(cloned_contract.wasm_bytes, original_contract.wasm_bytes);
342 assert_eq!(cloned_contract.wasm_hash.0, original_contract.wasm_hash.0);
343
344 assert!(cloned_contract.client_configs.is_some());
345 let cloned_configs = cloned_contract.client_configs.unwrap();
346 let original_configs = original_contract.client_configs.unwrap();
347
348 assert_eq!(cloned_configs.contract_id.0, original_configs.contract_id.0);
349 }
350
351 #[test]
352 fn test_contract_new() {
353 let mut temp_file = NamedTempFile::new().unwrap();
355 let wasm_bytes = b"test wasm bytes";
356 temp_file.write_all(wasm_bytes).unwrap();
357
358 let wasm_path = temp_file.path().to_str().unwrap();
359 let contract = Contract::new(wasm_path, None).unwrap();
360
361 assert_eq!(contract.wasm_bytes, wasm_bytes);
362 assert_eq!(contract.wasm_hash, crypto::sha256_hash(wasm_bytes));
363 assert!(contract.client_configs.is_none());
364 }
365
366 #[test]
367 fn test_contract_id() {
368 let wasm_bytes = b"mock wasm bytes".to_vec();
369 let contract_without_configs = Contract {
370 wasm_bytes: wasm_bytes.clone(),
371 wasm_hash: crypto::sha256_hash(&wasm_bytes),
372 client_configs: None,
373 };
374
375 assert!(contract_without_configs.contract_id().is_none());
376
377 let env = mock_env(None, None, None);
378 let account = Account::single(mock_signer1());
379 let contract_id = mock_contract_id(account.clone(), &env);
380
381 let contract_with_configs = Contract {
382 wasm_bytes: wasm_bytes.clone(),
383 wasm_hash: crypto::sha256_hash(&wasm_bytes),
384 client_configs: Some(ClientContractConfigs {
385 contract_id,
386 env: env.clone(),
387 account: account.clone(),
388 }),
389 };
390
391 let retrieved_id = contract_with_configs.contract_id();
392 assert!(retrieved_id.is_some());
393 assert_eq!(retrieved_id.unwrap().0, contract_id.0);
394 }
395
396 #[tokio::test]
397 async fn test_file_reader() {
398 let wasm_path = "path/to/wasm";
399 let client_configs = None;
400 let file_reader = MockFileReader::new(Ok(b"mock wasm bytes".to_vec()));
401 let contract = Contract::new_with_reader(wasm_path, client_configs, file_reader).unwrap();
402 assert_eq!(contract.wasm_bytes, b"mock wasm bytes".to_vec());
403 }
404
405 #[tokio::test]
406 async fn test_upload_wasm() {
407 let simulate_transaction_envelope_result = mock_simulate_tx_response(None);
408 let signer_1_account_id = mock_signer1().account_id().0.to_string();
409 let get_account_result = mock_account_entry(&signer_1_account_id);
410
411 let env = mock_env(
412 Some(Ok(get_account_result)),
413 Some(Ok(simulate_transaction_envelope_result)),
414 None,
415 );
416 let wasm_path = "path/to/wasm";
417 let mut account = Account::single(mock_signer1());
418 let client_configs = ClientContractConfigs {
419 contract_id: mock_contract_id(account.clone(), &env),
420 env: env.clone(),
421 account: account.clone(),
422 };
423 let file_reader = MockFileReader::new(Ok(b"mock wasm bytes".to_vec()));
424 let contract =
425 Contract::new_with_reader(wasm_path, Some(client_configs), file_reader).unwrap();
426
427 assert!(contract.upload_wasm(&mut account, &env).await.is_ok());
428 }
429
430 #[tokio::test]
431 async fn test_upload_wasm_contract_code_already_exists() {
432 let simulate_transaction_envelope_result = mock_simulate_tx_response(None);
433
434 let signer_1_account_id = mock_signer1().account_id().0.to_string();
435 let get_account_result = mock_account_entry(&signer_1_account_id);
436
437 let send_transaction_result = Err(SorobanHelperError::ContractCodeAlreadyExists);
438
439 let env = mock_env(
440 Some(Ok(get_account_result)),
441 Some(Ok(simulate_transaction_envelope_result)),
442 Some(send_transaction_result),
443 );
444 let wasm_path = "path/to/wasm";
445 let mut account = Account::single(mock_signer1());
446 let client_configs = ClientContractConfigs {
447 contract_id: mock_contract_id(account.clone(), &env),
448 env: env.clone(),
449 account: account.clone(),
450 };
451 let file_reader = MockFileReader::new(Ok(b"mock wasm bytes".to_vec()));
452 let contract =
453 Contract::new_with_reader(wasm_path, Some(client_configs), file_reader).unwrap();
454
455 let res = contract.upload_wasm(&mut account, &env).await;
456 assert!(res.is_ok());
458 }
459
460 #[tokio::test]
461 async fn test_contract_invoke() {
462 let simulate_transaction_envelope_result = mock_simulate_tx_response(None);
463
464 let signer_1_account_id = mock_signer1().account_id().0.to_string();
465 let get_account_result = mock_account_entry(&signer_1_account_id);
466
467 let send_transaction_result = Ok(mock_transaction_response());
468
469 let env = mock_env(
470 Some(Ok(get_account_result)),
471 Some(Ok(simulate_transaction_envelope_result)),
472 Some(send_transaction_result),
473 );
474 let wasm_path = "path/to/wasm";
475 let account = Account::single(mock_signer1());
476 let client_configs = ClientContractConfigs {
477 contract_id: mock_contract_id(account.clone(), &env),
478 env: env.clone(),
479 account: account.clone(),
480 };
481 let file_reader = MockFileReader::new(Ok(b"mock wasm bytes".to_vec()));
482 let mut contract =
483 Contract::new_with_reader(wasm_path, Some(client_configs), file_reader).unwrap();
484
485 let res = contract.invoke("function_name", vec![]).await;
486 assert!(res.is_ok());
487 assert_eq!(
488 res.unwrap().result_meta,
489 mock_transaction_response().result_meta
490 );
491 }
492
493 #[tokio::test]
494 async fn test_contract_deploy() {
495 let simulate_transaction_envelope_result = mock_simulate_tx_response(None);
496 let signer_1_account_id = mock_signer1().account_id().0.to_string();
497 let get_account_result = mock_account_entry(&signer_1_account_id);
498
499 let contract_val = create_contract_id_val();
501 let send_transaction_result = Ok(mock_transaction_response_with_return_value(contract_val));
502
503 let env = mock_env(
504 Some(Ok(get_account_result)),
505 Some(Ok(simulate_transaction_envelope_result)),
506 Some(send_transaction_result),
507 );
508 let wasm_path = "path/to/wasm";
509 let mut account = Account::single(mock_signer1());
510 let client_configs = ClientContractConfigs {
511 contract_id: mock_contract_id(account.clone(), &env),
512 env: env.clone(),
513 account: account.clone(),
514 };
515 let file_reader = MockFileReader::new(Ok(b"mock wasm bytes".to_vec()));
516
517 let wasm_hash = crypto::sha256_hash(b"mock wasm bytes");
518 let contract =
519 Contract::new_with_reader(wasm_path, Some(client_configs), file_reader).unwrap();
520 let res = contract.deploy(&env, &mut account, None).await;
521 assert!(res.is_ok());
522 assert_eq!(res.unwrap().wasm_hash, wasm_hash);
523 }
524
525 #[test]
526 fn test_set_client_configs() {
527 let wasm_bytes = b"mock wasm bytes".to_vec();
528 let mut contract = Contract {
529 wasm_bytes: wasm_bytes.clone(),
530 wasm_hash: crypto::sha256_hash(&wasm_bytes),
531 client_configs: None,
532 };
533
534 let env = mock_env(None, None, None);
535 let account = Account::single(mock_signer1());
536 let contract_id = mock_contract_id(account.clone(), &env);
537
538 let configs = ClientContractConfigs {
539 contract_id,
540 env: env.clone(),
541 account: account.clone(),
542 };
543
544 contract.set_client_configs(configs.clone());
545
546 assert!(contract.client_configs.is_some());
547 let set_configs = contract.client_configs.unwrap();
548 assert_eq!(set_configs.contract_id.0, contract_id.0);
549 }
550}