diff --git a/CHANGELOG.md b/CHANGELOG.md index d89cf710..b6d3deaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## Unreleased changes - Added - 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/ConcordiumNetSdk.sln b/ConcordiumNetSdk.sln index ed3e8bea..de06e00a 100644 --- a/ConcordiumNetSdk.sln +++ b/ConcordiumNetSdk.sln @@ -83,6 +83,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployModule", "examples\De EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InitContract", "examples\InitContract\InitContract.csproj", "{E68CBBAC-7BC9-46D3-AA04-2B7A62BD6921}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Transations.UpdateContractMint", "examples\UpdateContractMint\Transations.UpdateContractMint.csproj", "{A1B271B6-CC7B-4949-A3F5-4B7F1C2080CA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -245,6 +247,10 @@ Global {E68CBBAC-7BC9-46D3-AA04-2B7A62BD6921}.Debug|Any CPU.Build.0 = Debug|Any CPU {E68CBBAC-7BC9-46D3-AA04-2B7A62BD6921}.Release|Any CPU.ActiveCfg = Release|Any CPU {E68CBBAC-7BC9-46D3-AA04-2B7A62BD6921}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B271B6-CC7B-4949-A3F5-4B7F1C2080CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B271B6-CC7B-4949-A3F5-4B7F1C2080CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B271B6-CC7B-4949-A3F5-4B7F1C2080CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B271B6-CC7B-4949-A3F5-4B7F1C2080CA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -290,5 +296,6 @@ Global {DBFBB7D1-E82D-4380-8263-B4B0AC3A6266} = {FD2CDD9F-4650-4705-9CA2-98CC81F8891D} {D35681A3-04AE-41BA-86F3-3CF5369D6D97} = {FD2CDD9F-4650-4705-9CA2-98CC81F8891D} {E68CBBAC-7BC9-46D3-AA04-2B7A62BD6921} = {FD2CDD9F-4650-4705-9CA2-98CC81F8891D} + {A1B271B6-CC7B-4949-A3F5-4B7F1C2080CA} = {FD2CDD9F-4650-4705-9CA2-98CC81F8891D} EndGlobalSection EndGlobal diff --git a/examples/UpdateContractMint/Program.cs b/examples/UpdateContractMint/Program.cs new file mode 100644 index 00000000..f09dbc8b --- /dev/null +++ b/examples/UpdateContractMint/Program.cs @@ -0,0 +1,263 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using CommandLine; +using Concordium.Sdk.Client; +using Concordium.Sdk.Types; +using Concordium.Sdk.Wallets; + +namespace Transactions.UpdateContractMint; + +// We disable these warnings since CommandLine needs to set properties in options +// but we don't want to give default values. +#pragma warning disable CS8618 + +internal sealed class Options +{ + [Option( + 'k', + "keys", + HelpText = "Path to a file with contents that is in the Concordium browser wallet key export format.", + Required = true + )] + public string WalletKeysFile { get; set; } + [Option(HelpText = "URL representing the endpoint where the gRPC V2 API is served.", + Default = "https://grpc.testnet.concordium.com:20000/")] + public string Endpoint { get; set; } + [Option("contract", HelpText = "The index of the smart contract.", Default = "7936")] + public ulong Contract { get; set; } + [Option("token", HelpText = "Token ID to mint", Default = "00")] + public string TokenId { get; set; } + [Option("metadata-url", HelpText = "URL to the metadata", Default = "")] + public string MetadataUrl { get; set; } + [Option("max-energy", HelpText = "The maximum energy to spend on the module.", Default = 5000)] + public ulong MaxEnergy { get; set; } +} + +/// +/// Mint CIS-2 tokens for the 'cis2_multi' smart contract example. +/// +/// https://github.com/Concordium/concordium-rust-smart-contracts/blob/86511efac8e335abac66176df895c21a5cde252c/examples/cis2-multi/src/lib.rs +/// +/// This example demonstrates: +/// +/// - Submitting a transaction updating a smart contract, where the parameter is constructed +/// using a smart contract module schema. +/// - Waiting for the transaction to finalize. +/// - Checking the outcome and reading the logged events of the contract, parsing the events using +/// a smart contract module schema. +/// +/// The example assumes: +/// - You have your account key information stored in the Concordium browser wallet key export +/// format, and that a path pointing to it is supplied to it from the command line. +/// - The 'cis2_multi' smart contract example is deployed on chain (already on testnet) with a +/// module reference matching the value of `CIS2_MULTI_MODULE_REF`. +/// - You have the contract address of an instance of the 'cis2_multi' smart contract (On testnet +/// the contract with index 7936 can be used). +/// +internal class Program +{ + /// + /// Example send a contract update transaction. + /// + public static async Task Main(string[] args) => + await Parser.Default + .ParseArguments(args) + .WithParsedAsync(Run); + + /// Module reference for a module containing the 'cis2_multi' smart contract example + private static readonly string _cis2MultiModuleRef = + "755d0e1f2820a3285e23ac4fa1862ae2dfa75ab8927133904e04fea7e9f1f4c9"; + + /// Receive name for the 'mint' entrypoint of the contract. + private static readonly string _cis2MultiReceiveMint = "cis2_multi.mint"; + + private static async Task Run(Options options) + { + // Read the account keys from a file. + var walletData = File.ReadAllText(options.WalletKeysFile); + var account = WalletAccount.FromWalletKeyExportFormat(walletData); + + // Construct the client. + var clientOptions = new ConcordiumClientOptions + { + Timeout = TimeSpan.FromSeconds(10) + }; + using var client = new ConcordiumClient(new Uri(options.Endpoint), clientOptions); + + // Create the update transaction. + var amount = CcdAmount.Zero; + var contract = ContractAddress.From(options.Contract, 0); + + // Fetch the module source from chain, to extract the embedded schema. + var moduleReference = new ModuleReference(_cis2MultiModuleRef); + var moduleSourceResponse = await client.GetModuleSourceAsync(new LastFinal(), moduleReference); + var moduleSchema = moduleSourceResponse.Response.GetModuleSchema()!; + + var receiveName = ReceiveName.Parse(_cis2MultiReceiveMint); + + // Construct the mint parameter, this parameter class is tied to details of the smart + // contract itself. + var parameter = new MintParameter + { + Owner = new JsonAccountAddr + { + Account = new[] { account.AccountAddress.ToString() } + }, + MetadataUrl = new JsonMetadataUrl + { + Url = options.MetadataUrl, + ContentHash = new JsonNoHash + { + None = Array.Empty() + }, + }, + TokenId = options.TokenId + }; + var jsonString = JsonSerializer.Serialize(parameter, new JsonSerializerOptions { WriteIndented = true }); + Console.WriteLine($"Mint using the JSON parameter:\n{jsonString}"); + var jsonParameter = new Utf8Json(JsonSerializer.SerializeToUtf8Bytes(parameter)); + + // Convert the JSON parameter into the byte format expected by the smart contract using the + // information in the smart contract module schema. + var parameterBytes = Parameter.UpdateJson( + moduleSchema, + receiveName.GetContractName(), + receiveName.GetEntrypoint(), + jsonParameter + ); + + + // Prepare the transaction for signing. + var updatePayload = new Concordium.Sdk.Transactions.UpdateContract( + amount, + contract, + receiveName, + parameterBytes + ); + var sender = account.AccountAddress; + var sequenceNumber = client.GetNextAccountSequenceNumber(sender).Item1; + var expiry = Expiry.AtMinutesFromNow(30); + var maxEnergy = new EnergyAmount(options.MaxEnergy); + var preparedPayload = updatePayload.Prepare(sender, sequenceNumber, expiry, maxEnergy); + + // Sign the transaction using the account keys. + var signedTransaction = preparedPayload.Sign(account); + + // Submit the transaction. + var txHash = await client.SendAccountTransactionAsync(signedTransaction); + Console.WriteLine($"Successfully submitted transfer transaction with hash {txHash}"); + + // Watch the status of the transaction, until it becomes finalized in a block. + Console.WriteLine($"Waiting for the transaction to finalize..."); + var finalized = await client.WaitUntilFinalized(txHash); + + Console.WriteLine($"Finalized in block with hash {finalized.BlockHash}"); + + // Check whether the transaction was rejected and if so exit. + if (finalized.Summary.TryGetRejectedAccountTransaction(out var reason)) + { + Console.WriteLine($"Transaction got rejected:\n{reason}"); + return; + } + Console.WriteLine($"Transaction got accepted!"); + + // Exact the logs of updated smart contract instances from the outcome. + if (!finalized.Summary.TryGetContractUpdateLogs(out var updates)) + { + throw new InvalidOperationException( + "Transaction summary failed to parse as a contract update transaction." + ); + } + // Print out the events from each updated contract in the block. + foreach (var update in updates) + { + var updatedContract = update.Item1; + Console.WriteLine($"Contract {updatedContract} logged:"); + // If this is our contract, then we use the module schema to parse the events. + if (updatedContract == contract) + { + foreach (var evt in update.Item2) + { + // Use the smart contract module schema to deserialize the events. + var json = evt.GetDeserializeEvent(moduleSchema, receiveName.GetContractName()); + Console.WriteLine($"- {json}"); + } + } + else + { + // For other contracts we just log the event bytes as a hex encoded string. + foreach (var evt in update.Item2) + { + Console.WriteLine($"- {evt.ToHexString()}"); + } + } + } + } + + /// + /// Class for constructing the JSON representation of the smart contract parameter for 'mint' + /// entrypoint of 'cis2_multi' contract. + /// The exact structure will depend on the details of the smart contract module. + /// + private class MintParameter + { + /// + /// The owner for the newly minted tokens. + /// + /// This property only represents the owner being an account address, but the smart contract + /// also allow for a contract address here, which we choose not include in the type for + /// simplicity. + /// + [JsonPropertyName("owner")] + public JsonAccountAddr Owner { get; set; } + /// + /// The metadata url for a newly minted token type. + /// + [JsonPropertyName("metadata_url")] + public JsonMetadataUrl MetadataUrl { get; set; } + /// + /// The TokenID identifying the token type to mint. + /// + [JsonPropertyName("token_id")] + public string TokenId { get; set; } + } + + /// + /// Indicator class for some address being an account address. + /// Note this only represents a single account address, but still contains an array of strings + /// for the JSON format to match. + /// + private class JsonAccountAddr + { + public string[] Account { get; set; } + } + + /// + /// Class for constructing the JSON representation of the token metadata URL. + /// + private class JsonMetadataUrl + { + /// + /// The URL pointing to the token metadata. + /// + [JsonPropertyName("url")] + public string Url { get; set; } + /// + /// The hash of the token metadata. + /// + /// This property only supports not providing a content hash of the metadata, but the smart + /// contract supports this as well, we choose not include this in the type for simplicity. + /// + [JsonPropertyName("hash")] + public JsonNoHash ContentHash { get; set; } + } + + /// + /// Indicator of no checksum (SHA256 hash) provided for the token metadata. + /// Note this still contains an array of int for the JSON format to match. + /// + private class JsonNoHash + { + public int[] None { get; set; } + } +} diff --git a/examples/UpdateContractMint/Transations.UpdateContractMint.csproj b/examples/UpdateContractMint/Transations.UpdateContractMint.csproj new file mode 100644 index 00000000..74addacb --- /dev/null +++ b/examples/UpdateContractMint/Transations.UpdateContractMint.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + enable + false + + + + + + + + + + diff --git a/rust-bindings/src/lib.rs b/rust-bindings/src/lib.rs index 016c0ace..c7cd1972 100644 --- a/rust-bindings/src/lib.rs +++ b/rust-bindings/src/lib.rs @@ -1,8 +1,8 @@ use anyhow::Result; use concordium_contracts_common::{ + from_bytes, schema::{Type, VersionedModuleSchema, VersionedSchemaError}, - schema_json::ToJsonError, - Cursor, + schema_json, Cursor, }; use serde_json::to_vec; use std::{ffi::CStr, os::raw::c_char}; @@ -149,6 +149,136 @@ pub unsafe extern "C" fn get_event_contract( }) } +/// Construct smart contract receive parameter from a JSON string and a smart +/// contract schema. +/// +/// # Arguments +/// +/// * 'schema_ptr' - Pointer to smart contract module schema. +/// * 'schema_size' - The byte size of the smart contract module schema. +/// * 'schema_version' - Version of the smart contract module schema (Optional). +/// * 'contract_name' - Contract name provided as a null terminated string. +/// * 'function_name' - Receive function name provided as a null terminated +/// string. +/// * '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 into_receive_parameter( + schema_ptr: *const u8, + schema_size: i32, + schema_version: FFIByteOption, + contract_name: *const c_char, + function_name: *const c_char, + json_ptr: *const u8, + json_size: i32, + callback: ResultCallback, +) -> 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)?; + let function_name_str = get_str_from_pointer(function_name)?; + let json_slice = std::slice::from_raw_parts(json_ptr, json_size as usize); + let module_schema = VersionedModuleSchema::new(schema, &schema_version.into_option())?; + let parameter_schema_type = + module_schema.get_receive_param_schema(contract_name_str, function_name_str)?; + let json_value: serde_json::Value = serde_json::from_slice(json_slice)?; + Ok(parameter_schema_type.serial_value(&json_value)?) + }) +} + +/// Construct smart contract init parameter from a JSON string and a smart +/// contract schema. +/// +/// # Arguments +/// +/// * 'schema_ptr' - Pointer to smart contract module schema. +/// * 'schema_size' - The byte size of the smart contract module schema. +/// * 'schema_version' - Version of the smart contract module schema (Optional). +/// * 'contract_name' - Contract name provided as a null terminated string. +/// * '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 into_init_parameter( + schema_ptr: *const u8, + schema_size: i32, + schema_version: FFIByteOption, + contract_name: *const c_char, + json_ptr: *const u8, + json_size: i32, + callback: ResultCallback, +) -> 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)?; + let json_slice = std::slice::from_raw_parts(json_ptr, json_size as usize); + let module_schema = VersionedModuleSchema::new(schema, &schema_version.into_option())?; + let parameter_schema_type = module_schema.get_init_param_schema(contract_name_str)?; + let json_value: serde_json::Value = serde_json::from_slice(json_slice)?; + Ok(parameter_schema_type.serial_value(&json_value)?) + }) +} + +/// 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. /// @@ -169,7 +299,7 @@ fn assign_result Result, FFIError>>(callback: ResultCallb 0 } Err(e) => { - let error = format!("{}", e).into_bytes(); + let error = e.to_string().into_bytes(); let error_length = error.len() as i32; let ptr = error.as_ptr(); callback(ptr, error_length); @@ -178,7 +308,7 @@ fn assign_result Result, FFIError>>(callback: ResultCallb } } -pub fn get_receive_contract_parameter_aux( +fn get_receive_contract_parameter_aux( schema: &[u8], schema_version: Option, contract_name: &str, @@ -197,15 +327,19 @@ fn schema_display_aux(schema: &[u8], schema_version: Option) -> Result u16 { match self { - FFIError::JsonError(_) => 1, - FFIError::SerdeJsonError => 2, - FFIError::Utf8Error => 3, + FFIError::ToJsonError(_) => 1, + FFIError::SerdeJsonError(_) => 2, + FFIError::Utf8Error(_) => 3, FFIError::VersionedSchemaError(schema_error) => match schema_error { VersionedSchemaError::ParseError => 4, VersionedSchemaError::MissingSchemaVersion => 5, @@ -233,22 +367,12 @@ impl FFIError { VersionedSchemaError::NoEventInContract => 17, VersionedSchemaError::EventNotSupported => 18, }, + FFIError::FromJsonError(_) => 19, + FFIError::ParseSchemaType => 20, } } } -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, @@ -266,6 +390,7 @@ fn deserialize_type_value(value: &[u8], value_type: &Type) -> Result, FF 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. diff --git a/src/Client/ConcordiumClient.cs b/src/Client/ConcordiumClient.cs index dfd249f7..f8e5ccc6 100644 --- a/src/Client/ConcordiumClient.cs +++ b/src/Client/ConcordiumClient.cs @@ -205,6 +205,59 @@ public async Task GetBlockItemStatusAsync(TransactionHash tr return TransactionStatusFactory.CreateTransactionStatus(blockItemStatus); } + /// + /// Wait until the transaction is finalized. + /// + /// 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 + /// Hash of the block finalizing the transaction and the block summary of the transaction. + /// RPC error occurred, access for more information. + public async Task<(BlockHash BlockHash, BlockItemSummary Summary)> WaitUntilFinalized( + TransactionHash transactionHash, + CancellationToken token = default + ) + { + static bool isFinalized(ITransactionStatus status, out (BlockHash, BlockItemSummary)? output) + { + switch (status) + { + case TransactionStatusFinalized finalized: + output = finalized.State; + return true; + default: + output = null; + return false; + }; + } + + // Check the transaction status straight away and return if finalized already. + var status = await this.GetBlockItemStatusAsync(transactionHash, token); + if (isFinalized(status, out var finalized)) + { + return finalized!.Value; + } + + // Otherwise listen for new finalized blocks and recheck the status. + var blocks = this.GetFinalizedBlocks(token); + await foreach (var block in blocks) + { + var nowStatus = await this.GetBlockItemStatusAsync(transactionHash, token); + if (isFinalized(nowStatus, out var nowFinalized)) + { + return nowFinalized!.Value; + } + } + // The above loop only exits when the block is finalized. + throw new InvalidOperationException("Unreachable code"); + } + /// /// Get the information for the given account in the given block. /// diff --git a/src/Interop/InteropBinding.cs b/src/Interop/InteropBinding.cs index 54f02d39..9a931c06 100644 --- a/src/Interop/InteropBinding.cs +++ b/src/Interop/InteropBinding.cs @@ -41,6 +41,35 @@ private static extern SchemaJsonResult GetEventContract( int value_size, [MarshalAs(UnmanagedType.FunctionPtr)] SetResultCallback callback); + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "into_receive_parameter")] + private static extern SchemaJsonResult IntoReceiveParameter( + [MarshalAs(UnmanagedType.LPArray)] byte[] schema, + int schema_size, + FfiByteOption schema_version, + [MarshalAs(UnmanagedType.LPUTF8Str)] string contract_name, + [MarshalAs(UnmanagedType.LPUTF8Str)] string function_name, + [MarshalAs(UnmanagedType.LPArray)] byte[] json_ptr, + int json_size, + [MarshalAs(UnmanagedType.FunctionPtr)] SetResultCallback callback); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "into_init_parameter")] + private static extern SchemaJsonResult IntoInitParameter( + [MarshalAs(UnmanagedType.LPArray)] byte[] schema, + int schema_size, + FfiByteOption schema_version, + [MarshalAs(UnmanagedType.LPUTF8Str)] string contract_name, + [MarshalAs(UnmanagedType.LPArray)] byte[] json_ptr, + 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. /// @@ -136,6 +165,118 @@ internal static Utf8Json GetEventContract(VersionedModuleSchema schema, Contract throw interopException; } + /// + /// Contruct a smart contract receive parameter from a JSON string and the module schema. + /// + /// Smart contract module schema + /// Name of the smart contract + /// Entrypoint of the contract to construct the parameter for + /// JSON representation of the smart contract parameter + /// Smart contract parameter + internal static Parameter IntoReceiveParameter( + VersionedModuleSchema schema, + ContractIdentifier contractName, + EntryPoint functionName, + Utf8Json json + ) + { + var ffiOption = FfiByteOption.Create(schema.Version); + var result = Array.Empty(); + + var statusCode = IntoReceiveParameter( + schema.Schema, + schema.Schema.Length, + ffiOption, + contractName.ContractName, + functionName.Name, + json.Bytes, + json.Bytes.Length, + (ptr, size) => + { + result = new byte[size]; + Marshal.Copy(ptr, result, 0, size); + }); + + if (!statusCode.IsError() && result != null) + { + return new Parameter(result); + } + + var interopException = SchemaJsonException.Create(statusCode, result); + throw interopException; + } + + /// + /// Contruct a smart contract init parameter from a JSON string and the module schema. + /// + /// Smart contract module schema + /// Name of the smart contract + /// JSON representation of the smart contract parameter + /// Smart contract parameter + internal static Parameter IntoInitParameter( + VersionedModuleSchema schema, + ContractIdentifier contractName, + Utf8Json json + ) + { + var ffiOption = FfiByteOption.Create(schema.Version); + var result = Array.Empty(); + + var statusCode = IntoInitParameter( + schema.Schema, + schema.Schema.Length, + ffiOption, + contractName.ContractName, + json.Bytes, + json.Bytes.Length, + (ptr, size) => + { + result = new byte[size]; + Marshal.Copy(ptr, result, 0, size); + }); + + if (!statusCode.IsError() && result != null) + { + return new Parameter(result); + } + + var interopException = SchemaJsonException.Create(statusCode, result); + 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 1a399b6d..85961218 100644 --- a/src/Interop/SchemaJsonResult.cs +++ b/src/Interop/SchemaJsonResult.cs @@ -3,14 +3,14 @@ namespace Concordium.Sdk.Interop; /// /// Result type which on errors hold error type information. /// -public enum SchemaJsonResult +public enum SchemaJsonResult : ushort { /// /// No error /// NoError = 0, /// - /// Represents errors occurring while deserializing to the schema JSON format. + /// Represents errors occurring while converting to the schema JSON format. /// JsonError = 1, /// @@ -83,6 +83,14 @@ public enum SchemaJsonResult /// Versioned Schema Error - Events not supported for this module version /// VersionedSchemaErrorEventNotSupported = 18, + /// + /// Represents errors occurring while converting from the schema JSON format. + /// + FromJsonError = 19, + /// + /// Represents errors occurring parsing a smart contract schema type. + /// + ParseSchemaType = 20 } internal static class ErrorExtensions diff --git a/src/Types/ContractEvent.cs b/src/Types/ContractEvent.cs index dfdac385..448330fc 100644 --- a/src/Types/ContractEvent.cs +++ b/src/Types/ContractEvent.cs @@ -14,7 +14,7 @@ public sealed record ContractEvent(byte[] Bytes) public string ToHexString() => Convert.ToHexString(this.Bytes).ToLowerInvariant(); /// - /// Deserialize event from . + /// Deserialize event from . /// /// Module schema in hexadecimal. /// Contract name. diff --git a/src/Types/Parameter.cs b/src/Types/Parameter.cs index 15f193be..66c3fd64 100644 --- a/src/Types/Parameter.cs +++ b/src/Types/Parameter.cs @@ -33,7 +33,7 @@ public sealed record Parameter(byte[] Param) : IEquatable /// /// Copies the parameters to a byte array which has the length preprended. /// - public byte[] ToBytes() + public byte[] ToBytes() { using var memoryStream = new MemoryStream((int)this.SerializedLength()); memoryStream.Write(Serialization.ToBytes((ushort)this.Param.Length)); @@ -41,6 +41,42 @@ public byte[] ToBytes() return memoryStream.ToArray(); } + /// + /// Create a parameter from JSON representation using the smart contract module schema for a smart contract update transaction. + /// + /// The smart contract module schema. + /// The name of the contract. + /// The name of entrypoint of the smart contract. + /// The UTF8 encoding of the JSON representation of the smart contract parameter. + public static Parameter UpdateJson( + VersionedModuleSchema moduleSchema, + ContractIdentifier contractName, + EntryPoint functionName, + Utf8Json jsonParameter + ) => Interop.InteropBinding.IntoReceiveParameter(moduleSchema, contractName, functionName, jsonParameter); + + /// + /// Create a parameter from JSON representation using the smart contract module schema for a smart contract init transaction. + /// + /// The smart contract module schema. + /// The name of the contract. + /// The UTF8 encoding of the JSON representation of the smart contract parameter. + public static Parameter InitJson( + VersionedModuleSchema moduleSchema, + ContractIdentifier contractName, + 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/src/Types/VersionedModuleSchema.cs b/src/Types/VersionedModuleSchema.cs index 767cd8cf..dfe1eafa 100644 --- a/src/Types/VersionedModuleSchema.cs +++ b/src/Types/VersionedModuleSchema.cs @@ -11,7 +11,7 @@ namespace Concordium.Sdk.Types; public sealed record VersionedModuleSchema(byte[] Schema, ModuleSchemaVersion Version) { /// - /// Constructor which converts into hexadecimal string. + /// Constructor which converts into hexadecimal string. /// /// Module schema given as an hexadecimal string. /// Module schema version. diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index da2c29db..e4d980cf 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -90,7 +90,7 @@ private protected abstract (byte[]? Schema, ModuleSchemaVersion SchemaVersion)? ExtractSchemaFromWebAssemblyModule(Module module); /// - /// From custom sections in get entry with name . + /// From custom sections in get entry with name . /// /// Fails if multiple entries exist with the same name. /// diff --git a/tests/UnitTests/Interop/InteropBindingTests.cs b/tests/UnitTests/Interop/InteropBindingTests.cs index 492b719e..ec5ebc6a 100644 --- a/tests/UnitTests/Interop/InteropBindingTests.cs +++ b/tests/UnitTests/Interop/InteropBindingTests.cs @@ -256,6 +256,63 @@ public async Task GivenBadContractEvent_WhenDisplayEvent_ThenThrowException() e.Message.StartsWith("Failed to deserialize AccountAddress due to: Could not parse")); } + [Fact] + public async Task WhenIntoReceiveParamFromJson_ThenReturnParams() + { + // 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, ModuleSchemaVersion.Undefined); + var json = new Utf8Json(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(new + { + to = new + { + Account = new string[] { + "4tUoKeVapaTwwi2yY3Vwe5auM65VL2vk31R3eVhTW94hnB159F" + }, + }, + data = "" + })); + var contractIdentifier = new ContractIdentifier(contractName); + var entryPoint = new EntryPoint(entrypoint); + + // Act + var parameter = InteropBinding.IntoReceiveParameter(versionedModuleSchema, contractIdentifier, entryPoint, json); + + // Assert + await Verifier.Verify(parameter.ToHexString()) + .UseFileName("receive-params-hex-from-json") + .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)] [InlineData(ModuleSchemaVersion.V2, (byte)2)] diff --git a/tests/UnitTests/Interop/__snapshots__/receive-params-hex-from-json.verified.txt b/tests/UnitTests/Interop/__snapshots__/receive-params-hex-from-json.verified.txt new file mode 100644 index 00000000..e6f53346 --- /dev/null +++ b/tests/UnitTests/Interop/__snapshots__/receive-params-hex-from-json.verified.txt @@ -0,0 +1 @@ +00fffa722d840687699743e5f1a1ad86113d0404115661ab09a3611bffc1bdaabe0000 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