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, Upward,
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(v2::Upward<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<v2::Upward<RejectReason>> for ViewError {
107 fn from(value: v2::Upward<RejectReason>) -> Self {
108 Self::QueryFailed(value)
109 }
110}
111
112pub struct TransactionBuilder<const ADD_ENERGY: bool, Inner> {
122 client: v2::Client,
123 sender: AccountAddress,
124 energy: Energy,
125 add_energy: Option<Energy>,
126 expiry: Option<TransactionTime>,
127 nonce: Option<Nonce>,
128 payload: transactions::Payload,
129 inner: Inner,
130}
131
132impl<const ADD: bool, Inner> TransactionBuilder<ADD, Inner> {
133 fn new(
134 client: v2::Client,
135 sender: AccountAddress,
136 energy: Energy,
137 payload: transactions::Payload,
138 inner: Inner,
139 ) -> Self {
140 Self {
141 client,
142 sender,
143 energy,
144 add_energy: None,
145 expiry: None,
146 nonce: None,
147 payload,
148 inner,
149 }
150 }
151
152 pub fn expiry(mut self, expiry: TransactionTime) -> Self {
155 self.expiry = Some(expiry);
156 self
157 }
158
159 pub fn nonce(mut self, nonce: Nonce) -> Self {
163 self.nonce = Some(nonce);
164 self
165 }
166
167 pub fn current_energy(&self) -> Energy {
170 if ADD {
171 self.energy
173 + self
174 .add_energy
175 .unwrap_or_else(|| std::cmp::max(50, self.energy.energy / 10).into())
176 } else {
177 self.energy
178 }
179 }
180
181 pub async fn send_inner<A>(
184 mut self,
185 signer: &impl transactions::ExactSizeTransactionSigner,
186 k: impl FnOnce(TransactionHash, v2::Client) -> A,
187 ) -> v2::QueryResult<A> {
188 let nonce = if let Some(nonce) = self.nonce {
189 nonce
190 } else {
191 self.client
192 .get_next_account_sequence_number(&self.sender)
193 .await?
194 .nonce
195 };
196 let expiry = self
197 .expiry
198 .unwrap_or_else(|| TransactionTime::hours_after(1));
199 let energy = self.current_energy();
200 let tx = transactions::send::make_and_sign_transaction(
201 signer,
202 self.sender,
203 nonce,
204 expiry,
205 transactions::send::GivenEnergy::Add(energy),
206 self.payload,
207 );
208 let tx_hash = self.client.send_account_transaction(tx).await?;
209 Ok(k(tx_hash, self.client))
210 }
211}
212
213impl<Inner> TransactionBuilder<true, Inner> {
214 pub fn extra_energy(mut self, energy: Energy) -> Self {
220 self.add_energy = Some(energy);
221 self
222 }
223}
224
225pub struct ContractInitInner<Type> {
228 event: ContractInitializedEvent,
230 phantom: PhantomData<Type>,
231}
232
233impl<Type> ContractInitInner<Type> {
234 fn new(event: ContractInitializedEvent) -> Self {
235 Self {
236 event,
237 phantom: PhantomData,
238 }
239 }
240}
241
242pub type ContractInitBuilder<Type> = TransactionBuilder<true, ContractInitInner<Type>>;
250
251pub struct ContractInitHandle<Type> {
257 tx_hash: TransactionHash,
258 client: v2::Client,
259 phantom: PhantomData<Type>,
260}
261
262impl<Type> std::fmt::Display for ContractInitHandle<Type> {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 self.tx_hash.fmt(f)
267 }
268}
269
270#[derive(Debug, thiserror::Error)]
271pub enum ContractInitError {
274 #[error("The status of the transaction could not be ascertained: {0}")]
275 Query(#[from] QueryError),
276 #[error("Contract update failed with reason: {0:?}")]
277 Failed(v2::Upward<RejectReason>),
278}
279
280impl<Type> ContractInitHandle<Type> {
281 pub fn hash(&self) -> TransactionHash {
283 self.tx_hash
284 }
285
286 pub async fn wait_for_finalization(
292 mut self,
293 ) -> Result<(ContractClient<Type>, Vec<ContractEvent>), ContractInitError> {
294 let (_, result) = self.client.wait_until_finalized(&self.tx_hash).await?;
295
296 let mk_error = |msg| {
297 Err(ContractInitError::from(QueryError::RPCError(
298 RPCError::CallError(tonic::Status::invalid_argument(msg)),
299 )))
300 };
301 let v2::upward::Upward::Known(details) = result.details else {
302 return mk_error(
303 "Expected smart contract initialization status, but received unknown block item \
304 details.",
305 );
306 };
307 match details {
308 crate::types::BlockItemSummaryDetails::AccountTransaction(at) => {
309 let v2::upward::Upward::Known(effects) = at.effects else {
310 return mk_error(
311 "Expected smart contract initialization status, but received unknown \
312 block item details.",
313 );
314 };
315 match effects {
316 AccountTransactionEffects::ContractInitialized { data } => {
317 let contract_client =
318 ContractClient::create(self.client, data.address).await?;
319 Ok((contract_client, data.events))
320 }
321 AccountTransactionEffects::None {
322 transaction_type: _,
323 reject_reason,
324 } => Err(ContractInitError::Failed(reject_reason)),
325 _ => mk_error(
326 "Expected smart contract initialization status, but did not receive it.",
327 ),
328 }
329 }
330 crate::types::BlockItemSummaryDetails::AccountCreation(_) => mk_error(
331 "Expected smart contract initialization status, but received account creation.",
332 ),
333 crate::types::BlockItemSummaryDetails::Update(_) => mk_error(
334 "Expected smart contract initialization status, but received chain update \
335 instruction.",
336 ),
337 crate::types::BlockItemSummaryDetails::TokenCreationDetails(_) => mk_error(
338 "Expected smart contract initialization status, but received token creation chain \
339 update instruction.",
340 ),
341 }
342 }
343
344 pub async fn wait_for_finalization_timeout(
347 self,
348 timeout: std::time::Duration,
349 ) -> Result<(ContractClient<Type>, Vec<ContractEvent>), ContractInitError> {
350 let result = tokio::time::timeout(timeout, self.wait_for_finalization()).await;
351 match result {
352 Ok(r) => r,
353 Err(_elapsed) => Err(ContractInitError::Query(QueryError::RPCError(
354 RPCError::CallError(tonic::Status::deadline_exceeded(
355 "Deadline waiting for result of transaction is exceeded.",
356 )),
357 ))),
358 }
359 }
360}
361
362#[derive(thiserror::Error, Debug)]
363pub enum DryRunNewInstanceError {
365 #[error("Dry run succeeded, but contract initialization failed due to {0:#?}.")]
366 Failed(v2::Upward<RejectReason>),
367 #[error("Dry run failed: {0}")]
368 DryRun(#[from] dry_run::DryRunError),
369 #[error("Parameter too large: {0}")]
370 ExceedsParameterSize(#[from] ExceedsParameterSize),
371 #[error("Node query error: {0}")]
372 Query(#[from] v2::QueryError),
373 #[error("Contract name not valid: {0}")]
374 InvalidContractName(#[from] NewContractNameError),
375 #[error("The reported energy consumed for the dry run is less than expected ({min}).")]
376 InvalidEnergy {
377 min: Energy,
379 },
380}
381
382impl From<v2::Upward<RejectReason>> for DryRunNewInstanceError {
383 fn from(value: v2::Upward<RejectReason>) -> Self {
384 Self::Failed(value)
385 }
386}
387
388impl<Type> ContractInitBuilder<Type> {
389 pub async fn dry_run_new_instance<P: contracts_common::Serial>(
395 client: Client,
396 sender: AccountAddress,
397 mod_ref: ModuleReference,
398 name: &str,
399 amount: Amount,
400 parameter: &P,
401 ) -> Result<Self, DryRunNewInstanceError> {
402 let parameter = OwnedParameter::from_serial(parameter)?;
403 Self::dry_run_new_instance_raw(client, sender, mod_ref, name, amount, parameter).await
404 }
405
406 pub async fn dry_run_new_instance_raw(
421 mut client: Client,
422 sender: AccountAddress,
423 mod_ref: ModuleReference,
424 name: &str,
425 amount: Amount,
426 parameter: OwnedParameter,
427 ) -> Result<Self, DryRunNewInstanceError> {
428 let name = OwnedContractName::new(format!("init_{name}"))?;
429 let mut dr = client.dry_run(BlockIdentifier::LastFinal).await?;
430 let payload = InitContractPayload {
431 amount,
432 mod_ref,
433 init_name: name,
434 param: parameter,
435 };
436 let payload = transactions::Payload::InitContract { payload };
437 let encoded_payload = payload.encode();
438 let payload_size = encoded_payload.size();
439 let tx = DryRunTransaction {
440 sender,
441 energy_amount: dr.inner.0.energy_quota(),
442 payload: encoded_payload,
443 signatures: Vec::new(),
444 };
445 let result = dr
446 .inner
447 .0
448 .begin_run_transaction(tx)
449 .await
450 .map_err(dry_run::DryRunError::from)?
451 .await?
452 .inner;
453
454 let data = match result.details.effects.known_or_else(|_unknown| {
455 dry_run::DryRunError::CallError(tonic::Status::invalid_argument(
456 "Unexpected response from dry-running a contract initialization.",
457 ))
458 })? {
459 AccountTransactionEffects::None {
460 transaction_type: _,
461 reject_reason,
462 } => return Err(reject_reason.into()),
463 AccountTransactionEffects::ContractInitialized { data } => data,
464 _ => {
465 return Err(
466 dry_run::DryRunError::CallError(tonic::Status::invalid_argument(
467 "Unexpected response from dry-running a contract initialization.",
468 ))
469 .into(),
470 )
471 }
472 };
473 let base_cost = transactions::cost::base_cost(
474 TRANSACTION_HEADER_SIZE + u64::from(u32::from(payload_size)),
475 1,
476 );
477 let energy = result
478 .energy_cost
479 .checked_sub(base_cost)
480 .ok_or(DryRunNewInstanceError::InvalidEnergy { min: base_cost })?;
481
482 Ok(ContractInitBuilder::new(
483 client,
484 sender,
485 energy,
486 payload,
487 ContractInitInner::new(data),
488 ))
489 }
490
491 pub fn event(&self) -> &ContractInitializedEvent {
497 &self.inner.event
498 }
499
500 pub async fn send(
503 self,
504 signer: &impl transactions::ExactSizeTransactionSigner,
505 ) -> v2::QueryResult<ContractInitHandle<Type>> {
506 let phantom = self.inner.phantom;
507 self.send_inner(signer, |tx_hash, client| ContractInitHandle {
508 tx_hash,
509 client,
510 phantom,
511 })
512 .await
513 }
514}
515
516pub type ModuleDeployBuilder = TransactionBuilder<false, ModuleReference>;
517
518#[derive(thiserror::Error, Debug)]
519pub enum DryRunModuleDeployError {
522 #[error("Dry run succeeded, but module deployment failed due to {0:#?}.")]
523 Failed(v2::Upward<RejectReason>),
524 #[error("Dry run failed: {0}")]
525 DryRun(#[from] dry_run::DryRunError),
526 #[error("Node query error: {0}")]
527 Query(#[from] v2::QueryError),
528 #[error("The reported energy consumed for the dry run is less than expected ({min}).")]
529 InvalidEnergy {
530 min: Energy,
532 },
533}
534
535impl DryRunModuleDeployError {
536 pub fn already_exists(&self) -> bool {
538 let Self::Failed(reason) = self else {
539 return false;
540 };
541 matches!(
542 reason,
543 v2::Upward::Known(RejectReason::ModuleHashAlreadyExists { .. })
544 )
545 }
546}
547
548impl From<v2::Upward<RejectReason>> for DryRunModuleDeployError {
549 fn from(value: v2::Upward<RejectReason>) -> Self {
550 Self::Failed(value)
551 }
552}
553
554impl ModuleDeployBuilder {
555 pub async fn dry_run_module_deploy(
560 mut client: Client,
561 sender: AccountAddress,
562 module: WasmModule,
563 ) -> Result<Self, DryRunModuleDeployError> {
564 let mut dr = client.dry_run(BlockIdentifier::LastFinal).await?;
565 let payload = transactions::Payload::DeployModule { module };
566 let encoded_payload = payload.encode();
567 let payload_size = encoded_payload.size();
568 let tx = DryRunTransaction {
569 sender,
570 energy_amount: dr.inner.0.energy_quota(),
571 payload: encoded_payload,
572 signatures: Vec::new(),
573 };
574 let result = dr
575 .inner
576 .0
577 .begin_run_transaction(tx)
578 .await
579 .map_err(dry_run::DryRunError::from)?
580 .await?
581 .inner;
582
583 let module_ref = match result.details.effects.known_or_else(|_unknown| {
584 dry_run::DryRunError::CallError(tonic::Status::invalid_argument(
585 "Unexpected response from dry-running a module deployment.",
586 ))
587 })? {
588 AccountTransactionEffects::None {
589 transaction_type: _,
590 reject_reason,
591 } => return Err(reject_reason.into()),
592 AccountTransactionEffects::ModuleDeployed { module_ref } => module_ref,
593 _ => {
594 return Err(
595 dry_run::DryRunError::CallError(tonic::Status::invalid_argument(
596 "Unexpected response from dry-running a module deployment.",
597 ))
598 .into(),
599 )
600 }
601 };
602 let base_cost = transactions::cost::base_cost(
603 TRANSACTION_HEADER_SIZE + u64::from(u32::from(payload_size)),
604 1,
605 );
606 let energy = result
607 .energy_cost
608 .checked_sub(base_cost)
609 .ok_or(DryRunModuleDeployError::InvalidEnergy { min: base_cost })?;
610 Ok(Self::new(client, sender, energy, payload, module_ref))
611 }
612}
613
614impl ModuleDeployBuilder {
615 pub async fn send(
618 self,
619 signer: &impl transactions::ExactSizeTransactionSigner,
620 ) -> v2::QueryResult<ModuleDeployHandle> {
621 self.send_inner(signer, |tx_hash, client| ModuleDeployHandle {
622 tx_hash,
623 client,
624 })
625 .await
626 }
627}
628
629pub struct ModuleDeployHandle {
630 tx_hash: TransactionHash,
631 client: v2::Client,
632}
633
634impl std::fmt::Display for ModuleDeployHandle {
637 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
638 self.tx_hash.fmt(f)
639 }
640}
641
642#[derive(Debug, thiserror::Error)]
643pub enum ModuleDeployError {
646 #[error("The status of the transaction could not be ascertained: {0}")]
647 Query(#[from] QueryError),
648 #[error("Module deployment failed with reason: {0:?}")]
649 Failed(v2::Upward<RejectReason>),
650}
651
652#[derive(Debug, Clone, Copy)]
653pub struct ModuleDeployData {
655 pub energy: Energy,
657 pub cost: Amount,
659 pub module_reference: ModuleReference,
661}
662
663impl ModuleDeployHandle {
664 pub fn hash(&self) -> TransactionHash {
666 self.tx_hash
667 }
668
669 pub async fn wait_for_finalization(mut self) -> Result<ModuleDeployData, ModuleDeployError> {
672 let (_, result) = self.client.wait_until_finalized(&self.tx_hash).await?;
673
674 let mk_error = |msg| {
675 Err(ModuleDeployError::from(QueryError::RPCError(
676 RPCError::CallError(tonic::Status::invalid_argument(msg)),
677 )))
678 };
679
680 let v2::upward::Upward::Known(details) = result.details else {
681 return mk_error(
682 "Expected module deploy status, but received unknown block item details.",
683 );
684 };
685 match details {
686 crate::types::BlockItemSummaryDetails::AccountTransaction(at) => {
687 let v2::upward::Upward::Known(effects) = at.effects else {
688 return mk_error(
689 "Expected module deploy status, but received unknown block item effect.",
690 );
691 };
692 match effects {
693 AccountTransactionEffects::ModuleDeployed { module_ref } => {
694 Ok(ModuleDeployData {
695 energy: result.energy_cost,
696 cost: at.cost,
697 module_reference: module_ref,
698 })
699 }
700 AccountTransactionEffects::None {
701 transaction_type: _,
702 reject_reason,
703 } => Err(ModuleDeployError::Failed(reject_reason)),
704 _ => mk_error("Expected module deploy status, but did not receive it."),
705 }
706 }
707 crate::types::BlockItemSummaryDetails::AccountCreation(_) => {
708 mk_error("Expected module deploy status, but received account creation.")
709 }
710 crate::types::BlockItemSummaryDetails::Update(_) => {
711 mk_error("Expected module deploy status, but received chain update instruction.")
712 }
713 crate::types::BlockItemSummaryDetails::TokenCreationDetails(_) => mk_error(
714 "Expected module deploy status, but received token creation chain update \
715 instruction.",
716 ),
717 }
718 }
719
720 pub async fn wait_for_finalization_timeout(
723 self,
724 timeout: std::time::Duration,
725 ) -> Result<ModuleDeployData, ModuleDeployError> {
726 let result = tokio::time::timeout(timeout, self.wait_for_finalization()).await;
727 match result {
728 Ok(r) => r,
729 Err(_elapsed) => Err(ModuleDeployError::Query(QueryError::RPCError(
730 RPCError::CallError(tonic::Status::deadline_exceeded(
731 "Deadline waiting for result of transaction is exceeded.",
732 )),
733 ))),
734 }
735 }
736}
737
738#[derive(Debug, Clone)]
740pub struct ErrorSchema(pub Value);
741
742impl std::fmt::Display for ErrorSchema {
748 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
749 match &self.0 {
750 Value::Object(map) => {
751 if let Some(key) = map.keys().next() {
752 write!(f, "{}", key)?;
753 if let Some(value) = map.values().next() {
754 if value.is_array() {
755 write!(f, "{}", ErrorSchema(value.clone()))?;
756 }
757 }
758 }
759 }
760 Value::Array(arr) => {
761 if let Some(value) = arr.iter().next() {
762 write!(f, "::{}", ErrorSchema(value.clone()))?;
763 }
764 }
765 _ => write!(f, "{}", self.0)?,
766 }
767 Ok(())
768 }
769}
770
771#[derive(Debug, Clone)]
774pub enum DecodedReason {
775 Std {
776 reject_reason: i32,
778 parsed: ConcordiumStdRejectReason,
780 },
781 Custom {
782 return_value: ReturnValue,
784 reject_reason: i32,
786 parsed: ErrorSchema,
792 },
793}
794
795impl std::fmt::Display for DecodedReason {
798 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
799 match self {
800 DecodedReason::Std { parsed, .. } => {
801 write!(f, "{}", parsed)
802 }
803 DecodedReason::Custom { parsed, .. } => {
804 write!(f, "{}", parsed)
805 }
806 }
807 }
808}
809
810pub enum InvokeContractOutcome {
815 Success(SimulatedTransaction),
817 Failure(RejectedTransaction),
819}
820
821pub type SimulatedTransaction = ContractUpdateBuilder;
827
828#[derive(Debug, Clone)]
830pub struct RejectedTransaction {
831 pub return_value: Option<ReturnValue>,
833 pub reason: Upward<RejectReason>,
835 pub decoded_reason: Option<DecodedReason>,
840 pub used_energy: Energy,
842 pub payload: transactions::Payload,
844}
845
846impl InvokeContractOutcome {
847 pub fn success(self) -> Result<SimulatedTransaction, RejectedTransaction> {
849 match self {
850 InvokeContractOutcome::Success(simulated_transaction) => Ok(simulated_transaction),
851 InvokeContractOutcome::Failure(rejected_transaction) => Err(rejected_transaction),
852 }
853 }
854}
855
856#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
857#[repr(i32)]
858pub enum ConcordiumStdRejectReason {
859 #[error("[Unspecified (Default reject)]")]
860 Unspecified = -2147483648, #[error("[Error ()]")]
862 Unit = -2147483647, #[error("[ParseError]")]
864 Parse = -2147483646, #[error("[LogError::Full]")]
866 LogFull = -2147483645,
867 #[error("[LogError::Malformed]")]
868 LogMalformed = -2147483644,
869 #[error("[NewContractNameError::MissingInitPrefix]")]
870 NewContractNameMissingInitPrefix = -2147483643,
871 #[error("[NewContractNameError::TooLong]")]
872 NewContractNameTooLong = -2147483642,
873 #[error("[NewReceiveNameError::MissingDotSeparator]")]
874 NewReceiveNameMissingDotSeparator = -2147483641,
875 #[error("[NewReceiveNameError::TooLong]")]
876 NewReceiveNameTooLong = -2147483640,
877 #[error("[NewContractNameError::ContainsDot]")]
878 NewContractNameContainsDot = -2147483639,
879 #[error("[NewContractNameError::InvalidCharacters]")]
880 NewContractNameInvalidCharacters = -2147483638,
881 #[error("[NewReceiveNameError::InvalidCharacters]")]
882 NewReceiveNameInvalidCharacters = -2147483637,
883 #[error("[NotPayableError]")]
884 NotPayableError = -2147483636,
885 #[error("[TransferError::AmountTooLarge]")]
886 TransferAmountTooLarge = -2147483635,
887 #[error("[TransferError::MissingAccount]")]
888 TransferMissingAccount = -2147483634,
889 #[error("[CallContractError::AmountTooLarge]")]
890 CallContractAmountTooLarge = -2147483633,
891 #[error("[CallContractError::MissingAccount]")]
892 CallContractMissingAccount = -2147483632,
893 #[error("[CallContractError::MissingContract]")]
894 CallContractMissingContract = -2147483631,
895 #[error("[CallContractError::MissingEntrypoint]")]
896 CallContractMissingEntrypoint = -2147483630,
897 #[error("[CallContractError::MessageFailed]")]
898 CallContractMessageFailed = -2147483629,
899 #[error("[CallContractError::LogicReject]")]
900 CallContractLogicReject = -2147483628,
901 #[error("[CallContractError::Trap]")]
902 CallContractTrap = -2147483627,
903 #[error("[UpgradeError::MissingModule]")]
904 UpgradeMissingModule = -2147483626,
905 #[error("[UpgradeError::MissingContract]")]
906 UpgradeMissingContract = -2147483625,
907 #[error("[UpgradeError::UnsupportedModuleVersion]")]
908 UpgradeUnsupportedModuleVersion = -2147483624,
909 #[error("[QueryAccountBalanceError]")]
910 QueryAccountBalanceError = -2147483623,
911 #[error("[QueryContractBalanceError]")]
912 QueryContractBalanceError = -2147483622,
913}
914
915pub fn decode_concordium_std_error(reject_reason: i32) -> Option<ConcordiumStdRejectReason> {
918 if (-2147483648..=-2147483622).contains(&reject_reason) {
919 let reason: ConcordiumStdRejectReason = unsafe { ::std::mem::transmute(reject_reason) };
920 Some(reason)
921 } else {
922 None
923 }
924}
925
926pub fn decode_smart_contract_revert(
1142 return_value: Option<&ReturnValue>,
1143 reject_reason: &RejectReason,
1144 schema: Option<&VersionedModuleSchema>,
1145) -> Option<DecodedReason> {
1146 match reject_reason {
1147 RejectReason::RejectedReceive {
1148 reject_reason: error_code,
1149 contract_address: _,
1150 receive_name,
1151 parameter: _,
1152 } => {
1153 let receive_name = receive_name.as_receive_name();
1154
1155 if let Some(decoded_error) = decode_concordium_std_error(*error_code) {
1158 return Some(DecodedReason::Std {
1159 reject_reason: *error_code,
1160 parsed: decoded_error,
1161 });
1162 }
1163
1164 let schema = schema?;
1170
1171 let (Some(error_schema), Some(return_value)) = (
1172 schema
1173 .get_receive_error_schema(
1174 receive_name.contract_name(),
1175 receive_name.entrypoint_name().into(),
1176 )
1177 .ok(),
1178 return_value,
1179 ) else {
1180 return None;
1183 };
1184
1185 let mut cursor = Cursor::new(&return_value.value);
1186
1187 error_schema
1188 .to_json(&mut cursor)
1189 .ok()
1190 .map(|decoded_reason| DecodedReason::Custom {
1191 return_value: return_value.clone(),
1192 reject_reason: *error_code,
1193 parsed: ErrorSchema(decoded_reason),
1194 })
1195 }
1196 _ => None,
1201 }
1202}
1203
1204impl<Type> ContractClient<Type> {
1205 pub async fn create(mut client: Client, address: ContractAddress) -> v2::QueryResult<Self> {
1213 let contract_instance_info = client
1215 .get_instance_info(address, BlockIdentifier::LastFinal)
1216 .await?
1217 .response;
1218
1219 let contract_name = contract_instance_info.name().clone();
1220 let module_reference = contract_instance_info.source_module();
1221
1222 let wasm_module = client
1224 .get_module_source(&module_reference, BlockIdentifier::LastFinal)
1225 .await?
1226 .response;
1227
1228 let schema = match wasm_module.version {
1230 WasmVersion::V0 => utils::get_embedded_schema_v0(wasm_module.source.as_ref()).ok(),
1231 WasmVersion::V1 => utils::get_embedded_schema_v1(wasm_module.source.as_ref()).ok(),
1232 };
1233
1234 Ok(Self {
1235 client,
1236 address,
1237 contract_name: Arc::new(contract_name),
1238 phantom: PhantomData,
1239 schema: Arc::new(schema),
1240 })
1241 }
1242
1243 pub fn new(client: Client, address: ContractAddress, contract_name: OwnedContractName) -> Self {
1255 Self {
1256 client,
1257 address,
1258 contract_name: Arc::new(contract_name),
1259 phantom: PhantomData,
1260 schema: Arc::new(None),
1261 }
1262 }
1263
1264 pub fn new_with_schema(
1281 client: Client,
1282 address: ContractAddress,
1283 contract_name: OwnedContractName,
1284 schema: VersionedModuleSchema,
1285 ) -> Self {
1286 Self {
1287 client,
1288 address,
1289 contract_name: Arc::new(contract_name),
1290 phantom: PhantomData,
1291 schema: Arc::new(Some(schema)),
1292 }
1293 }
1294
1295 pub async fn view<P: contracts_common::Serial, A: contracts_common::Deserial, E>(
1307 &mut self,
1308 entrypoint: &str,
1309 parameter: &P,
1310 bi: impl v2::IntoBlockIdentifier,
1311 ) -> Result<A, E>
1312 where
1313 E: From<NewReceiveNameError>
1314 + From<v2::Upward<RejectReason>>
1315 + From<contracts_common::ParseError>
1316 + From<v2::QueryError>
1317 + From<ExceedsParameterSize>,
1318 {
1319 let parameter = OwnedParameter::from_serial(parameter)?;
1320 self.view_raw::<A, E>(entrypoint, parameter, bi).await
1321 }
1322
1323 pub async fn view_raw<A: contracts_common::Deserial, E>(
1325 &mut self,
1326 entrypoint: &str,
1327 parameter: OwnedParameter,
1328 bi: impl v2::IntoBlockIdentifier,
1329 ) -> Result<A, E>
1330 where
1331 E: From<NewReceiveNameError>
1332 + From<v2::Upward<RejectReason>>
1333 + From<contracts_common::ParseError>
1334 + From<v2::QueryError>,
1335 {
1336 let ir = self
1337 .invoke_raw::<E>(entrypoint, Amount::zero(), None, parameter, bi)
1338 .await?;
1339 match ir {
1340 smart_contracts::InvokeContractResult::Success { return_value, .. } => {
1341 let Some(bytes) = return_value else {
1342 return Err(contracts_common::ParseError {}.into());
1343 };
1344 let response: A = contracts_common::from_bytes(&bytes.value)?;
1345 Ok(response)
1346 }
1347 smart_contracts::InvokeContractResult::Failure { reason, .. } => Err(reason.into()),
1348 }
1349 }
1350
1351 pub async fn invoke_raw<E>(
1354 &mut self,
1355 entrypoint: &str,
1356 amount: Amount,
1357 invoker: Option<Address>,
1358 parameter: OwnedParameter,
1359 bi: impl v2::IntoBlockIdentifier,
1360 ) -> Result<InvokeContractResult, E>
1361 where
1362 E: From<NewReceiveNameError> + From<v2::Upward<RejectReason>> + From<v2::QueryError>,
1363 {
1364 let contract_name = self.contract_name.as_contract_name().contract_name();
1365 let method = OwnedReceiveName::try_from(format!("{contract_name}.{entrypoint}"))?;
1366
1367 let context = ContractContext {
1368 invoker,
1369 contract: self.address,
1370 amount,
1371 method,
1372 parameter,
1373 energy: None,
1374 };
1375
1376 let invoke_result = self.client.invoke_instance(bi, &context).await?.response;
1377 Ok(invoke_result)
1378 }
1379
1380 pub async fn dry_run_update<P: contracts_common::Serial, E>(
1391 &mut self,
1392 entrypoint: &str,
1393 amount: Amount,
1394 sender: AccountAddress,
1395 message: &P,
1396 ) -> Result<ContractUpdateBuilder, E>
1397 where
1398 E: From<NewReceiveNameError>
1399 + From<v2::Upward<RejectReason>>
1400 + From<v2::QueryError>
1401 + From<ExceedsParameterSize>,
1402 {
1403 let message = OwnedParameter::from_serial(message)?;
1404 self.dry_run_update_raw(entrypoint, amount, sender, message)
1405 .await
1406 }
1407
1408 pub async fn dry_run_update_with_reject_reason_info<P: contracts_common::Serial, E>(
1422 &mut self,
1423 entrypoint: &str,
1424 amount: Amount,
1425 sender: AccountAddress,
1426 message: &P,
1427 ) -> Result<InvokeContractOutcome, E>
1428 where
1429 E: From<NewReceiveNameError> + From<v2::QueryError> + From<ExceedsParameterSize>,
1430 {
1431 let message = OwnedParameter::from_serial(message)?;
1432 self.dry_run_update_raw_with_reject_reason_info(entrypoint, amount, sender, message)
1433 .await
1434 }
1435
1436 pub async fn dry_run_update_raw<E>(
1439 &mut self,
1440 entrypoint: &str,
1441 amount: Amount,
1442 sender: AccountAddress,
1443 message: OwnedParameter,
1444 ) -> Result<ContractUpdateBuilder, E>
1445 where
1446 E: From<NewReceiveNameError> + From<v2::Upward<RejectReason>> + From<v2::QueryError>,
1447 {
1448 let contract_name = self.contract_name.as_contract_name().contract_name();
1449 let receive_name = OwnedReceiveName::try_from(format!("{contract_name}.{entrypoint}"))?;
1450
1451 let payload = UpdateContractPayload {
1452 amount,
1453 address: self.address,
1454 receive_name,
1455 message,
1456 };
1457
1458 let context = ContractContext::new_from_payload(sender, None, payload);
1459
1460 let invoke_result = self
1461 .client
1462 .invoke_instance(BlockIdentifier::LastFinal, &context)
1463 .await?
1464 .response;
1465 let payload = UpdateContractPayload {
1466 amount,
1467 address: context.contract,
1468 receive_name: context.method,
1469 message: context.parameter,
1470 };
1471
1472 match invoke_result {
1473 InvokeContractResult::Success {
1474 used_energy,
1475 return_value,
1476 events,
1477 } => Ok(ContractUpdateBuilder::new(
1478 self.client.clone(),
1479 sender,
1480 used_energy,
1481 transactions::Payload::Update { payload },
1482 ContractUpdateInner {
1483 return_value,
1484 events,
1485 },
1486 )),
1487 InvokeContractResult::Failure { reason, .. } => Err(reason.into()),
1488 }
1489 }
1490
1491 pub async fn dry_run_update_raw_with_reject_reason_info<E>(
1494 &mut self,
1495 entrypoint: &str,
1496 amount: Amount,
1497 sender: AccountAddress,
1498 message: OwnedParameter,
1499 ) -> Result<InvokeContractOutcome, E>
1500 where
1501 E: From<NewReceiveNameError> + From<v2::QueryError>,
1502 {
1503 let contract_name = self.contract_name.as_contract_name().contract_name();
1504 let receive_name = OwnedReceiveName::try_from(format!("{contract_name}.{entrypoint}"))?;
1505
1506 let payload = UpdateContractPayload {
1507 amount,
1508 address: self.address,
1509 receive_name: receive_name.clone(),
1510 message,
1511 };
1512
1513 let context = ContractContext::new_from_payload(sender, None, payload.clone());
1514
1515 let invoke_result = self
1516 .client
1517 .invoke_instance(BlockIdentifier::LastFinal, &context)
1518 .await?
1519 .response;
1520
1521 match invoke_result {
1522 InvokeContractResult::Success {
1523 used_energy,
1524 return_value,
1525 events,
1526 } => Ok(InvokeContractOutcome::Success(SimulatedTransaction::new(
1527 self.client.clone(),
1528 sender,
1529 used_energy,
1530 transactions::Payload::Update { payload },
1531 ContractUpdateInner {
1532 return_value,
1533 events,
1534 },
1535 ))),
1536 InvokeContractResult::Failure {
1537 reason,
1538 return_value,
1539 used_energy,
1540 } => {
1541 let decoded_reason = reason.as_ref().known().and_then(|reason| {
1542 decode_smart_contract_revert(
1543 return_value.as_ref(),
1544 reason,
1545 (*self.schema).as_ref(),
1546 )
1547 });
1548
1549 Ok(InvokeContractOutcome::Failure(RejectedTransaction {
1550 payload: transactions::Payload::Update { payload },
1551 return_value,
1552 used_energy,
1553 reason,
1554 decoded_reason,
1555 }))
1556 }
1557 }
1558 }
1559
1560 pub fn make_update<P: contracts_common::Serial, E>(
1562 &self,
1563 signer: &impl transactions::ExactSizeTransactionSigner,
1564 metadata: &ContractTransactionMetadata,
1565 entrypoint: &str,
1566 message: &P,
1567 ) -> Result<AccountTransaction<EncodedPayload>, E>
1568 where
1569 E: From<NewReceiveNameError> + From<ExceedsParameterSize>,
1570 {
1571 let message = OwnedParameter::from_serial(message)?;
1572 self.make_update_raw::<E>(signer, metadata, entrypoint, message)
1573 }
1574
1575 pub async fn update<P: contracts_common::Serial, E>(
1577 &mut self,
1578 signer: &impl transactions::ExactSizeTransactionSigner,
1579 metadata: &ContractTransactionMetadata,
1580 entrypoint: &str,
1581 message: &P,
1582 ) -> Result<TransactionHash, E>
1583 where
1584 E: From<NewReceiveNameError> + From<v2::RPCError> + From<ExceedsParameterSize>,
1585 {
1586 let message = OwnedParameter::from_serial(message)?;
1587 self.update_raw::<E>(signer, metadata, entrypoint, message)
1588 .await
1589 }
1590
1591 pub async fn update_raw<E>(
1593 &mut self,
1594 signer: &impl transactions::ExactSizeTransactionSigner,
1595 metadata: &ContractTransactionMetadata,
1596 entrypoint: &str,
1597 message: OwnedParameter,
1598 ) -> Result<TransactionHash, E>
1599 where
1600 E: From<NewReceiveNameError> + From<v2::RPCError>,
1601 {
1602 let tx = self.make_update_raw::<E>(signer, metadata, entrypoint, message)?;
1603 let hash = self.client.send_account_transaction(tx).await?;
1604 Ok(hash)
1605 }
1606
1607 pub fn make_update_raw<E>(
1610 &self,
1611 signer: &impl transactions::ExactSizeTransactionSigner,
1612 metadata: &ContractTransactionMetadata,
1613 entrypoint: &str,
1614 message: OwnedParameter,
1615 ) -> Result<AccountTransaction<EncodedPayload>, E>
1616 where
1617 E: From<NewReceiveNameError>,
1618 {
1619 let contract_name = self.contract_name.as_contract_name().contract_name();
1620 let receive_name = OwnedReceiveName::try_from(format!("{contract_name}.{entrypoint}"))?;
1621
1622 let payload = UpdateContractPayload {
1623 amount: metadata.amount,
1624 address: self.address,
1625 receive_name,
1626 message,
1627 };
1628
1629 let tx = transactions::send::make_and_sign_transaction(
1630 signer,
1631 metadata.sender_address,
1632 metadata.nonce,
1633 metadata.expiry,
1634 metadata.energy,
1635 transactions::Payload::Update { payload },
1636 );
1637 Ok(tx)
1638 }
1639}
1640
1641pub struct ContractUpdateInner {
1644 return_value: Option<ReturnValue>,
1645 events: Vec<Upward<ContractTraceElement>>,
1646}
1647
1648pub type ContractUpdateBuilder = TransactionBuilder<true, ContractUpdateInner>;
1650
1651impl ContractUpdateBuilder {
1652 pub async fn send(
1655 self,
1656 signer: &impl transactions::ExactSizeTransactionSigner,
1657 ) -> v2::QueryResult<ContractUpdateHandle> {
1658 self.send_inner(signer, |tx_hash, client| ContractUpdateHandle {
1659 tx_hash,
1660 client,
1661 })
1662 .await
1663 }
1664
1665 pub fn return_value(&self) -> Option<&ReturnValue> {
1667 self.inner.return_value.as_ref()
1668 }
1669
1670 pub fn events(&self) -> &[Upward<ContractTraceElement>] {
1676 &self.inner.events
1677 }
1678}
1679
1680pub struct ContractUpdateHandle {
1686 tx_hash: TransactionHash,
1687 client: v2::Client,
1688}
1689
1690impl std::fmt::Display for ContractUpdateHandle {
1693 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1694 self.tx_hash.fmt(f)
1695 }
1696}
1697
1698#[derive(Debug, thiserror::Error)]
1699pub enum ContractUpdateError {
1702 #[error("The status of the transaction could not be ascertained: {0}")]
1703 Query(#[from] QueryError),
1704 #[error("Contract update failed with reason: {0:?}")]
1705 Failed(v2::Upward<RejectReason>),
1706}
1707
1708impl ContractUpdateHandle {
1709 pub fn hash(&self) -> TransactionHash {
1711 self.tx_hash
1712 }
1713
1714 pub async fn wait_for_finalization(
1717 mut self,
1718 ) -> Result<ContractUpdateInfo, ContractUpdateError> {
1719 let (_, result) = self.client.wait_until_finalized(&self.tx_hash).await?;
1720
1721 let mk_error = |msg| {
1722 Err(ContractUpdateError::from(QueryError::RPCError(
1723 RPCError::CallError(tonic::Status::invalid_argument(msg)),
1724 )))
1725 };
1726 let v2::upward::Upward::Known(details) = result.details else {
1727 return mk_error(
1728 "Expected smart contract update status, but received unknown block item details.",
1729 );
1730 };
1731 match details {
1732 crate::types::BlockItemSummaryDetails::AccountTransaction(at) => {
1733 let v2::upward::Upward::Known(effects) = at.effects else {
1734 return mk_error(
1735 "Expected smart contract update status, but received unknown block item \
1736 effects.",
1737 );
1738 };
1739 match effects {
1740 AccountTransactionEffects::ContractUpdateIssued { effects } => {
1741 let Some(execution_tree) = crate::types::execution_tree(effects) else {
1742 return mk_error(
1743 "Expected smart contract update, but received invalid execution \
1744 tree.",
1745 );
1746 };
1747 Ok(ContractUpdateInfo {
1748 execution_tree,
1749 energy_cost: result.energy_cost,
1750 cost: at.cost,
1751 transaction_hash: self.tx_hash,
1752 sender: at.sender,
1753 })
1754 }
1755 AccountTransactionEffects::None {
1756 transaction_type: _,
1757 reject_reason,
1758 } => Err(ContractUpdateError::Failed(reject_reason)),
1759 _ => mk_error("Expected smart contract update status, but did not receive it."),
1760 }
1761 }
1762 crate::types::BlockItemSummaryDetails::AccountCreation(_) => {
1763 mk_error("Expected smart contract update status, but received account creation.")
1764 }
1765 crate::types::BlockItemSummaryDetails::Update(_) => mk_error(
1766 "Expected smart contract update status, but received chain update instruction.",
1767 ),
1768 crate::types::BlockItemSummaryDetails::TokenCreationDetails(_) => mk_error(
1769 "Expected smart contract update status, but received token creation chain update \
1770 instruction.",
1771 ),
1772 }
1773 }
1774
1775 pub async fn wait_for_finalization_timeout(
1778 self,
1779 timeout: std::time::Duration,
1780 ) -> Result<ContractUpdateInfo, ContractUpdateError> {
1781 let result = tokio::time::timeout(timeout, self.wait_for_finalization()).await;
1782 match result {
1783 Ok(r) => r,
1784 Err(_elapsed) => Err(ContractUpdateError::Query(QueryError::RPCError(
1785 RPCError::CallError(tonic::Status::deadline_exceeded(
1786 "Deadline waiting for result of transaction is exceeded.",
1787 )),
1788 ))),
1789 }
1790 }
1791}