From 66060144a00ab39130ba3668c240bb6a2c87268a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Holm=20Gj=C3=B8rup?= Date: Fri, 9 Feb 2024 12:03:19 +0100 Subject: [PATCH] Add SchemaType and method for constructing parameters using JSON --- CHANGELOG.md | 2 + rust-bindings/src/lib.rs | 43 +++++++++++++++++++ src/Client/ConcordiumClient.cs | 4 ++ src/Interop/InteropBinding.cs | 41 ++++++++++++++++++ src/Interop/SchemaJsonResult.cs | 6 ++- src/Types/Parameter.cs | 9 ++++ src/Types/SchemaType.cs | 39 +++++++++++++++++ .../UnitTests/Interop/InteropBindingTests.cs | 28 +++++++++++- ...wCCD-wrap-param-hex-from-json.verified.txt | 1 + 9 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 src/Types/SchemaType.cs create mode 100644 tests/UnitTests/Interop/__snapshots__/wCCD-wrap-param-hex-from-json.verified.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a99f1c4..b6d3deaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ - New transaction `InitContract` - Add `WaitUntilFinalized` method on `ConcordiumClient` for waiting for transactions to finalized. - Add `Parameter.UpdateJson` and `Parameter.InitJson` for constructing parameters for update and init transactions using JSON and smart contract module schemas (see the example in example/UpdateContractMint). + - Add class `SchemaType` for representing a single type in a module schema, such as the parameter. + - Add `Parameter.FromJson` for constructing parameters using `SchemaType` and JSON. ## 4.3.1 - Added diff --git a/rust-bindings/src/lib.rs b/rust-bindings/src/lib.rs index 54d93869..c7cd1972 100644 --- a/rust-bindings/src/lib.rs +++ b/rust-bindings/src/lib.rs @@ -1,5 +1,6 @@ use anyhow::Result; use concordium_contracts_common::{ + from_bytes, schema::{Type, VersionedModuleSchema, VersionedSchemaError}, schema_json, Cursor, }; @@ -239,6 +240,45 @@ pub unsafe extern "C" fn into_init_parameter( }) } +/// Convert some JSON representation into bytes using a smart contract schema +/// type. +/// +/// # Arguments +/// +/// * 'schema_type_ptr' - Pointer to smart contract schema type. +/// * 'schema_type_size' - The byte size of the smart contract schema type. +/// * 'json_ptr' - Pointer to the UTF8 encoded JSON parameter. +/// * 'json_size' - The byte size of the encoded JSON parameter. +/// * 'callback' - Callback which can be used to set resulting output +/// +/// # Returns +/// +/// 0 if the call succeeded otherwise the return value corresponds to some error +/// code. +/// +/// # Safety +/// +/// Every pointer provided as an argument is assumed to be alive for the +/// duration of the call. +#[no_mangle] +pub unsafe extern "C" fn schema_json_to_bytes( + schema_type_ptr: *const u8, + schema_type_size: i32, + json_ptr: *const u8, + json_size: i32, + callback: ResultCallback, +) -> u16 { + assign_result(callback, || { + let schema_type_bytes = + std::slice::from_raw_parts(schema_type_ptr, schema_type_size as usize); + let json_slice = std::slice::from_raw_parts(json_ptr, json_size as usize); + let parameter_schema_type: Type = + from_bytes(&schema_type_bytes).map_err(|_| FFIError::ParseSchemaType)?; + let json_value: serde_json::Value = serde_json::from_slice(json_slice)?; + Ok(parameter_schema_type.serial_value(&json_value)?) + }) +} + /// Compute result using the provided callback f, convert it into a C string and /// assign it to the provided target. /// @@ -298,6 +338,8 @@ enum FFIError { VersionedSchemaError(#[from] VersionedSchemaError), #[error(transparent)] FromJsonError(#[from] schema_json::JsonError), + #[error("error parsing the schema type")] + ParseSchemaType, } impl FFIError { @@ -326,6 +368,7 @@ impl FFIError { VersionedSchemaError::EventNotSupported => 18, }, FFIError::FromJsonError(_) => 19, + FFIError::ParseSchemaType => 20, } } } diff --git a/src/Client/ConcordiumClient.cs b/src/Client/ConcordiumClient.cs index 67a22c5e..f8e5ccc6 100644 --- a/src/Client/ConcordiumClient.cs +++ b/src/Client/ConcordiumClient.cs @@ -210,6 +210,10 @@ public async Task GetBlockItemStatusAsync(TransactionHash tr /// /// For production it is recommended to provide a with a timeout, /// since a faulty/misbehaving node could otherwise make this wait indefinitely. + /// + /// The with of NotFound is thrown + /// if the transaction is not known by the node. Sending a transaction right before calling this + /// method, might produce this exception, due to the node still processing the transaction. /// /// Transaction Hash which is included in blocks returned. /// Cancellation token diff --git a/src/Interop/InteropBinding.cs b/src/Interop/InteropBinding.cs index 7fdbaa7a..9a931c06 100644 --- a/src/Interop/InteropBinding.cs +++ b/src/Interop/InteropBinding.cs @@ -62,6 +62,14 @@ private static extern SchemaJsonResult IntoInitParameter( int json_size, [MarshalAs(UnmanagedType.FunctionPtr)] SetResultCallback callback); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "schema_json_to_bytes")] + private static extern SchemaJsonResult SchemaJsonToBytes( + [MarshalAs(UnmanagedType.LPArray)] byte[] schema_type, + int schema_type_size, + [MarshalAs(UnmanagedType.LPArray)] byte[] json_ptr, + int json_size, + [MarshalAs(UnmanagedType.FunctionPtr)] SetResultCallback callback); + /// /// Callback to set byte array result of interop call. /// @@ -236,6 +244,39 @@ Utf8Json json throw interopException; } + /// + /// Contruct a smart contract init parameter from a JSON string and the module schema. + /// + /// Smart contract schema type + /// JSON representation of the smart contract parameter + /// Smart contract parameter as bytes + internal static byte[] SchemaJsonToBytes( + SchemaType schemaType, + Utf8Json json + ) + { + var result = Array.Empty(); + + var statusCode = SchemaJsonToBytes( + schemaType.Type, + schemaType.Type.Length, + json.Bytes, + json.Bytes.Length, + (ptr, size) => + { + result = new byte[size]; + Marshal.Copy(ptr, result, 0, size); + }); + + if (!statusCode.IsError() && result != null) + { + return result; + } + + var interopException = SchemaJsonException.Create(statusCode, result); + throw interopException; + } + /// /// A C# layout which compiled to a C interpretable structure. This is used as an optional parameter. /// diff --git a/src/Interop/SchemaJsonResult.cs b/src/Interop/SchemaJsonResult.cs index 03d92398..85961218 100644 --- a/src/Interop/SchemaJsonResult.cs +++ b/src/Interop/SchemaJsonResult.cs @@ -86,7 +86,11 @@ public enum SchemaJsonResult : ushort /// /// Represents errors occurring while converting from the schema JSON format. /// - FromJsonError = 19 + FromJsonError = 19, + /// + /// Represents errors occurring parsing a smart contract schema type. + /// + ParseSchemaType = 20 } internal static class ErrorExtensions diff --git a/src/Types/Parameter.cs b/src/Types/Parameter.cs index 5e79bb56..66c3fd64 100644 --- a/src/Types/Parameter.cs +++ b/src/Types/Parameter.cs @@ -67,6 +67,15 @@ public static Parameter InitJson( Utf8Json jsonParameter ) => Interop.InteropBinding.IntoInitParameter(moduleSchema, contractName, jsonParameter); + /// + /// Create a parameter from JSON representation using the smart contract schema type. + /// + /// The smart contract schema type for the parameter. + /// The UTF8 encoding of the JSON representation of the smart contract parameter. + public static Parameter FromJson( + SchemaType schemaType, + Utf8Json jsonParameter + ) => new(Interop.InteropBinding.SchemaJsonToBytes(schemaType, jsonParameter)); /// /// Create a parameter from a byte array. diff --git a/src/Types/SchemaType.cs b/src/Types/SchemaType.cs new file mode 100644 index 00000000..7f99fa97 --- /dev/null +++ b/src/Types/SchemaType.cs @@ -0,0 +1,39 @@ +namespace Concordium.Sdk.Types; + +/// +/// Smart contract schema type. +/// This represents a single type as part of a smart contract module schema, and allows for +/// converting structure data, such as JSON, from and to the binary representation used by a +/// smart contract. +/// +public sealed record SchemaType(byte[] Type) : IEquatable +{ + /// Construct SchemaType from a HEX encoding. + public static SchemaType FromHexString(string hexString) + { + var value = Convert.FromHexString(hexString); + return new(value); + } + + /// Construct SchemaType from a base64 encoding. + public static SchemaType FromBase64String(string base64) + { + var value = Convert.FromBase64String(base64); + return new(value); + } + + /// Check for equality. + public bool Equals(SchemaType? other) => other != null && this.Type.SequenceEqual(other.Type); + + /// Gets hash code. + public override int GetHashCode() + { + var paramHash = Helpers.HashCode.GetHashCodeByteArray(this.Type); + return paramHash; + } + + /// + /// Convert schema type to hex string. + /// + public string ToHexString() => Convert.ToHexString(this.Type).ToLowerInvariant(); +} diff --git a/tests/UnitTests/Interop/InteropBindingTests.cs b/tests/UnitTests/Interop/InteropBindingTests.cs index 602aaa42..ec5ebc6a 100644 --- a/tests/UnitTests/Interop/InteropBindingTests.cs +++ b/tests/UnitTests/Interop/InteropBindingTests.cs @@ -263,7 +263,7 @@ public async Task WhenIntoReceiveParamFromJson_ThenReturnParams() 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, ModuleSchemaVersion.Undefined); // Bad schema + var versionedModuleSchema = new VersionedModuleSchema(schema, ModuleSchemaVersion.Undefined); var json = new Utf8Json(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(new { to = new @@ -286,6 +286,32 @@ await Verifier.Verify(parameter.ToHexString()) .UseDirectory("__snapshots__"); } + [Fact] + public async Task WhenParameterFromJson_ThenReturnBytes() + { + // Arrange + var wCcdWrapSchemaType = SchemaType.FromBase64String("FAACAAAAAgAAAHRvFQIAAAAHAAAAQWNjb3VudAEBAAAACwgAAABDb250cmFjdAECAAAADBYBBAAAAGRhdGEdAQ=="); + var json = new Utf8Json(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(new + { + to = new + { + Account = new string[] { + "4tUoKeVapaTwwi2yY3Vwe5auM65VL2vk31R3eVhTW94hnB159F" + }, + }, + data = "" + })); + + // Act + var bytes = InteropBinding.SchemaJsonToBytes(wCcdWrapSchemaType, json); + + // Assert + await Verifier.Verify(Convert.ToHexString(bytes)) + .UseFileName("wCCD-wrap-param-hex-from-json") + .UseDirectory("__snapshots__"); + } + + [Theory] [InlineData(ModuleSchemaVersion.V0, (byte)0)] diff --git a/tests/UnitTests/Interop/__snapshots__/wCCD-wrap-param-hex-from-json.verified.txt b/tests/UnitTests/Interop/__snapshots__/wCCD-wrap-param-hex-from-json.verified.txt new file mode 100644 index 00000000..b17a1fab --- /dev/null +++ b/tests/UnitTests/Interop/__snapshots__/wCCD-wrap-param-hex-from-json.verified.txt @@ -0,0 +1 @@ +00FFFA722D840687699743E5F1A1AD86113D0404115661AB09A3611BFFC1BDAABE0000 \ No newline at end of file