diff --git a/CHANGELOG.md b/CHANGELOG.md index 43069d77..46a7f63a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased changes +## 4.3.1 +- Added + - Enhanced error handling for FFI calls and exposing detailed exceptions to the client, providing information about potential errors. + ## 4.3.0 - Bugfix - Switched the GitHub runners from using 'ubuntu-latest' to 'ubuntu-20.04' to ensure compatibility with the default .NET 6 Docker image for the SDK. diff --git a/concordium-base b/concordium-base index cfe43b79..872e8590 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit cfe43b798b369d7da2a949b4a200ad1848da0455 +Subproject commit 872e8590b3de8391a854a5f6282ac49e2005a520 diff --git a/rust-bindings/Cargo.lock b/rust-bindings/Cargo.lock index e60e2d84..1efcef23 100644 --- a/rust-bindings/Cargo.lock +++ b/rust-bindings/Cargo.lock @@ -66,9 +66,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.9.0" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -120,11 +120,12 @@ dependencies = [ [[package]] name = "bs58" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" dependencies = [ "sha2", + "tinyvec", ] [[package]] @@ -192,7 +193,7 @@ dependencies = [ [[package]] name = "concordium-contracts-common" -version = "8.1.1" +version = "9.0.0" dependencies = [ "base64", "bs58", @@ -235,12 +236,23 @@ dependencies = [ ] [[package]] -name = "digest" -version = "0.9.0" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", ] [[package]] @@ -383,12 +395,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -524,6 +530,7 @@ dependencies = [ "hex", "serde", "serde_json", + "thiserror", ] [[package]] @@ -587,15 +594,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.9.9" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "block-buffer", "cfg-if", "cpufeatures", "digest", - "opaque-debug", ] [[package]] diff --git a/rust-bindings/Cargo.toml b/rust-bindings/Cargo.toml index 16528ce3..4c445843 100644 --- a/rust-bindings/Cargo.toml +++ b/rust-bindings/Cargo.toml @@ -11,13 +11,14 @@ name="rust_bindings" crate-type = ["cdylib"] [dependencies] +thiserror = "1.0" anyhow="1.0" serde_json = "1.0" serde = { version = "1.0" } -concordium-contracts-common = { version = "8.1.1", features = ["derive-serde"], path = "../concordium-base/smart-contracts/contracts-common/concordium-contracts-common" } +concordium-contracts-common = { version = "9.0.0", features = ["derive-serde"], path = "../concordium-base/smart-contracts/contracts-common/concordium-contracts-common" } [dev-dependencies] hex = "0.4" [profile.release] -rpath = true \ No newline at end of file +rpath = true diff --git a/rust-bindings/src/lib.rs b/rust-bindings/src/lib.rs index ce1c534e..016c0ace 100644 --- a/rust-bindings/src/lib.rs +++ b/rust-bindings/src/lib.rs @@ -1,10 +1,12 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use concordium_contracts_common::{ - schema::{Type, VersionedModuleSchema}, + schema::{Type, VersionedModuleSchema, VersionedSchemaError}, + schema_json::ToJsonError, Cursor, }; use serde_json::to_vec; use std::{ffi::CStr, os::raw::c_char}; +use thiserror::Error; pub type JsonString = String; @@ -49,7 +51,7 @@ pub unsafe extern "C" fn schema_display( schema_size: i32, schema_version: FFIByteOption, callback: ResultCallback, -) -> bool { +) -> u16 { let schema = std::slice::from_raw_parts(schema_ptr, schema_size as usize); assign_result(callback, || { schema_display_aux(schema, schema_version.into_option()) @@ -88,7 +90,7 @@ pub unsafe extern "C" fn get_receive_contract_parameter( value_ptr: *const u8, value_size: i32, callback: ResultCallback, -) -> bool { +) -> u16 { assign_result(callback, || { let schema = std::slice::from_raw_parts(schema_ptr, schema_size as usize); let contract_name_str = get_str_from_pointer(contract_name)?; @@ -132,7 +134,7 @@ pub unsafe extern "C" fn get_event_contract( value_ptr: *const u8, value_size: i32, callback: ResultCallback, -) -> bool { +) -> u16 { assign_result(callback, || { let schema = std::slice::from_raw_parts(schema_ptr, schema_size as usize); let contract_name_str = get_str_from_pointer(contract_name)?; @@ -158,20 +160,20 @@ pub unsafe extern "C" fn get_event_contract( /// # Returns /// /// A boolean, that indicates whether the computation was successful or not. -fn assign_result Result>>(callback: ResultCallback, f: F) -> bool { +fn assign_result Result, FFIError>>(callback: ResultCallback, f: F) -> u16 { match f() { Ok(output) => { let out_lenght = output.len() as i32; let ptr = output.as_ptr(); callback(ptr, out_lenght); - true + 0 } Err(e) => { let error = format!("{}", e).into_bytes(); let error_length = error.len() as i32; let ptr = error.as_ptr(); callback(ptr, error_length); - false + e.to_int() } } } @@ -182,45 +184,92 @@ pub fn get_receive_contract_parameter_aux( contract_name: &str, entrypoint: &str, value: &[u8], -) -> Result> { +) -> Result, FFIError> { let module_schema = VersionedModuleSchema::new(schema, &schema_version)?; let parameter_type = module_schema.get_receive_param_schema(contract_name, entrypoint)?; - let deserialized = deserialize_type_value(value, ¶meter_type, true)?; + let deserialized = deserialize_type_value(value, ¶meter_type)?; Ok(deserialized) } -fn schema_display_aux(schema: &[u8], schema_version: Option) -> Result> { +fn schema_display_aux(schema: &[u8], schema_version: Option) -> Result, FFIError> { let display = VersionedModuleSchema::new(schema, &schema_version)?; Ok(display.to_string().into_bytes()) } +#[derive(Error, Debug)] +pub enum FFIError { + #[error("{0}")] + JsonError(String), + #[error("error when using serde")] + SerdeJsonError, + #[error("encountered string which wasn't utf8 encoded")] + Utf8Error, + #[error(transparent)] + VersionedSchemaError(#[from] VersionedSchemaError), +} + +impl FFIError { + /// The enumeration starts a 1 since return value 0 indicating a successfull + /// FFI call. + fn to_int(&self) -> u16 { + match self { + FFIError::JsonError(_) => 1, + FFIError::SerdeJsonError => 2, + FFIError::Utf8Error => 3, + FFIError::VersionedSchemaError(schema_error) => match schema_error { + VersionedSchemaError::ParseError => 4, + VersionedSchemaError::MissingSchemaVersion => 5, + VersionedSchemaError::InvalidSchemaVersion => 6, + VersionedSchemaError::NoContractInModule => 7, + VersionedSchemaError::NoReceiveInContract => 8, + VersionedSchemaError::NoInitInContract => 9, + VersionedSchemaError::NoParamsInReceive => 10, + VersionedSchemaError::NoParamsInInit => 11, + VersionedSchemaError::NoErrorInReceive => 12, + VersionedSchemaError::NoErrorInInit => 13, + VersionedSchemaError::ErrorNotSupported => 14, + VersionedSchemaError::NoReturnValueInReceive => 15, + VersionedSchemaError::ReturnValueNotSupported => 16, + VersionedSchemaError::NoEventInContract => 17, + VersionedSchemaError::EventNotSupported => 18, + }, + } + } +} + +impl From for FFIError { + fn from(_: std::str::Utf8Error) -> Self { FFIError::Utf8Error } +} + +impl From for FFIError { + fn from(_: serde_json::Error) -> Self { FFIError::SerdeJsonError } +} + +impl From for FFIError { + fn from(value: ToJsonError) -> Self { FFIError::JsonError(value.display(true)) } +} + fn get_event_contract_aux( schema: &[u8], schema_version: Option, contract_name: &str, value: &[u8], -) -> Result> { +) -> Result, FFIError> { let module_schema = VersionedModuleSchema::new(schema, &schema_version)?; let parameter_type = module_schema.get_event_schema(contract_name)?; - let deserialized = deserialize_type_value(value, ¶meter_type, true)?; + let deserialized = deserialize_type_value(value, ¶meter_type)?; Ok(deserialized) } -fn deserialize_type_value( - value: &[u8], - value_type: &Type, - verbose_error_message: bool, -) -> Result> { +fn deserialize_type_value(value: &[u8], value_type: &Type) -> Result, FFIError> { let mut cursor = Cursor::new(value); - match value_type.to_json(&mut cursor) { - Ok(v) => Ok(to_vec(&v)?), - Err(e) => Err(anyhow!("{}", e.display(verbose_error_message))), - } + let v = value_type.to_json(&mut cursor)?; + Ok(to_vec(&v)?) } /// The provided raw pointer [`c_char`] must be a [`std::ffi::CString`]. /// The content of the pointer [`c_char`] must not be mutated for the duration /// of lifetime 'a. -fn get_str_from_pointer<'a>(input: *const c_char) -> Result<&'a str> { +fn get_str_from_pointer<'a>(input: *const c_char) -> Result<&'a str, FFIError> { let c_str: &CStr = unsafe { CStr::from_ptr(input) }; Ok(c_str.to_str()?) } diff --git a/src/Concordium.Sdk.csproj b/src/Concordium.Sdk.csproj index 7076476c..f975b568 100644 --- a/src/Concordium.Sdk.csproj +++ b/src/Concordium.Sdk.csproj @@ -15,7 +15,7 @@ concordium;concordium-net-sdk;blockchain;sdk; Concordium ConcordiumNetSdk - 4.3.0 + 4.3.1 icon.png README.md MPL-2.0 diff --git a/src/Exceptions/InteropBindingException.cs b/src/Exceptions/InteropBindingException.cs deleted file mode 100644 index ab27186d..00000000 --- a/src/Exceptions/InteropBindingException.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text; - -namespace Concordium.Sdk.Exceptions; - -/// -/// Thrown when a interop call failed with possible error as message. -/// -internal sealed class InteropBindingException : Exception -{ - private const string EmptyErrorMessage = "Empty error message returned"; - - internal static InteropBindingException Create(byte[]? message) => - message != null ? new InteropBindingException(Encoding.UTF8.GetString(message)) : Empty(); - - private InteropBindingException(string message) : base(message) - { } - - private static InteropBindingException Empty() => new(EmptyErrorMessage); -} - diff --git a/src/Exceptions/SchemaJsonException.cs b/src/Exceptions/SchemaJsonException.cs new file mode 100644 index 00000000..5c51cf71 --- /dev/null +++ b/src/Exceptions/SchemaJsonException.cs @@ -0,0 +1,23 @@ +using System.Text; +using Concordium.Sdk.Interop; + +namespace Concordium.Sdk.Exceptions; + +/// +/// Thrown when a interop call failed with possible error as message. +/// +public sealed class SchemaJsonException : Exception +{ + private const string EmptyErrorMessage = "Empty error message returned"; + /// + /// Type of error + /// + public SchemaJsonResult SchemaJsonResult { get; } + + internal static SchemaJsonException Create(SchemaJsonResult schemaJsonResult, byte[]? message) => + message != null ? new SchemaJsonException(schemaJsonResult, Encoding.UTF8.GetString(message)) : Empty(schemaJsonResult); + + private SchemaJsonException(SchemaJsonResult schemaJsonResult, string message) : base(message) => this.SchemaJsonResult = schemaJsonResult; + + private static SchemaJsonException Empty(SchemaJsonResult schemaJsonResult) => new(schemaJsonResult, EmptyErrorMessage); +} diff --git a/src/Interop/InteropBinding.cs b/src/Interop/InteropBinding.cs index 49006135..54f02d39 100644 --- a/src/Interop/InteropBinding.cs +++ b/src/Interop/InteropBinding.cs @@ -15,14 +15,14 @@ internal static class InteropBinding private const string DllName = "rust_bindings"; [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "schema_display")] - private static extern bool SchemaDisplay( + private static extern SchemaJsonResult SchemaDisplay( [MarshalAs(UnmanagedType.LPArray)] byte[] schema, int schema_size, FfiByteOption schema_version, [MarshalAs(UnmanagedType.FunctionPtr)] SetResultCallback callback); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "get_receive_contract_parameter")] - private static extern bool GetReceiveContractParameter( + private static extern SchemaJsonResult GetReceiveContractParameter( [MarshalAs(UnmanagedType.LPArray)] byte[] schema, int schema_size, FfiByteOption schema_version, [MarshalAs(UnmanagedType.LPUTF8Str)] string contract_name, @@ -32,7 +32,7 @@ private static extern bool GetReceiveContractParameter( [MarshalAs(UnmanagedType.FunctionPtr)] SetResultCallback callback); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "get_event_contract")] - private static extern bool GetEventContract( + private static extern SchemaJsonResult GetEventContract( [MarshalAs(UnmanagedType.LPArray)] byte[] schema, int schema_size, FfiByteOption schema_version, @@ -63,12 +63,12 @@ internal static Utf8Json SchemaDisplay(VersionedModuleSchema schema) Marshal.Copy(ptr, result, 0, size); }); - if (schemaDisplay && result != null) + if (!schemaDisplay.IsError() && result != null) { return new Utf8Json(result); } - var interopException = InteropBindingException.Create(result); + var interopException = SchemaJsonException.Create(schemaDisplay, result); throw interopException; } @@ -97,12 +97,12 @@ internal static Utf8Json GetReceiveContractParameter(VersionedModuleSchema schem Marshal.Copy(ptr, result, 0, size); }); - if (receiveContractParameter && result != null) + if (!receiveContractParameter.IsError() && result != null) { return new Utf8Json(result); } - var interopException = InteropBindingException.Create(result); + var interopException = SchemaJsonException.Create(receiveContractParameter, result); throw interopException; } @@ -127,12 +127,12 @@ internal static Utf8Json GetEventContract(VersionedModuleSchema schema, Contract Marshal.Copy(ptr, result, 0, size); }); - if (schemaDisplay && result != null) + if (!schemaDisplay.IsError() && result != null) { return new Utf8Json(result); } - var interopException = InteropBindingException.Create(result); + var interopException = SchemaJsonException.Create(schemaDisplay, result); throw interopException; } diff --git a/src/Interop/SchemaJsonResult.cs b/src/Interop/SchemaJsonResult.cs new file mode 100644 index 00000000..1a399b6d --- /dev/null +++ b/src/Interop/SchemaJsonResult.cs @@ -0,0 +1,91 @@ +namespace Concordium.Sdk.Interop; + +/// +/// Result type which on errors hold error type information. +/// +public enum SchemaJsonResult +{ + /// + /// No error + /// + NoError = 0, + /// + /// Represents errors occurring while deserializing to the schema JSON format. + /// + JsonError = 1, + /// + /// This type represents all possible errors that can occur when serializing or + /// deserializing JSON data. + /// + SerdeJsonError = 2, + /// + /// Errors which can occur when attempting to interpret a sequence of bytes + /// as a string. + /// + Utf8Error = 3, + /// + /// Versioned Schema Error - Parse error + /// + VersionedSchemaErrorParseError = 4, + /// + /// Versioned Schema Error - Missing Schema Version + /// + VersionedSchemaErrorMissingSchemaVersion = 5, + /// + /// Versioned Schema Error - Invalid Schema Version + /// + VersionedSchemaErrorInvalidSchemaVersion = 6, + /// + /// Versioned Schema Error - No Contract In Module + /// + VersionedSchemaErrorNoContractInModule = 7, + /// + /// Versioned Schema Error - Receive function schema not found in contract schema + /// + VersionedSchemaErrorNoReceiveInContract = 8, + /// + /// Versioned Schema Error - Init function schema not found in contract schema + /// + VersionedSchemaErrorNoInitInContract = 9, + /// + /// Versioned Schema Error - Receive function schema does not contain a parameter schema + /// + VersionedSchemaErrorNoParamsInReceive = 10, + /// + /// Versioned Schema Error - Init function schema does not contain a parameter schema + /// + VersionedSchemaErrorNoParamsInInit = 11, + /// + /// Versioned Schema Error - Receive function schema not found in contract schema + /// + VersionedSchemaErrorNoErrorInReceive = 12, + /// + /// Versioned Schema Error - Init function schema does not contain an error schema + /// + VersionedSchemaErrorNoErrorInInit = 13, + /// + /// Versioned Schema Error - Errors not supported for this module version + /// + VersionedSchemaErrorErrorNotSupported = 14, + /// + /// Versioned Schema Error - Receive function schema has no return value schema + /// + VersionedSchemaErrorNoReturnValueInReceive = 15, + /// + /// Versioned Schema Error - Return values not supported for this module version + /// + VersionedSchemaErrorReturnValueNotSupported = 16, + /// + /// Versioned Schema Error - Event schema not found in contract schema + /// + VersionedSchemaErrorNoEventInContract = 17, + /// + /// Versioned Schema Error - Events not supported for this module version + /// + VersionedSchemaErrorEventNotSupported = 18, +} + +internal static class ErrorExtensions +{ + internal static bool IsError(this SchemaJsonResult schemaJsonResult) => schemaJsonResult != SchemaJsonResult.NoError; +} diff --git a/src/Types/ContractEvent.cs b/src/Types/ContractEvent.cs index d75002c2..dfdac385 100644 --- a/src/Types/ContractEvent.cs +++ b/src/Types/ContractEvent.cs @@ -19,7 +19,7 @@ public sealed record ContractEvent(byte[] Bytes) /// Module schema in hexadecimal. /// Contract name. /// deserialized as json uft8 encoded. - /// Thrown when event wasn't able to be deserialized from schema. + /// Thrown when event wasn't able to be deserialized from schema. public Utf8Json GetDeserializeEvent( VersionedModuleSchema schema, ContractIdentifier contractName diff --git a/src/Types/ContractInitializedEvent.cs b/src/Types/ContractInitializedEvent.cs index c9531c49..8bce0a6b 100644 --- a/src/Types/ContractInitializedEvent.cs +++ b/src/Types/ContractInitializedEvent.cs @@ -45,7 +45,7 @@ internal static ContractInitializedEvent From(Grpc.V2.ContractInitializedEvent i /// /// Module schema in hexadecimal. /// List of deserialized json uft8 encoded events. Possible null if this was returned from deserialization. - /// Thrown if an event wasn't able to be deserialized from schema. + /// Thrown if an event wasn't able to be deserialized from schema. public IList GetDeserializedEvents(VersionedModuleSchema schema) { var deserialized = new List(this.Events.Count); diff --git a/src/Types/ContractTraceElement.cs b/src/Types/ContractTraceElement.cs index f99df036..730d2c5d 100644 --- a/src/Types/ContractTraceElement.cs +++ b/src/Types/ContractTraceElement.cs @@ -79,7 +79,7 @@ public sealed record Updated( /// /// Versioned module schema. /// deserialized as json uft8 encoded. - /// Thrown when message wasn't able to be deserialized form schema. + /// Thrown when message wasn't able to be deserialized form schema. public Utf8Json GetDeserializeMessage(VersionedModuleSchema schema) => GetDeserializeMessage(schema, this.ReceiveName.GetContractName(), this.ReceiveName.GetEntrypoint(), this.Message); @@ -91,7 +91,7 @@ public Utf8Json GetDeserializeMessage(VersionedModuleSchema schema) => /// Entrypoint on contract. /// Message to entrypoint. /// deserialized as json uft8 encoded. - /// Thrown when message wasn't able to be deserialized from schema. + /// Thrown when message wasn't able to be deserialized from schema. public static Utf8Json GetDeserializeMessage( VersionedModuleSchema schema, ContractIdentifier contractIdentifier, @@ -105,7 +105,7 @@ Parameter message /// /// Module schema. /// List of deserialized json uft8 encoded events. Possible null if this was returned from deserialization. - /// Thrown if an event wasn't able to be deserialized from schema. + /// Thrown if an event wasn't able to be deserialized from schema. public IList GetDeserializedEvents(VersionedModuleSchema schema) { var deserialized = new List(this.Events.Count); @@ -141,7 +141,7 @@ public sealed record Interrupted(ContractAddress Address, IList E /// Module schema. /// Contract name. /// List of deserialized json uft8 encoded events. Possible null if this was returned from deserialization. - /// Thrown if an event wasn't able to be deserialized from schema. + /// Thrown if an event wasn't able to be deserialized from schema. public IList GetDeserializedEvents(VersionedModuleSchema schema, ContractIdentifier contractName) { var deserialized = new List(this.Events.Count); diff --git a/src/Types/ProtocolVersion.cs b/src/Types/ProtocolVersion.cs index ab5b51a5..ca768737 100644 --- a/src/Types/ProtocolVersion.cs +++ b/src/Types/ProtocolVersion.cs @@ -34,7 +34,12 @@ public enum ProtocolVersion P5, /// Protocol `P6` is a future protocol update that will introduce a new /// consensus. - P6 + P6, + /// + /// Protocol `P7` modifies hashing to better support light clients, and + /// implements tokenomics changes. + /// + P7, } @@ -60,6 +65,7 @@ internal static ProtocolVersion Into(this Grpc.V2.ProtocolVersion protocolVersio Grpc.V2.ProtocolVersion._4 => ProtocolVersion.P4, Grpc.V2.ProtocolVersion._5 => ProtocolVersion.P5, Grpc.V2.ProtocolVersion._6 => ProtocolVersion.P6, + Grpc.V2.ProtocolVersion._7 => ProtocolVersion.P7, _ => throw new MissingEnumException(protocolVersion) }; } diff --git a/src/Types/RejectReason.cs b/src/Types/RejectReason.cs index f709dbd8..837bc7d2 100644 --- a/src/Types/RejectReason.cs +++ b/src/Types/RejectReason.cs @@ -194,7 +194,7 @@ public sealed record RejectedReceive(int RejectReason, ContractAddress ContractA /// /// Versioned module schema. /// deserialized as json uft8 encoded. - /// Thrown when message wasn't able to be deserialized using the schema. + /// Thrown when message wasn't able to be deserialized using the schema. public Utf8Json GetDeserializeMessage(VersionedModuleSchema schema) => Updated.GetDeserializeMessage(schema, this.ReceiveName.GetContractName(), this.ReceiveName.GetEntrypoint(), this.Parameter); } diff --git a/src/Types/VersionedModuleSchema.cs b/src/Types/VersionedModuleSchema.cs index 40a4d18b..767cd8cf 100644 --- a/src/Types/VersionedModuleSchema.cs +++ b/src/Types/VersionedModuleSchema.cs @@ -21,6 +21,6 @@ public sealed record VersionedModuleSchema(byte[] Schema, ModuleSchemaVersion Ve /// Deserialize schema. /// /// Schema as json uft8 encoded. - /// Thrown when schema wasn't able to be deserialized. + /// Thrown when schema wasn't able to be deserialized. public Utf8Json GetDeserializedSchema() => InteropBinding.SchemaDisplay(this); }; diff --git a/tests/UnitTests/Interop/InteropBindingTests.cs b/tests/UnitTests/Interop/InteropBindingTests.cs index bf08e6fc..492b719e 100644 --- a/tests/UnitTests/Interop/InteropBindingTests.cs +++ b/tests/UnitTests/Interop/InteropBindingTests.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; +using Concordium.Sdk.Exceptions; using Concordium.Sdk.Interop; using Concordium.Sdk.Types; using FluentAssertions; @@ -44,6 +45,23 @@ await Verifier.Verify(message.ToString()) .UseDirectory("__snapshots__"); } + [Fact] + public async Task GivenBadSchema_WhenSchemaDisplay_ThenThrowExceptionWithParseError() + { + // Arrange + var schema = Convert.FromHexString((await File.ReadAllTextAsync("./Data/cis2-nft-schema")).Trim()); + var versionedModuleSchema = new VersionedModuleSchema(schema[..^3], ModuleSchemaVersion.V1); // Bad schema + + // Act + var action = () => InteropBinding.SchemaDisplay(versionedModuleSchema); + + // Assert + action.Should().Throw() + .Where(e => + e.SchemaJsonResult == SchemaJsonResult.VersionedSchemaErrorParseError && + e.Message.Equals("Parse error", StringComparison.Ordinal)); + } + [Fact] public async Task WhenDisplayReceiveParam_ThenReturnParams() { @@ -66,6 +84,95 @@ await Verifier.Verify(message.ToString()) .UseDirectory("__snapshots__"); } + [Fact] + public async Task GivenBadReceiveParam_WhenDisplayReceiveParam_ThenThrowException() + { + // Arrange + var schema = (await File.ReadAllTextAsync("./Data/cis2_wCCD_sub")).Trim(); + const string contractName = "cis2_wCCD"; + const string entrypoint = "wrap"; + var value = Convert.FromHexString("005f8b99a3ea8089002291fd646554848b00e7a0cd934e5bad6e6e93a4d4f4dc790000"); + var versionedModuleSchema = new VersionedModuleSchema(Convert.FromHexString(schema), ModuleSchemaVersion.Undefined); + var parameter = new Parameter(value[..^3]); // Bad parameter + var contractIdentifier = new ContractIdentifier(contractName); + var entryPoint = new EntryPoint(entrypoint); + + // Act + var action = () => InteropBinding.GetReceiveContractParameter(versionedModuleSchema, contractIdentifier, entryPoint, parameter); + + // Assert + action.Should().Throw() + .Where(e => + e.SchemaJsonResult == SchemaJsonResult.JsonError && + e.Message.StartsWith("Failed to deserialize AccountAddress due to: Could not parse AccountAddress", StringComparison.InvariantCulture)); + } + + [Fact] + public async Task GivenBadContractIdentifier_WhenDisplayReceiveParam_ThenThrowException() + { + // Arrange + var schema = (await File.ReadAllTextAsync("./Data/cis2_wCCD_sub")).Trim(); + const string contractName = "cis2_wCCD"; + const string entrypoint = "wrap"; + var versionedModuleSchema = new VersionedModuleSchema(Convert.FromHexString(schema), ModuleSchemaVersion.Undefined); + var parameter = new Parameter(Convert.FromHexString("005f8b99a3ea8089002291fd646554848b00e7a0cd934e5bad6e6e93a4d4f4dc790000")); + var contractIdentifier = new ContractIdentifier(contractName[..^3]); // Bad contract identifier + var entryPoint = new EntryPoint(entrypoint); + + // Act + var action = () => InteropBinding.GetReceiveContractParameter(versionedModuleSchema, contractIdentifier, entryPoint, parameter); + + // Assert + action.Should().Throw() + .Where(e => + e.SchemaJsonResult == SchemaJsonResult.VersionedSchemaErrorNoContractInModule && + e.Message.Equals("Unable to find contract schema in module schema", StringComparison.Ordinal)); + } + + [Fact] + public async Task GivenBadEntrypoint_WhenDisplayReceiveParam_ThenThrowException() + { + // Arrange + var schema = (await File.ReadAllTextAsync("./Data/cis2_wCCD_sub")).Trim(); + const string contractName = "cis2_wCCD"; + const string entrypoint = "wrap"; + var versionedModuleSchema = new VersionedModuleSchema(Convert.FromHexString(schema), ModuleSchemaVersion.Undefined); + var parameter = new Parameter(Convert.FromHexString("005f8b99a3ea8089002291fd646554848b00e7a0cd934e5bad6e6e93a4d4f4dc790000")); + var contractIdentifier = new ContractIdentifier(contractName); + var entryPoint = new EntryPoint(entrypoint[..^2]); // Bad entrypoint + + // Act + var action = () => InteropBinding.GetReceiveContractParameter(versionedModuleSchema, contractIdentifier, entryPoint, parameter); + + // Assert + action.Should().Throw() + .Where(e => + e.SchemaJsonResult == SchemaJsonResult.VersionedSchemaErrorNoReceiveInContract && + e.Message.Equals("Receive function schema not found in contract schema", StringComparison.Ordinal)); + } + + [Fact] + public async Task GivenBadSchema_WhenDisplayReceiveParam_ThenThrowException() + { + // Arrange + var schema = Convert.FromHexString((await File.ReadAllTextAsync("./Data/cis2_wCCD_sub")).Trim()); + const string contractName = "cis2_wCCD"; + const string entrypoint = "wrap"; + var versionedModuleSchema = new VersionedModuleSchema(schema[30..], ModuleSchemaVersion.Undefined); // Bad schema + var parameter = new Parameter(Convert.FromHexString("005f8b99a3ea8089002291fd646554848b00e7a0cd934e5bad6e6e93a4d4f4dc790000")); + var contractIdentifier = new ContractIdentifier(contractName); + var entryPoint = new EntryPoint(entrypoint[..^2]); + + // Act + var action = () => InteropBinding.GetReceiveContractParameter(versionedModuleSchema, contractIdentifier, entryPoint, parameter); + + // Assert + action.Should().Throw() + .Where(e => + e.SchemaJsonResult == SchemaJsonResult.VersionedSchemaErrorMissingSchemaVersion && + e.Message.Equals("Missing Schema Version", StringComparison.Ordinal)); + } + [Fact] public async Task WhenDisplayEvent_ThenReturnEvent() { @@ -86,6 +193,69 @@ await Verifier.Verify(message.ToString()) .UseDirectory("__snapshots__"); } + [Fact] + public async Task GivenBadSchema_WhenDisplayEvent_ThenThrowException() + { + // Arrange + var schema = Convert.FromHexString((await File.ReadAllTextAsync("./Data/cis2_wCCD_sub")).Trim()); + const string contractName = "cis2_wCCD"; + const string value = "fe00c0843d005f8b99a3ea8089002291fd646554848b00e7a0cd934e5bad6e6e93a4d4f4dc79"; + var versionedModuleSchema = new VersionedModuleSchema(schema[..^3], ModuleSchemaVersion.Undefined); // Bad schema + var contractIdentifier = new ContractIdentifier(contractName); + var contractEvent = new ContractEvent(Convert.FromHexString(value)); + + // Act + var action = () => InteropBinding.GetEventContract(versionedModuleSchema, contractIdentifier, contractEvent); + + // Assert + action.Should().Throw() + .Where(e => + e.SchemaJsonResult == SchemaJsonResult.VersionedSchemaErrorMissingSchemaVersion && + e.Message.Equals("Missing Schema Version", StringComparison.Ordinal)); + } + + [Fact] + public async Task GivenBadContractIdentifier_WhenDisplayEvent_ThenThrowException() + { + // Arrange + var schema = (await File.ReadAllTextAsync("./Data/cis2_wCCD_sub")).Trim(); + const string contractName = "cis2_wCCD"; + const string value = "fe00c0843d005f8b99a3ea8089002291fd646554848b00e7a0cd934e5bad6e6e93a4d4f4dc79"; + var versionedModuleSchema = new VersionedModuleSchema(Convert.FromHexString(schema), ModuleSchemaVersion.Undefined); + var contractIdentifier = new ContractIdentifier(contractName[..^3]); // Bad contract identifier + var contractEvent = new ContractEvent(Convert.FromHexString(value)); + + // Act + var action = () => InteropBinding.GetEventContract(versionedModuleSchema, contractIdentifier, contractEvent); + + // Assert + action.Should().Throw() + .Where(e => + e.SchemaJsonResult == SchemaJsonResult.VersionedSchemaErrorNoContractInModule && + e.Message.Equals("Unable to find contract schema in module schema", StringComparison.Ordinal)); + } + + [Fact] + public async Task GivenBadContractEvent_WhenDisplayEvent_ThenThrowException() + { + // Arrange + var schema = (await File.ReadAllTextAsync("./Data/cis2_wCCD_sub")).Trim(); + const string contractName = "cis2_wCCD"; + const string value = "fe00c0843d005f8b99a3ea8089002291fd646554848b00e7a0cd934e5bad6e6e93a4d4f4dc79"; + var versionedModuleSchema = new VersionedModuleSchema(Convert.FromHexString(schema), ModuleSchemaVersion.Undefined); + var contractIdentifier = new ContractIdentifier(contractName); + var contractEvent = new ContractEvent(Convert.FromHexString(value)[..^3]); // Bad contract event + + // Act + var action = () => InteropBinding.GetEventContract(versionedModuleSchema, contractIdentifier, contractEvent); + + // Assert + action.Should().Throw() + .Where(e => + e.SchemaJsonResult == SchemaJsonResult.JsonError && + e.Message.StartsWith("Failed to deserialize AccountAddress due to: Could not parse")); + } + [Theory] [InlineData(ModuleSchemaVersion.V0, (byte)0)] [InlineData(ModuleSchemaVersion.V2, (byte)2)]