1use crate::{
10 indexer::ContractUpdateInfo,
11 types::{
12 smart_contracts::{self, ContractContext, InvokeContractResult, ReturnValue},
13 transactions, AccountTransactionEffects, ContractInitializedEvent, RejectReason,
14 },
15 v2::{
16 self,
17 dry_run::{self, DryRunTransaction},
18 BlockIdentifier, Client,
19 },
20};
21use concordium_base::{
22 base::{Energy, Nonce},
23 common::types::{self, TransactionTime},
24 contracts_common::{
25 self, schema::VersionedModuleSchema, AccountAddress, Address, Amount, ContractAddress,
26 Cursor, NewContractNameError, NewReceiveNameError,
27 },
28 hashes::TransactionHash,
29 smart_contracts::{
30 ContractEvent, ContractTraceElement, ExceedsParameterSize, ModuleReference,
31 OwnedContractName, OwnedParameter, OwnedReceiveName, WasmModule, WasmVersion,
32 },
33 transactions::{
34 construct::TRANSACTION_HEADER_SIZE, AccountTransaction, EncodedPayload,
35 InitContractPayload, PayloadLike, UpdateContractPayload,
36 },
37};
38pub use concordium_base::{cis2_types::MetadataUrl, cis4_types::*};
39use concordium_smart_contract_engine::utils;
40use serde_json::Value;
41use std::{fmt, marker::PhantomData, sync::Arc};
42use v2::{QueryError, RPCError};
43
44#[derive(Debug)]
51pub struct ContractClient<Type> {
52 pub client: Client,
54 pub address: ContractAddress,
56 pub contract_name: Arc<contracts_common::OwnedContractName>,
58 pub schema: Arc<Option<VersionedModuleSchema>>,
60 phantom: PhantomData<Type>,
61}
62
63impl<Type> Clone for ContractClient<Type> {
64 fn clone(&self) -> Self {
65 Self {
66 client: self.client.clone(),
67 address: self.address,
68 contract_name: self.contract_name.clone(),
69 phantom: PhantomData,
70 schema: self.schema.clone(),
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy)]
77pub struct ContractTransactionMetadata {
78 pub sender_address: AccountAddress,
80 pub nonce: Nonce,
82 pub expiry: types::TransactionTime,
84 pub energy: transactions::send::GivenEnergy,
86 pub amount: types::Amount,
88}
89
90#[derive(Debug, thiserror::Error)]
91pub enum ViewError {
94 #[error("Invalid receive name: {0}")]
95 InvalidName(#[from] NewReceiveNameError),
96 #[error("Node rejected with reason: {0:#?}")]
97 QueryFailed(RejectReason),
98 #[error("Response was not as expected: {0}")]
99 InvalidResponse(#[from] contracts_common::ParseError),
100 #[error("Network error: {0}")]
101 NetworkError(#[from] v2::QueryError),
102 #[error("Parameter is too large: {0}")]
103 ParameterError(#[from] ExceedsParameterSize),
104}
105
106impl From<RejectReason> for ViewError {
107 fn from(value: RejectReason) -> Self { Self::QueryFailed(value) }
108}
109
110pub struct TransactionBuilder<const ADD_ENERGY: bool, Inner> {
120 client: v2::Client,
121 sender: AccountAddress,
122 energy: Energy,
123 add_energy: Option<Energy>,
124 expiry: Option<TransactionTime>,
125 nonce: Option<Nonce>,
126 payload: transactions::Payload,
127 inner: Inner,
128}
129
130impl<const ADD: bool, Inner> TransactionBuilder<ADD, Inner> {
131 fn new(
132 client: v2::Client,
133 sender: AccountAddress,
134 energy: Energy,
135 payload: transactions::Payload,
136 inner: Inner,
137 ) -> Self {
138 Self {
139 client,
140 sender,
141 energy,
142 add_energy: None,
143 expiry: None,
144 nonce: None,
145 payload,
146 inner,
147 }
148 }
149
150 pub fn expiry(mut self, expiry: TransactionTime) -> Self {
153 self.expiry = Some(expiry);
154 self
155 }
156
157 pub fn nonce(mut self, nonce: Nonce) -> Self {
161 self.nonce = Some(nonce);
162 self
163 }
164
165 pub fn current_energy(&self) -> Energy {
168 if ADD {
169 self.energy
171 + self
172 .add_energy
173 .unwrap_or_else(|| std::cmp::max(50, self.energy.energy / 10).into())
174 } else {
175 self.energy
176 }
177 }
178
179 pub async fn send_inner<A>(
182 mut self,
183 signer: &impl transactions::ExactSizeTransactionSigner,
184 k: impl FnOnce(TransactionHash, v2::Client) -> A,
185 ) -> v2::QueryResult<A> {
186 let nonce = if let Some(nonce) = self.nonce {
187 nonce
188 } else {
189 self.client
190 .get_next_account_sequence_number(&self.sender)
191 .await?
192 .nonce
193 };
194 let expiry = self
195 .expiry
196 .unwrap_or_else(|| TransactionTime::hours_after(1));
197 let energy = self.current_energy();
198 let tx = transactions::send::make_and_sign_transaction(
199 signer,
200 self.sender,
201 nonce,
202 expiry,
203 transactions::send::GivenEnergy::Add(energy),
204 self.payload,
205 );
206 let tx_hash = self.client.send_account_transaction(tx).await?;
207 Ok(k(tx_hash, self.client))
208 }
209}
210
211impl<Inner> TransactionBuilder<true, Inner> {
212 pub fn extra_energy(mut self, energy: Energy) -> Self {
218 self.add_energy = Some(energy);
219 self
220 }
221}
222
223pub struct ContractInitInner<Type> {
226 event: ContractInitializedEvent,
228 phantom: PhantomData<Type>,
229}
230
231impl<Type> ContractInitInner<Type> {
232 fn new(event: ContractInitializedEvent) -> Self {
233 Self {
234 event,
235 phantom: PhantomData,
236 }
237 }
238}
239
240pub type ContractInitBuilder<Type> = TransactionBuilder<true, ContractInitInner<Type>>;
248
249pub struct ContractInitHandle<Type> {
255 tx_hash: TransactionHash,
256 client: v2::Client,
257 phantom: PhantomData<Type>,
258}
259
260impl<Type> std::fmt::Display for ContractInitHandle<Type> {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.tx_hash.fmt(f) }
264}
265
266#[derive(Debug, thiserror::Error)]
267pub enum ContractInitError {
270 #[error("The status of the transaction could not be ascertained: {0}")]
271 Query(#[from] QueryError),
272 #[error("Contract update failed with reason: {0:?}")]
273 Failed(RejectReason),
274}
275
276impl<Type> ContractInitHandle<Type> {
277 pub fn hash(&self) -> TransactionHash { self.tx_hash }
279
280 pub async fn wait_for_finalization(
286 mut self,
287 ) -> Result<(ContractClient<Type>, Vec<ContractEvent>), ContractInitError> {
288 let (_, result) = self.client.wait_until_finalized(&self.tx_hash).await?;
289
290 let mk_error = |msg| {
291 Err(ContractInitError::from(QueryError::RPCError(
292 RPCError::CallError(tonic::Status::invalid_argument(msg)),
293 )))
294 };
295
296 match result.details {
297 crate::types::BlockItemSummaryDetails::AccountTransaction(at) => match at.effects {
298 AccountTransactionEffects::ContractInitialized { data } => {
299 let contract_client = ContractClient::create(self.client, data.address).await?;
300 Ok((contract_client, data.events))
301 }
302 AccountTransactionEffects::None {
303 transaction_type: _,
304 reject_reason,
305 } => Err(ContractInitError::Failed(reject_reason)),
306 _ => mk_error(
307 "Expected smart contract initialization status, but did not receive it.",
308 ),
309 },
310 crate::types::BlockItemSummaryDetails::AccountCreation(_) => mk_error(
311 "Expected smart contract initialization status, but received account creation.",
312 ),
313 crate::types::BlockItemSummaryDetails::Update(_) => mk_error(
314 "Expected smart contract initialization status, but received chain update \
315 instruction.",
316 ),
317 }
318 }
319
320 pub async fn wait_for_finalization_timeout(
323 self,
324 timeout: std::time::Duration,
325 ) -> Result<(ContractClient<Type>, Vec<ContractEvent>), ContractInitError> {
326 let result = tokio::time::timeout(timeout, self.wait_for_finalization()).await;
327 match result {
328 Ok(r) => r,
329 Err(_elapsed) => Err(ContractInitError::Query(QueryError::RPCError(
330 RPCError::CallError(tonic::Status::deadline_exceeded(
331 "Deadline waiting for result of transaction is exceeded.",
332 )),
333 ))),
334 }
335 }
336}
337
338#[derive(thiserror::Error, Debug)]
339pub enum DryRunNewInstanceError {
341 #[error("Dry run succeeded, but contract initialization failed due to {0:#?}.")]
342 Failed(RejectReason),
343 #[error("Dry run failed: {0}")]
344 DryRun(#[from] dry_run::DryRunError),
345 #[error("Parameter too large: {0}")]
346 ExceedsParameterSize(#[from] ExceedsParameterSize),
347 #[error("Node query error: {0}")]
348 Query(#[from] v2::QueryError),
349 #[error("Contract name not valid: {0}")]
350 InvalidContractName(#[from] NewContractNameError),
351 #[error("The reported energy consumed for the dry run is less than expected ({min}).")]
352 InvalidEnergy {
353 min: Energy,
355 },
356}
357
358impl From<RejectReason> for DryRunNewInstanceError {
359 fn from(value: RejectReason) -> Self { Self::Failed(value) }
360}
361
362impl<Type> ContractInitBuilder<Type> {
363 pub async fn dry_run_new_instance<P: contracts_common::Serial>(
369 client: Client,
370 sender: AccountAddress,
371 mod_ref: ModuleReference,
372 name: &str,
373 amount: Amount,
374 parameter: &P,
375 ) -> Result<Self, DryRunNewInstanceError> {
376 let parameter = OwnedParameter::from_serial(parameter)?;
377 Self::dry_run_new_instance_raw(client, sender, mod_ref, name, amount, parameter).await
378 }
379
380 pub async fn dry_run_new_instance_raw(
395 mut client: Client,
396 sender: AccountAddress,
397 mod_ref: ModuleReference,
398 name: &str,
399 amount: Amount,
400 parameter: OwnedParameter,
401 ) -> Result<Self, DryRunNewInstanceError> {
402 let name = OwnedContractName::new(format!("init_{name}"))?;
403 let mut dr = client.dry_run(BlockIdentifier::LastFinal).await?;
404 let payload = InitContractPayload {
405 amount,
406 mod_ref,
407 init_name: name,
408 param: parameter,
409 };
410 let payload = transactions::Payload::InitContract { payload };
411 let encoded_payload = payload.encode();
412 let payload_size = encoded_payload.size();
413 let tx = DryRunTransaction {
414 sender,
415 energy_amount: dr.inner.0.energy_quota(),
416 payload: encoded_payload,
417 signatures: Vec::new(),
418 };
419 let result = dr
420 .inner
421 .0
422 .begin_run_transaction(tx)
423 .await
424 .map_err(dry_run::DryRunError::from)?
425 .await?
426 .inner;
427
428 let data = match result.details.effects {
429 AccountTransactionEffects::None {
430 transaction_type: _,
431 reject_reason,
432 } => return Err(reject_reason.into()),
433 AccountTransactionEffects::ContractInitialized { data } => data,
434 _ => {
435 return Err(
436 dry_run::DryRunError::CallError(tonic::Status::invalid_argument(
437 "Unexpected response from dry-running a contract initialization.",
438 ))
439 .into(),
440 )
441 }
442 };
443 let base_cost = transactions::cost::base_cost(
444 TRANSACTION_HEADER_SIZE + u64::from(u32::from(payload_size)),
445 1,
446 );
447 let energy = result
448 .energy_cost
449 .checked_sub(base_cost)
450 .ok_or(DryRunNewInstanceError::InvalidEnergy { min: base_cost })?;
451
452 Ok(ContractInitBuilder::new(
453 client,
454 sender,
455 energy,
456 payload,
457 ContractInitInner::new(data),
458 ))
459 }
460
461 pub fn event(&self) -> &ContractInitializedEvent { &self.inner.event }
467
468 pub async fn send(
471 self,
472 signer: &impl transactions::ExactSizeTransactionSigner,
473 ) -> v2::QueryResult<ContractInitHandle<Type>> {
474 let phantom = self.inner.phantom;
475 self.send_inner(signer, |tx_hash, client| ContractInitHandle {
476 tx_hash,
477 client,
478 phantom,
479 })
480 .await
481 }
482}
483
484pub type ModuleDeployBuilder = TransactionBuilder<false, ModuleReference>;
485
486#[derive(thiserror::Error, Debug)]
487pub enum DryRunModuleDeployError {
490 #[error("Dry run succeeded, but module deployment failed due to {0:#?}.")]
491 Failed(RejectReason),
492 #[error("Dry run failed: {0}")]
493 DryRun(#[from] dry_run::DryRunError),
494 #[error("Node query error: {0}")]
495 Query(#[from] v2::QueryError),
496 #[error("The reported energy consumed for the dry run is less than expected ({min}).")]
497 InvalidEnergy {
498 min: Energy,
500 },
501}
502
503impl DryRunModuleDeployError {
504 pub fn already_exists(&self) -> bool {
506 let Self::Failed(reason) = self else {
507 return false;
508 };
509 matches!(reason, RejectReason::ModuleHashAlreadyExists { .. })
510 }
511}
512
513impl From<RejectReason> for DryRunModuleDeployError {
514 fn from(value: RejectReason) -> Self { Self::Failed(value) }
515}
516
517impl ModuleDeployBuilder {
518 pub async fn dry_run_module_deploy(
523 mut client: Client,
524 sender: AccountAddress,
525 module: WasmModule,
526 ) -> Result<Self, DryRunModuleDeployError> {
527 let mut dr = client.dry_run(BlockIdentifier::LastFinal).await?;
528 let payload = transactions::Payload::DeployModule { module };
529 let encoded_payload = payload.encode();
530 let payload_size = encoded_payload.size();
531 let tx = DryRunTransaction {
532 sender,
533 energy_amount: dr.inner.0.energy_quota(),
534 payload: encoded_payload,
535 signatures: Vec::new(),
536 };
537 let result = dr
538 .inner
539 .0
540 .begin_run_transaction(tx)
541 .await
542 .map_err(dry_run::DryRunError::from)?
543 .await?
544 .inner;
545
546 let module_ref = match result.details.effects {
547 AccountTransactionEffects::None {
548 transaction_type: _,
549 reject_reason,
550 } => return Err(reject_reason.into()),
551 AccountTransactionEffects::ModuleDeployed { module_ref } => module_ref,
552 _ => {
553 return Err(
554 dry_run::DryRunError::CallError(tonic::Status::invalid_argument(
555 "Unexpected response from dry-running a contract initialization.",
556 ))
557 .into(),
558 )
559 }
560 };
561 let base_cost = transactions::cost::base_cost(
562 TRANSACTION_HEADER_SIZE + u64::from(u32::from(payload_size)),
563 1,
564 );
565 let energy = result
566 .energy_cost
567 .checked_sub(base_cost)
568 .ok_or(DryRunModuleDeployError::InvalidEnergy { min: base_cost })?;
569 Ok(Self::new(client, sender, energy, payload, module_ref))
570 }
571}
572
573impl ModuleDeployBuilder {
574 pub async fn send(
577 self,
578 signer: &impl transactions::ExactSizeTransactionSigner,
579 ) -> v2::QueryResult<ModuleDeployHandle> {
580 self.send_inner(signer, |tx_hash, client| ModuleDeployHandle {
581 tx_hash,
582 client,
583 })
584 .await
585 }
586}
587
588pub struct ModuleDeployHandle {
589 tx_hash: TransactionHash,
590 client: v2::Client,
591}
592
593impl std::fmt::Display for ModuleDeployHandle {
596 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.tx_hash.fmt(f) }
597}
598
599#[derive(Debug, thiserror::Error)]
600pub enum ModuleDeployError {
603 #[error("The status of the transaction could not be ascertained: {0}")]
604 Query(#[from] QueryError),
605 #[error("Module deployment failed with reason: {0:?}")]
606 Failed(RejectReason),
607}
608
609#[derive(Debug, Clone, Copy)]
610pub struct ModuleDeployData {
612 pub energy: Energy,
614 pub cost: Amount,
616 pub module_reference: ModuleReference,
618}
619
620impl ModuleDeployHandle {
621 pub fn hash(&self) -> TransactionHash { self.tx_hash }
623
624 pub async fn wait_for_finalization(mut self) -> Result<ModuleDeployData, ModuleDeployError> {
627 let (_, result) = self.client.wait_until_finalized(&self.tx_hash).await?;
628
629 let mk_error = |msg| {
630 Err(ModuleDeployError::from(QueryError::RPCError(
631 RPCError::CallError(tonic::Status::invalid_argument(msg)),
632 )))
633 };
634
635 match result.details {
636 crate::types::BlockItemSummaryDetails::AccountTransaction(at) => match at.effects {
637 AccountTransactionEffects::ModuleDeployed { module_ref } => Ok(ModuleDeployData {
638 energy: result.energy_cost,
639 cost: at.cost,
640 module_reference: module_ref,
641 }),
642 AccountTransactionEffects::None {
643 transaction_type: _,
644 reject_reason,
645 } => Err(ModuleDeployError::Failed(reject_reason)),
646 _ => mk_error("Expected module deploy status, but did not receive it."),
647 },
648 crate::types::BlockItemSummaryDetails::AccountCreation(_) => {
649 mk_error("Expected module deploy status, but received account creation.")
650 }
651 crate::types::BlockItemSummaryDetails::Update(_) => {
652 mk_error("Expected module deploy status, but received chain update instruction.")
653 }
654 }
655 }
656
657 pub async fn wait_for_finalization_timeout(
660 self,
661 timeout: std::time::Duration,
662 ) -> Result<ModuleDeployData, ModuleDeployError> {
663 let result = tokio::time::timeout(timeout, self.wait_for_finalization()).await;
664 match result {
665 Ok(r) => r,
666 Err(_elapsed) => Err(ModuleDeployError::Query(QueryError::RPCError(
667 RPCError::CallError(tonic::Status::deadline_exceeded(
668 "Deadline waiting for result of transaction is exceeded.",
669 )),
670 ))),
671 }
672 }
673}
674
675#[derive(Debug, Clone)]
677pub struct ErrorSchema(pub Value);
678
679impl std::fmt::Display for ErrorSchema {
685 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
686 match &self.0 {
687 Value::Object(map) => {
688 if let Some(key) = map.keys().next() {
689 write!(f, "{}", key)?;
690 if let Some(value) = map.values().next() {
691 if value.is_array() {
692 write!(f, "{}", ErrorSchema(value.clone()))?;
693 }
694 }
695 }
696 }
697 Value::Array(arr) => {
698 if let Some(value) = arr.iter().next() {
699 write!(f, "::{}", ErrorSchema(value.clone()))?;
700 }
701 }
702 _ => write!(f, "{}", self.0)?,
703 }
704 Ok(())
705 }
706}
707
708#[derive(Debug, Clone)]
711pub enum DecodedReason {
712 Std {
713 reject_reason: i32,
715 parsed: ConcordiumStdRejectReason,
717 },
718 Custom {
719 return_value: ReturnValue,
721 reject_reason: i32,
723 parsed: ErrorSchema,
729 },
730}
731
732impl std::fmt::Display for DecodedReason {
735 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
736 match self {
737 DecodedReason::Std { parsed, .. } => {
738 write!(f, "{}", parsed)
739 }
740 DecodedReason::Custom { parsed, .. } => {
741 write!(f, "{}", parsed)
742 }
743 }
744 }
745}
746
747pub enum InvokeContractOutcome {
752 Success(SimulatedTransaction),
754 Failure(RejectedTransaction),
756}
757
758pub type SimulatedTransaction = ContractUpdateBuilder;
764
765#[derive(Debug, Clone)]
767pub struct RejectedTransaction {
768 pub return_value: Option<ReturnValue>,
770 pub reason: RejectReason,
772 pub decoded_reason: Option<DecodedReason>,
777 pub used_energy: Energy,
779 pub payload: transactions::Payload,
781}
782
783impl InvokeContractOutcome {
784 pub fn success(self) -> Result<SimulatedTransaction, RejectedTransaction> {
786 match self {
787 InvokeContractOutcome::Success(simulated_transaction) => Ok(simulated_transaction),
788 InvokeContractOutcome::Failure(rejected_transaction) => Err(rejected_transaction),
789 }
790 }
791}
792
793#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
794#[repr(i32)]
795pub enum ConcordiumStdRejectReason {
796 #[error("[Unspecified (Default reject)]")]
797 Unspecified = -2147483648, #[error("[Error ()]")]
799 Unit = -2147483647, #[error("[ParseError]")]
801 Parse = -2147483646, #[error("[LogError::Full]")]
803 LogFull = -2147483645,
804 #[error("[LogError::Malformed]")]
805 LogMalformed = -2147483644,
806 #[error("[NewContractNameError::MissingInitPrefix]")]
807 NewContractNameMissingInitPrefix = -2147483643,
808 #[error("[NewContractNameError::TooLong]")]
809 NewContractNameTooLong = -2147483642,
810 #[error("[NewReceiveNameError::MissingDotSeparator]")]
811 NewReceiveNameMissingDotSeparator = -2147483641,
812 #[error("[NewReceiveNameError::TooLong]")]
813 NewReceiveNameTooLong = -2147483640,
814 #[error("[NewContractNameError::ContainsDot]")]
815 NewContractNameContainsDot = -2147483639,
816 #[error("[NewContractNameError::InvalidCharacters]")]
817 NewContractNameInvalidCharacters = -2147483638,
818 #[error("[NewReceiveNameError::InvalidCharacters]")]
819 NewReceiveNameInvalidCharacters = -2147483637,
820 #[error("[NotPayableError]")]
821 NotPayableError = -2147483636,
822 #[error("[TransferError::AmountTooLarge]")]
823 TransferAmountTooLarge = -2147483635,
824 #[error("[TransferError::MissingAccount]")]
825 TransferMissingAccount = -2147483634,
826 #[error("[CallContractError::AmountTooLarge]")]
827 CallContractAmountTooLarge = -2147483633,
828 #[error("[CallContractError::MissingAccount]")]
829 CallContractMissingAccount = -2147483632,
830 #[error("[CallContractError::MissingContract]")]
831 CallContractMissingContract = -2147483631,
832 #[error("[CallContractError::MissingEntrypoint]")]
833 CallContractMissingEntrypoint = -2147483630,
834 #[error("[CallContractError::MessageFailed]")]
835 CallContractMessageFailed = -2147483629,
836 #[error("[CallContractError::LogicReject]")]
837 CallContractLogicReject = -2147483628,
838 #[error("[CallContractError::Trap]")]
839 CallContractTrap = -2147483627,
840 #[error("[UpgradeError::MissingModule]")]
841 UpgradeMissingModule = -2147483626,
842 #[error("[UpgradeError::MissingContract]")]
843 UpgradeMissingContract = -2147483625,
844 #[error("[UpgradeError::UnsupportedModuleVersion]")]
845 UpgradeUnsupportedModuleVersion = -2147483624,
846 #[error("[QueryAccountBalanceError]")]
847 QueryAccountBalanceError = -2147483623,
848 #[error("[QueryContractBalanceError]")]
849 QueryContractBalanceError = -2147483622,
850}
851
852pub fn decode_concordium_std_error(reject_reason: i32) -> Option<ConcordiumStdRejectReason> {
855 if (-2147483648..=-2147483622).contains(&reject_reason) {
856 let reason: ConcordiumStdRejectReason = unsafe { ::std::mem::transmute(reject_reason) };
857 Some(reason)
858 } else {
859 None
860 }
861}
862
863pub fn decode_smart_contract_revert(
1079 return_value: Option<&ReturnValue>,
1080 reject_reason: &RejectReason,
1081 schema: Option<&VersionedModuleSchema>,
1082) -> Option<DecodedReason> {
1083 match reject_reason {
1084 RejectReason::RejectedReceive {
1085 reject_reason: error_code,
1086 contract_address: _,
1087 receive_name,
1088 parameter: _,
1089 } => {
1090 let receive_name = receive_name.as_receive_name();
1091
1092 if let Some(decoded_error) = decode_concordium_std_error(*error_code) {
1095 return Some(DecodedReason::Std {
1096 reject_reason: *error_code,
1097 parsed: decoded_error,
1098 });
1099 }
1100
1101 let schema = schema?;
1107
1108 let (Some(error_schema), Some(return_value)) = (
1109 schema
1110 .get_receive_error_schema(
1111 receive_name.contract_name(),
1112 receive_name.entrypoint_name().into(),
1113 )
1114 .ok(),
1115 return_value,
1116 ) else {
1117 return None;
1120 };
1121
1122 let mut cursor = Cursor::new(&return_value.value);
1123
1124 error_schema
1125 .to_json(&mut cursor)
1126 .ok()
1127 .map(|decoded_reason| DecodedReason::Custom {
1128 return_value: return_value.clone(),
1129 reject_reason: *error_code,
1130 parsed: ErrorSchema(decoded_reason),
1131 })
1132 }
1133 _ => None,
1138 }
1139}
1140
1141impl<Type> ContractClient<Type> {
1142 pub async fn create(mut client: Client, address: ContractAddress) -> v2::QueryResult<Self> {
1150 let contract_instance_info = client
1152 .get_instance_info(address, BlockIdentifier::LastFinal)
1153 .await?
1154 .response;
1155
1156 let contract_name = contract_instance_info.name().clone();
1157 let module_reference = contract_instance_info.source_module();
1158
1159 let wasm_module = client
1161 .get_module_source(&module_reference, BlockIdentifier::LastFinal)
1162 .await?
1163 .response;
1164
1165 let schema = match wasm_module.version {
1167 WasmVersion::V0 => utils::get_embedded_schema_v0(wasm_module.source.as_ref()).ok(),
1168 WasmVersion::V1 => utils::get_embedded_schema_v1(wasm_module.source.as_ref()).ok(),
1169 };
1170
1171 Ok(Self {
1172 client,
1173 address,
1174 contract_name: Arc::new(contract_name),
1175 phantom: PhantomData,
1176 schema: Arc::new(schema),
1177 })
1178 }
1179
1180 pub fn new(client: Client, address: ContractAddress, contract_name: OwnedContractName) -> Self {
1192 Self {
1193 client,
1194 address,
1195 contract_name: Arc::new(contract_name),
1196 phantom: PhantomData,
1197 schema: Arc::new(None),
1198 }
1199 }
1200
1201 pub fn new_with_schema(
1218 client: Client,
1219 address: ContractAddress,
1220 contract_name: OwnedContractName,
1221 schema: VersionedModuleSchema,
1222 ) -> Self {
1223 Self {
1224 client,
1225 address,
1226 contract_name: Arc::new(contract_name),
1227 phantom: PhantomData,
1228 schema: Arc::new(Some(schema)),
1229 }
1230 }
1231
1232 pub async fn view<P: contracts_common::Serial, A: contracts_common::Deserial, E>(
1244 &mut self,
1245 entrypoint: &str,
1246 parameter: &P,
1247 bi: impl v2::IntoBlockIdentifier,
1248 ) -> Result<A, E>
1249 where
1250 E: From<NewReceiveNameError>
1251 + From<RejectReason>
1252 + From<contracts_common::ParseError>
1253 + From<v2::QueryError>
1254 + From<ExceedsParameterSize>, {
1255 let parameter = OwnedParameter::from_serial(parameter)?;
1256 self.view_raw::<A, E>(entrypoint, parameter, bi).await
1257 }
1258
1259 pub async fn view_raw<A: contracts_common::Deserial, E>(
1261 &mut self,
1262 entrypoint: &str,
1263 parameter: OwnedParameter,
1264 bi: impl v2::IntoBlockIdentifier,
1265 ) -> Result<A, E>
1266 where
1267 E: From<NewReceiveNameError>
1268 + From<RejectReason>
1269 + From<contracts_common::ParseError>
1270 + From<v2::QueryError>, {
1271 let ir = self
1272 .invoke_raw::<E>(entrypoint, Amount::zero(), None, parameter, bi)
1273 .await?;
1274 match ir {
1275 smart_contracts::InvokeContractResult::Success { return_value, .. } => {
1276 let Some(bytes) = return_value else {
1277 return Err(contracts_common::ParseError {}.into());
1278 };
1279 let response: A = contracts_common::from_bytes(&bytes.value)?;
1280 Ok(response)
1281 }
1282 smart_contracts::InvokeContractResult::Failure { reason, .. } => Err(reason.into()),
1283 }
1284 }
1285
1286 pub async fn invoke_raw<E>(
1289 &mut self,
1290 entrypoint: &str,
1291 amount: Amount,
1292 invoker: Option<Address>,
1293 parameter: OwnedParameter,
1294 bi: impl v2::IntoBlockIdentifier,
1295 ) -> Result<InvokeContractResult, E>
1296 where
1297 E: From<NewReceiveNameError> + From<RejectReason> + From<v2::QueryError>, {
1298 let contract_name = self.contract_name.as_contract_name().contract_name();
1299 let method = OwnedReceiveName::try_from(format!("{contract_name}.{entrypoint}"))?;
1300
1301 let context = ContractContext {
1302 invoker,
1303 contract: self.address,
1304 amount,
1305 method,
1306 parameter,
1307 energy: None,
1308 };
1309
1310 let invoke_result = self.client.invoke_instance(bi, &context).await?.response;
1311 Ok(invoke_result)
1312 }
1313
1314 pub async fn dry_run_update<P: contracts_common::Serial, E>(
1325 &mut self,
1326 entrypoint: &str,
1327 amount: Amount,
1328 sender: AccountAddress,
1329 message: &P,
1330 ) -> Result<ContractUpdateBuilder, E>
1331 where
1332 E: From<NewReceiveNameError>
1333 + From<RejectReason>
1334 + From<v2::QueryError>
1335 + From<ExceedsParameterSize>, {
1336 let message = OwnedParameter::from_serial(message)?;
1337 self.dry_run_update_raw(entrypoint, amount, sender, message)
1338 .await
1339 }
1340
1341 pub async fn dry_run_update_with_reject_reason_info<P: contracts_common::Serial, E>(
1355 &mut self,
1356 entrypoint: &str,
1357 amount: Amount,
1358 sender: AccountAddress,
1359 message: &P,
1360 ) -> Result<InvokeContractOutcome, E>
1361 where
1362 E: From<NewReceiveNameError> + From<v2::QueryError> + From<ExceedsParameterSize>, {
1363 let message = OwnedParameter::from_serial(message)?;
1364 self.dry_run_update_raw_with_reject_reason_info(entrypoint, amount, sender, message)
1365 .await
1366 }
1367
1368 pub async fn dry_run_update_raw<E>(
1371 &mut self,
1372 entrypoint: &str,
1373 amount: Amount,
1374 sender: AccountAddress,
1375 message: OwnedParameter,
1376 ) -> Result<ContractUpdateBuilder, E>
1377 where
1378 E: From<NewReceiveNameError> + From<RejectReason> + From<v2::QueryError>, {
1379 let contract_name = self.contract_name.as_contract_name().contract_name();
1380 let receive_name = OwnedReceiveName::try_from(format!("{contract_name}.{entrypoint}"))?;
1381
1382 let payload = UpdateContractPayload {
1383 amount,
1384 address: self.address,
1385 receive_name,
1386 message,
1387 };
1388
1389 let context = ContractContext::new_from_payload(sender, None, payload);
1390
1391 let invoke_result = self
1392 .client
1393 .invoke_instance(BlockIdentifier::LastFinal, &context)
1394 .await?
1395 .response;
1396 let payload = UpdateContractPayload {
1397 amount,
1398 address: context.contract,
1399 receive_name: context.method,
1400 message: context.parameter,
1401 };
1402
1403 match invoke_result {
1404 InvokeContractResult::Success {
1405 used_energy,
1406 return_value,
1407 events,
1408 } => Ok(ContractUpdateBuilder::new(
1409 self.client.clone(),
1410 sender,
1411 used_energy,
1412 transactions::Payload::Update { payload },
1413 ContractUpdateInner {
1414 return_value,
1415 events,
1416 },
1417 )),
1418 InvokeContractResult::Failure { reason, .. } => Err(reason.into()),
1419 }
1420 }
1421
1422 pub async fn dry_run_update_raw_with_reject_reason_info<E>(
1425 &mut self,
1426 entrypoint: &str,
1427 amount: Amount,
1428 sender: AccountAddress,
1429 message: OwnedParameter,
1430 ) -> Result<InvokeContractOutcome, E>
1431 where
1432 E: From<NewReceiveNameError> + From<v2::QueryError>, {
1433 let contract_name = self.contract_name.as_contract_name().contract_name();
1434 let receive_name = OwnedReceiveName::try_from(format!("{contract_name}.{entrypoint}"))?;
1435
1436 let payload = UpdateContractPayload {
1437 amount,
1438 address: self.address,
1439 receive_name: receive_name.clone(),
1440 message,
1441 };
1442
1443 let context = ContractContext::new_from_payload(sender, None, payload.clone());
1444
1445 let invoke_result = self
1446 .client
1447 .invoke_instance(BlockIdentifier::LastFinal, &context)
1448 .await?
1449 .response;
1450
1451 match invoke_result {
1452 InvokeContractResult::Success {
1453 used_energy,
1454 return_value,
1455 events,
1456 } => Ok(InvokeContractOutcome::Success(SimulatedTransaction::new(
1457 self.client.clone(),
1458 sender,
1459 used_energy,
1460 transactions::Payload::Update { payload },
1461 ContractUpdateInner {
1462 return_value,
1463 events,
1464 },
1465 ))),
1466 InvokeContractResult::Failure {
1467 reason,
1468 return_value,
1469 used_energy,
1470 } => {
1471 let decoded_reason = decode_smart_contract_revert(
1472 return_value.as_ref(),
1473 &reason,
1474 (*self.schema).as_ref(),
1475 );
1476
1477 Ok(InvokeContractOutcome::Failure(RejectedTransaction {
1478 payload: transactions::Payload::Update { payload },
1479 return_value,
1480 used_energy,
1481 reason,
1482 decoded_reason,
1483 }))
1484 }
1485 }
1486 }
1487
1488 pub fn make_update<P: contracts_common::Serial, E>(
1490 &self,
1491 signer: &impl transactions::ExactSizeTransactionSigner,
1492 metadata: &ContractTransactionMetadata,
1493 entrypoint: &str,
1494 message: &P,
1495 ) -> Result<AccountTransaction<EncodedPayload>, E>
1496 where
1497 E: From<NewReceiveNameError> + From<ExceedsParameterSize>, {
1498 let message = OwnedParameter::from_serial(message)?;
1499 self.make_update_raw::<E>(signer, metadata, entrypoint, message)
1500 }
1501
1502 pub async fn update<P: contracts_common::Serial, E>(
1504 &mut self,
1505 signer: &impl transactions::ExactSizeTransactionSigner,
1506 metadata: &ContractTransactionMetadata,
1507 entrypoint: &str,
1508 message: &P,
1509 ) -> Result<TransactionHash, E>
1510 where
1511 E: From<NewReceiveNameError> + From<v2::RPCError> + From<ExceedsParameterSize>, {
1512 let message = OwnedParameter::from_serial(message)?;
1513 self.update_raw::<E>(signer, metadata, entrypoint, message)
1514 .await
1515 }
1516
1517 pub async fn update_raw<E>(
1519 &mut self,
1520 signer: &impl transactions::ExactSizeTransactionSigner,
1521 metadata: &ContractTransactionMetadata,
1522 entrypoint: &str,
1523 message: OwnedParameter,
1524 ) -> Result<TransactionHash, E>
1525 where
1526 E: From<NewReceiveNameError> + From<v2::RPCError>, {
1527 let tx = self.make_update_raw::<E>(signer, metadata, entrypoint, message)?;
1528 let hash = self.client.send_account_transaction(tx).await?;
1529 Ok(hash)
1530 }
1531
1532 pub fn make_update_raw<E>(
1535 &self,
1536 signer: &impl transactions::ExactSizeTransactionSigner,
1537 metadata: &ContractTransactionMetadata,
1538 entrypoint: &str,
1539 message: OwnedParameter,
1540 ) -> Result<AccountTransaction<EncodedPayload>, E>
1541 where
1542 E: From<NewReceiveNameError>, {
1543 let contract_name = self.contract_name.as_contract_name().contract_name();
1544 let receive_name = OwnedReceiveName::try_from(format!("{contract_name}.{entrypoint}"))?;
1545
1546 let payload = UpdateContractPayload {
1547 amount: metadata.amount,
1548 address: self.address,
1549 receive_name,
1550 message,
1551 };
1552
1553 let tx = transactions::send::make_and_sign_transaction(
1554 signer,
1555 metadata.sender_address,
1556 metadata.nonce,
1557 metadata.expiry,
1558 metadata.energy,
1559 transactions::Payload::Update { payload },
1560 );
1561 Ok(tx)
1562 }
1563}
1564
1565pub struct ContractUpdateInner {
1568 return_value: Option<ReturnValue>,
1569 events: Vec<ContractTraceElement>,
1570}
1571
1572pub type ContractUpdateBuilder = TransactionBuilder<true, ContractUpdateInner>;
1574
1575impl ContractUpdateBuilder {
1576 pub async fn send(
1579 self,
1580 signer: &impl transactions::ExactSizeTransactionSigner,
1581 ) -> v2::QueryResult<ContractUpdateHandle> {
1582 self.send_inner(signer, |tx_hash, client| ContractUpdateHandle {
1583 tx_hash,
1584 client,
1585 })
1586 .await
1587 }
1588
1589 pub fn return_value(&self) -> Option<&ReturnValue> { self.inner.return_value.as_ref() }
1591
1592 pub fn events(&self) -> &[ContractTraceElement] { &self.inner.events }
1594}
1595
1596pub struct ContractUpdateHandle {
1602 tx_hash: TransactionHash,
1603 client: v2::Client,
1604}
1605
1606impl std::fmt::Display for ContractUpdateHandle {
1609 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.tx_hash.fmt(f) }
1610}
1611
1612#[derive(Debug, thiserror::Error)]
1613pub enum ContractUpdateError {
1616 #[error("The status of the transaction could not be ascertained: {0}")]
1617 Query(#[from] QueryError),
1618 #[error("Contract update failed with reason: {0:?}")]
1619 Failed(RejectReason),
1620}
1621
1622impl ContractUpdateHandle {
1623 pub fn hash(&self) -> TransactionHash { self.tx_hash }
1625
1626 pub async fn wait_for_finalization(
1629 mut self,
1630 ) -> Result<ContractUpdateInfo, ContractUpdateError> {
1631 let (_, result) = self.client.wait_until_finalized(&self.tx_hash).await?;
1632
1633 let mk_error = |msg| {
1634 Err(ContractUpdateError::from(QueryError::RPCError(
1635 RPCError::CallError(tonic::Status::invalid_argument(msg)),
1636 )))
1637 };
1638
1639 match result.details {
1640 crate::types::BlockItemSummaryDetails::AccountTransaction(at) => match at.effects {
1641 AccountTransactionEffects::ContractUpdateIssued { effects } => {
1642 let Some(execution_tree) = crate::types::execution_tree(effects) else {
1643 return mk_error(
1644 "Expected smart contract update, but received invalid execution tree.",
1645 );
1646 };
1647 Ok(ContractUpdateInfo {
1648 execution_tree,
1649 energy_cost: result.energy_cost,
1650 cost: at.cost,
1651 transaction_hash: self.tx_hash,
1652 sender: at.sender,
1653 })
1654 }
1655 AccountTransactionEffects::None {
1656 transaction_type: _,
1657 reject_reason,
1658 } => Err(ContractUpdateError::Failed(reject_reason)),
1659 _ => mk_error("Expected smart contract update status, but did not receive it."),
1660 },
1661 crate::types::BlockItemSummaryDetails::AccountCreation(_) => {
1662 mk_error("Expected smart contract update status, but received account creation.")
1663 }
1664 crate::types::BlockItemSummaryDetails::Update(_) => mk_error(
1665 "Expected smart contract update status, but received chain update instruction.",
1666 ),
1667 }
1668 }
1669
1670 pub async fn wait_for_finalization_timeout(
1673 self,
1674 timeout: std::time::Duration,
1675 ) -> Result<ContractUpdateInfo, ContractUpdateError> {
1676 let result = tokio::time::timeout(timeout, self.wait_for_finalization()).await;
1677 match result {
1678 Ok(r) => r,
1679 Err(_elapsed) => Err(ContractUpdateError::Query(QueryError::RPCError(
1680 RPCError::CallError(tonic::Status::deadline_exceeded(
1681 "Deadline waiting for result of transaction is exceeded.",
1682 )),
1683 ))),
1684 }
1685 }
1686}