From 0f968642d9df67a8c12b839222d60d495136af40 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 19 Dec 2023 14:33:38 +0100 Subject: [PATCH 01/12] Created InitContract payload, needs an example --- examples/InitContract/InitContract.csproj | 18 +++ examples/InitContract/Program.cs | 49 +++++++ src/Transactions/InitContract.cs | 140 +++++++++++++++++++ src/Types/EnergyAmount.cs | 37 +++++ src/Types/Hash.cs | 2 + src/Types/InitName.cs | 128 +++++++++++++++++ src/Types/ModuleReference.cs | 20 +++ src/Types/OnChainData.cs | 2 +- src/Types/Parameter.cs | 63 ++++++++- tests/UnitTests/Transactions/InitContract.cs | 100 +++++++++++++ tests/UnitTests/Types/InitName.cs | 81 +++++++++++ 11 files changed, 638 insertions(+), 2 deletions(-) create mode 100644 examples/InitContract/InitContract.csproj create mode 100644 examples/InitContract/Program.cs create mode 100644 src/Transactions/InitContract.cs create mode 100644 src/Types/InitName.cs create mode 100644 tests/UnitTests/Transactions/InitContract.cs create mode 100644 tests/UnitTests/Types/InitName.cs diff --git a/examples/InitContract/InitContract.csproj b/examples/InitContract/InitContract.csproj new file mode 100644 index 00000000..f9d1b111 --- /dev/null +++ b/examples/InitContract/InitContract.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + diff --git a/examples/InitContract/Program.cs b/examples/InitContract/Program.cs new file mode 100644 index 00000000..5a004085 --- /dev/null +++ b/examples/InitContract/Program.cs @@ -0,0 +1,49 @@ +using CommandLine; +using Concordium.Sdk.Client; +using Concordium.Sdk.Types; + +// 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 + +namespace InitContract; + +internal sealed class InitContractOptions { + [Option(HelpText = "URL representing the endpoint where the gRPC V2 API is served.", + Default = "http://node.testnet.concordium.com:20000/")] + public string Endpoint { get; set; } + [Option( + 'b', + "block-hash", + HelpText = "Block hash of the block. Defaults to LastFinal." + )] + public string BlockHash { get; set; } +} + +public static class Program +{ + /// + /// Example how to use + /// + public static async Task Main(string[] args) => + await Parser.Default + .ParseArguments(args) + .WithParsedAsync(Run); + + private static async Task Run(InitContractOptions o) + { + using var client = new ConcordiumClient(new Uri(o.Endpoint), new ConcordiumClientOptions()); + + IBlockHashInput bi = o.BlockHash != null ? new Given(BlockHash.From(o.BlockHash)) : new LastFinal(); + + var blockItems = await client.GetBlockItems(bi); + + Console.WriteLine($"All block items in block {blockItems.BlockHash}: ["); + await foreach (var item in blockItems.Response) + { + Console.WriteLine($"{item},"); + } + Console.WriteLine("]"); + } +} + diff --git a/src/Transactions/InitContract.cs b/src/Transactions/InitContract.cs new file mode 100644 index 00000000..e574b50e --- /dev/null +++ b/src/Transactions/InitContract.cs @@ -0,0 +1,140 @@ +using Concordium.Sdk.Exceptions; +using Concordium.Sdk.Types; + +namespace Concordium.Sdk.Transactions; + +/// +/// Represents a "transfer" account transaction. +/// +/// Used for transferring CCD from one account to another. +/// +/// Amount to send. +/// The smart contract module reference. +/// The init name of the smart contract. +/// The parameters for the smart contract. +public sealed record InitContract(CcdAmount Amount, ModuleReference ModuleRef, InitName InitName, Parameter Parameter) : AccountTransactionPayload +{ + /// + /// The init contract transaction type to be used in the serialized payload. + /// + private const byte TransactionType = (byte)Types.TransactionType.InitContract; + + /// + /// The minimum serialized length in the serialized payload. + /// + internal const uint MinSerializedLength = + CcdAmount.BytesLength + + Hash.BytesLength + + InitName.MinSerializedLength + + Parameter.MinSerializedLength; + + /// + /// Prepares the account transaction payload for signing. + /// + /// Address of the sender of the transaction. + /// Account sequence number to use for the transaction. + /// Expiration time of the transaction. + /// + /// The amount of energy that can be used for contract execution. + /// The base energy amount for transaction verification will be added to this cost. + /// + public PreparedAccountTransaction Prepare( + AccountAddress sender, + AccountSequenceNumber sequenceNumber, + Expiry expiry, + EnergyAmount energy + ) => new(sender, sequenceNumber, expiry, this._transactionBaseCost + energy, this); + + /// + /// The base transaction specific cost for submitting this type of + /// transaction to the chain. + /// + /// This should reflect the transaction-specific costs defined here: + /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs + /// + private readonly EnergyAmount _transactionBaseCost = new(300); + + /// + /// Gets the size (number of bytes) of the payload. + /// + internal override PayloadSize Size() => new( + sizeof(TransactionType) + + CcdAmount.BytesLength + + Hash.BytesLength + + this.InitName.SerializedLength() + + this.Parameter.SerializedLength()); + + /// + /// Deserialize a "InitContract" payload from a serialized byte array. + /// + /// The serialized InitContract payload. + /// Where to write the result of the operation. + public static bool TryDeserial(ReadOnlySpan bytes, out (InitContract? InitContract, string? Error) output) + { + if (bytes.Length < MinSerializedLength) + { + var msg = $"Invalid length in `InitContract.TryDeserial`. Expected at least {MinSerializedLength}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + if (bytes[0] != TransactionType) + { + var msg = $"Invalid transaction type in `InitContract.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + output = (null, msg); + return false; + }; + + var amountBytes = bytes[sizeof(TransactionType)..]; + if (!CcdAmount.TryDeserial(amountBytes, out var amount)) + { + output = (null, amount.Error); + return false; + }; + + var refBytes = bytes[(int)(CcdAmount.BytesLength + sizeof(TransactionType))..]; + if (!ModuleReference.TryDeserial(refBytes, out var moduleRef)) + { + output = (null, moduleRef.Error); + return false; + }; + + var nameBytes = bytes[(int)(Hash.BytesLength + CcdAmount.BytesLength + sizeof(TransactionType))..]; + if (!InitName.TryDeserial(nameBytes, out var name)) + { + output = (null, name.Error); + return false; + }; + if (name.Name == null) + { + throw new DeserialInvalidResultException(); + } + + var paramBytes = bytes[(int)(name.Name.SerializedLength() + Hash.BytesLength + CcdAmount.BytesLength + sizeof(TransactionType))..]; + if (!Parameter.TryDeserial(paramBytes, out var param)) + { + output = (null, param.Error); + return false; + }; + + if (amount.Amount == null || moduleRef.Ref == null || param.Parameter == null) + { + throw new DeserialInvalidResultException(); + } + + output = (new InitContract(amount.Amount.Value, moduleRef.Ref, name.Name, param.Parameter), null); + return true; + } + + /// + /// Copies the "transfer" account transaction in the binary format expected by the node to a byte array. + /// + public override byte[] ToBytes() { + using var memoryStream = new MemoryStream((int)this.Size().Size); + memoryStream.WriteByte(TransactionType); + memoryStream.Write(Amount.ToBytes()); + memoryStream.Write(ModuleRef.ToBytes()); + memoryStream.Write(InitName.ToBytes()); + memoryStream.Write(Parameter.ToBytes()); + return memoryStream.ToArray(); + } +} diff --git a/src/Types/EnergyAmount.cs b/src/Types/EnergyAmount.cs index 9def3443..2a3ef139 100644 --- a/src/Types/EnergyAmount.cs +++ b/src/Types/EnergyAmount.cs @@ -16,4 +16,41 @@ public readonly record struct EnergyAmount(ulong Value) public byte[] ToBytes() => Serialization.ToBytes(this.Value); internal static EnergyAmount From(Grpc.V2.Energy energy) => new(energy.Value); + + /// + /// Add Energy amounts. + /// + /// The result odoes not fit in + public static EnergyAmount operator +(EnergyAmount a, EnergyAmount b) + { + try + { + return new EnergyAmount(checked(a.Value + b.Value)); + } + catch (OverflowException e) + { + throw new ArgumentException( + $"The result of {a.Value} + {b.Value} does not fit in UInt64.", e + ); + } + } + + /// + /// Subtract CCD amounts. + /// + /// The result does not fit in + public static EnergyAmount operator -(EnergyAmount a, EnergyAmount b) + { + try + { + return new EnergyAmount(checked(a.Value - b.Value)); + } + catch (OverflowException e) + { + throw new ArgumentException( + $"The result of {a.Value} - {b.Value} does not fit in UInt64.", e + ); + } + } + } diff --git a/src/Types/Hash.cs b/src/Types/Hash.cs index 09f1acfb..6993ca16 100644 --- a/src/Types/Hash.cs +++ b/src/Types/Hash.cs @@ -1,3 +1,5 @@ +using System.Buffers.Binary; + namespace Concordium.Sdk.Types; /// diff --git a/src/Types/InitName.cs b/src/Types/InitName.cs new file mode 100644 index 00000000..b4c177aa --- /dev/null +++ b/src/Types/InitName.cs @@ -0,0 +1,128 @@ +using System.Buffers.Binary; +using System.Text; +using System.Text.RegularExpressions; +using Concordium.Sdk.Helpers; + +namespace Concordium.Sdk.Types; +/// +/// A contract init name. +/// +public sealed record InitName : IEquatable +{ + /// A contract init name. + public readonly string Name; + private readonly byte[] _bytes; + + /// + /// Gets the serialized length (number of bytes) of the init name. + /// + internal uint SerializedLength() => sizeof(ushort) + (uint)this._bytes.Length; + + /// + /// Gets the minimum serialized length (number of bytes) of the init name. + /// + internal const uint MinSerializedLength = sizeof(ushort); + + /// + /// Copies the init name to a byte array which has the length preprended. + /// + public byte[] ToBytes() + { + using var memoryStream = new MemoryStream((int)this.SerializedLength()); + memoryStream.Write(Serialization.ToBytes((ushort)this._bytes.Length)); + memoryStream.Write(this._bytes); + return memoryStream.ToArray(); + } + + /// + /// Deserialize an init name from a serialized byte array. + /// + /// The serialized init name. + /// Where to write the result of the operation. + public static bool TryDeserial(ReadOnlySpan bytes, out (InitName? Name, string? Error) output) + { + if (bytes.Length < MinSerializedLength) + { + var msg = $"Invalid length of input in `InitName.TryDeserial`. Expected at least {MinSerializedLength}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + + var sizeRead = BinaryPrimitives.ReadUInt16BigEndian(bytes); + var size = sizeRead + MinSerializedLength; + if (size > bytes.Length) + { + var msg = $"Invalid length of input in `InitName.TryDeserial`. Expected array of size at least {size}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + + try + { + var initNameBytes = bytes.Slice(sizeof(ushort), (int)sizeRead).ToArray(); + var ascii = Encoding.ASCII.GetString(initNameBytes); + var initName = new InitName(ascii); + output = (initName, null); + return true; + } + catch (ArgumentException e) + { + var msg = $"Invalid InitName in `InitName.TryDeserial`: {e.Message}"; + output = (null, msg); + return false; + }; + } + + /// + /// Creates an instance from a string. + /// + /// + /// The init name of a smart contract function. Expected format: + /// `init_<contract_name>`. It must only consist of atmost 100 ASCII alphanumeric + /// or punctuation characters, must not contain a '.' and must start with `init_`. + /// + public InitName(string Name) + { + var containsDot = Name.Contains('.'); + var longerThan100 = Name.Length > 100; + var startsWithInit = new Regex(@"^init_").IsMatch(Name); + var alphanumericOrPunctuation = Name.All(c => char.IsLetterOrDigit(c) || char.IsPunctuation(c)); + var isAscii = Name.All(char.IsAscii); + + if (containsDot) + { + throw new ArgumentException($"InitName must not contain a '.'"); + } + else if (longerThan100) + { + throw new ArgumentException($"InitName must not exceed 100 characters."); + } + else if (!startsWithInit) + { + throw new ArgumentException($"InitName must start with \"init_\""); + } + else if (!alphanumericOrPunctuation) + { + throw new ArgumentException($"InitName must only contain alphanumeric characters or punctuation characters"); + } + else if (!isAscii) + { + throw new ArgumentException($"InitName must only contain alphanumeric characters or punctuation characters"); + } + else + { + this._bytes = Encoding.ASCII.GetBytes(Name); + this.Name = Name; + } + } + + /// Check for equality. + public bool Equals(InitName? other) => other != null && this._bytes.SequenceEqual(other._bytes); + + /// Gets hash code. + public override int GetHashCode() + { + var paramHash = Helpers.HashCode.GetHashCodeByteArray(this._bytes); + return paramHash; + } +} diff --git a/src/Types/ModuleReference.cs b/src/Types/ModuleReference.cs index e91d55e6..2c6e6049 100644 --- a/src/Types/ModuleReference.cs +++ b/src/Types/ModuleReference.cs @@ -14,10 +14,30 @@ internal ModuleReference(ByteString byteString) : base(byteString.ToArray()) internal ModuleRef Into() => new() { Value = ByteString.CopyFrom(this.AsSpan()) }; + /// + /// Create a parameter from a byte array. + /// + /// The serialized parameters. + /// Where to write the result of the operation. + public static bool TryDeserial(ReadOnlySpan bytes, out (ModuleReference? Ref, string? Error) output) + { + if (bytes.Length < BytesLength) + { + var msg = $"Invalid length of input in `Hash.TryDeserial`. Expected at least {BytesLength}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + + output = (new ModuleReference(bytes.Slice(0, (int)BytesLength).ToArray()), null); + return true; + } + /// /// Initializes a new instance. /// /// A hash represented as a length-64 hex encoded string. /// The supplied string is not a 64-character hex encoded string. public ModuleReference(string hexString) : base(hexString) { } + + private ModuleReference(byte[] bytes) : base(bytes) { } } diff --git a/src/Types/OnChainData.cs b/src/Types/OnChainData.cs index cbf2bfa0..d8e85d60 100644 --- a/src/Types/OnChainData.cs +++ b/src/Types/OnChainData.cs @@ -142,7 +142,7 @@ public byte[] ToBytes() public override string ToString() => Convert.ToHexString(this._value).ToLowerInvariant(); /// - /// Create an "OnChainData" from a byte array. + /// Deserialize an "OnChainData" from a byte array. /// /// The serialized "OnChainData". /// Where to write the result of the operation. diff --git a/src/Types/Parameter.cs b/src/Types/Parameter.cs index 14f4e177..73ea69da 100644 --- a/src/Types/Parameter.cs +++ b/src/Types/Parameter.cs @@ -1,10 +1,71 @@ +using System.Buffers.Binary; +using Concordium.Sdk.Helpers; + namespace Concordium.Sdk.Types; /// /// Parameter to the init function or entrypoint. /// -public sealed record Parameter(byte[] Param) +public sealed record Parameter(byte[] Param) : IEquatable { + /// + /// Gets the serialized length (number of bytes) of the parameter. + /// + internal uint SerializedLength() => sizeof(ushort) + (uint)this.Param.Length; + + /// + /// Gets the minimum serialized length (number of bytes) of the parameter. + /// + internal const uint MinSerializedLength = sizeof(ushort); + + /// + /// Copies the parameters to a byte array which has the length preprended. + /// + public byte[] ToBytes() + { + using var memoryStream = new MemoryStream((int)this.SerializedLength()); + memoryStream.Write(Serialization.ToBytes((ushort)this.Param.Length)); + memoryStream.Write(this.Param); + return memoryStream.ToArray(); + } + + /// + /// Create a parameter from a byte array. + /// + /// The serialized parameters. + /// Where to write the result of the operation. + public static bool TryDeserial(ReadOnlySpan bytes, out (Parameter? Parameter, string? Error) output) + { + if (bytes.Length < MinSerializedLength) + { + var msg = $"Invalid length of input in `Parameter.TryDeserial`. Expected at least {MinSerializedLength}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + + var sizeRead = BinaryPrimitives.ReadUInt16BigEndian(bytes); + var size = sizeRead + MinSerializedLength; + if (size > bytes.Length) + { + var msg = $"Invalid length of input in `Parameter.TryDeserial`. Expected array of size at least {size}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + + output = (new Parameter(bytes.Slice(sizeof(ushort), (int)sizeRead).ToArray()), null); + return true; + } + + /// Check for equality. + public bool Equals(Parameter? other) => other != null && this.Param.SequenceEqual(other.Param); + + /// Gets hash code. + public override int GetHashCode() + { + var paramHash = Helpers.HashCode.GetHashCodeByteArray(this.Param); + return paramHash; + } + /// /// Convert parameters to hex string. /// diff --git a/tests/UnitTests/Transactions/InitContract.cs b/tests/UnitTests/Transactions/InitContract.cs new file mode 100644 index 00000000..50651779 --- /dev/null +++ b/tests/UnitTests/Transactions/InitContract.cs @@ -0,0 +1,100 @@ +using Concordium.Sdk.Transactions; +using Concordium.Sdk.Types; +using FluentAssertions; +using Xunit; + +namespace Concordium.Sdk.Tests.UnitTests.Transactions; + +public sealed class InitContractTests +{ + /// + /// Creates a new instance of the + /// transaction. + /// + public static InitContract newInitContract() + { + var amount = CcdAmount.FromCcd(100); + var moduleRef = new ModuleReference("0000000000000000000000000000000000000000000000000000000000000000"); + var initName = new InitName("init_name"); + var parameter = new Parameter(new byte[0]); + + return new InitContract(amount, moduleRef, initName, parameter); + } + + [Fact] + public void ToBytes_ReturnsCorrectValue() + { + // The expected payload was generated using the Concordium Rust SDK. + var expectedBytes = new byte[] + { + 1, + 0, + 0, + 0, + 0, + 5, + 245, + 225, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 105, + 110, + 105, + 116, + 95, + 110, + 97, + 109, + 101, + 0, + 0 + }; + newInitContract().ToBytes().Should().BeEquivalentTo(expectedBytes); + } + + [Fact] + public void ToBytes_InverseOfFromBytes() + { + if (InitContract.TryDeserial(newInitContract().ToBytes(), out var deserial)) + { + newInitContract().Should().Be(deserial.InitContract); + } + else + { + Assert.Fail(deserial.Error); + } + } +} diff --git a/tests/UnitTests/Types/InitName.cs b/tests/UnitTests/Types/InitName.cs new file mode 100644 index 00000000..8bc8ae69 --- /dev/null +++ b/tests/UnitTests/Types/InitName.cs @@ -0,0 +1,81 @@ +using System; +using System.Buffers.Binary; +using System.Linq; +using System.Text; +using Concordium.Sdk.Types; +using FluentAssertions; +using Xunit; + +namespace Concordium.Sdk.Tests.UnitTests.Types; + +public sealed class InitNameTests +{ + [Fact] + public void IsAlphaNumeric() => new InitName("init_some_ascii_here"); + + [Fact] + public void IsAlphaNumeric_Negative() => Assert.Throws(() => new InitName("init_åå")); + + [Fact] + public void Max100Length() + { + var name = "init______1_________2_________3_________4_________5_________6_________7_________8_________9_________"; + new InitName(name); + Assert.Equal(100, name.Length); + Assert.Throws(() => new InitName("init_åå")); + } + + [Fact] + public void Max100Length_Negative() + { + var name = "init______1_________2_________3_________4_________5_________6_________7_________8_________9_________0"; + Assert.Equal(101, name.Length); + Assert.Throws(() => new InitName(name)); + } + + [Fact] + public void StartsWithInit_Negative() + { + Assert.Throws(() => new InitName("ini_contract")); + Assert.Throws(() => new InitName("nit_contract")); + Assert.Throws(() => new InitName("initcontract")); + } + + [Fact] + public void CanContainPunctuation() => new InitName("init_,;:'\"(){}[]?!"); + + [Fact] + public void DeserializesCorrectly() + { + var name = new InitName("init_name"); + var bytes = new byte[] { + 0, + 9, + 105, + 110, + 105, + 116, + 95, + 110, + 97, + 109, + 101 + }; + Assert.Equal(name.ToBytes(), bytes); + } + + [Fact] + public void SerializeDeserialize() + { + var name = new InitName("init_name"); + var x = InitName.TryDeserial(name.ToBytes(), out var deserial); + if (x) + { + name.Should().Be(deserial.Name); + } + else + { + Assert.Fail(deserial.Error); + } + } +} From ff1f45d41e2e57797bddcf2e5e0d4cd2c3a5227d Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 19 Dec 2023 16:13:43 +0100 Subject: [PATCH 02/12] Formatted --- src/Transactions/InitContract.cs | 11 +- src/Types/Hash.cs | 2 - src/Types/InitName.cs | 23 ++-- src/Types/ModuleReference.cs | 2 +- src/Types/Parameter.cs | 2 +- tests/UnitTests/Transactions/InitContract.cs | 116 +++++++++---------- tests/UnitTests/Types/InitName.cs | 15 ++- 7 files changed, 87 insertions(+), 84 deletions(-) diff --git a/src/Transactions/InitContract.cs b/src/Transactions/InitContract.cs index e574b50e..b98f25c8 100644 --- a/src/Transactions/InitContract.cs +++ b/src/Transactions/InitContract.cs @@ -128,13 +128,14 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitContract? Init /// /// Copies the "transfer" account transaction in the binary format expected by the node to a byte array. /// - public override byte[] ToBytes() { + public override byte[] ToBytes() + { using var memoryStream = new MemoryStream((int)this.Size().Size); memoryStream.WriteByte(TransactionType); - memoryStream.Write(Amount.ToBytes()); - memoryStream.Write(ModuleRef.ToBytes()); - memoryStream.Write(InitName.ToBytes()); - memoryStream.Write(Parameter.ToBytes()); + memoryStream.Write(this.Amount.ToBytes()); + memoryStream.Write(this.ModuleRef.ToBytes()); + memoryStream.Write(this.InitName.ToBytes()); + memoryStream.Write(this.Parameter.ToBytes()); return memoryStream.ToArray(); } } diff --git a/src/Types/Hash.cs b/src/Types/Hash.cs index 6993ca16..09f1acfb 100644 --- a/src/Types/Hash.cs +++ b/src/Types/Hash.cs @@ -1,5 +1,3 @@ -using System.Buffers.Binary; - namespace Concordium.Sdk.Types; /// diff --git a/src/Types/InitName.cs b/src/Types/InitName.cs index b4c177aa..900b4e38 100644 --- a/src/Types/InitName.cs +++ b/src/Types/InitName.cs @@ -10,7 +10,8 @@ namespace Concordium.Sdk.Types; public sealed record InitName : IEquatable { /// A contract init name. - public readonly string Name; + public string Name { get; } + private readonly byte[] _bytes; /// @@ -59,7 +60,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitName? Name, st try { - var initNameBytes = bytes.Slice(sizeof(ushort), (int)sizeRead).ToArray(); + var initNameBytes = bytes.Slice(sizeof(ushort), sizeRead).ToArray(); var ascii = Encoding.ASCII.GetString(initNameBytes); var initName = new InitName(ascii); output = (initName, null); @@ -76,18 +77,18 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitName? Name, st /// /// Creates an instance from a string. /// - /// + /// /// The init name of a smart contract function. Expected format: /// `init_<contract_name>`. It must only consist of atmost 100 ASCII alphanumeric /// or punctuation characters, must not contain a '.' and must start with `init_`. /// - public InitName(string Name) + public InitName(string name) { - var containsDot = Name.Contains('.'); - var longerThan100 = Name.Length > 100; - var startsWithInit = new Regex(@"^init_").IsMatch(Name); - var alphanumericOrPunctuation = Name.All(c => char.IsLetterOrDigit(c) || char.IsPunctuation(c)); - var isAscii = Name.All(char.IsAscii); + var containsDot = name.Contains('.'); + var longerThan100 = name.Length > 100; + var startsWithInit = new Regex(@"^init_").IsMatch(name); + var alphanumericOrPunctuation = name.All(c => char.IsLetterOrDigit(c) || char.IsPunctuation(c)); + var isAscii = name.All(char.IsAscii); if (containsDot) { @@ -111,8 +112,8 @@ public InitName(string Name) } else { - this._bytes = Encoding.ASCII.GetBytes(Name); - this.Name = Name; + this._bytes = Encoding.ASCII.GetBytes(name); + this.Name = name; } } diff --git a/src/Types/ModuleReference.cs b/src/Types/ModuleReference.cs index 2c6e6049..338b26d3 100644 --- a/src/Types/ModuleReference.cs +++ b/src/Types/ModuleReference.cs @@ -28,7 +28,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (ModuleReference? R return false; }; - output = (new ModuleReference(bytes.Slice(0, (int)BytesLength).ToArray()), null); + output = (new ModuleReference(bytes[..BytesLength].ToArray()), null); return true; } diff --git a/src/Types/Parameter.cs b/src/Types/Parameter.cs index 73ea69da..a2312eaf 100644 --- a/src/Types/Parameter.cs +++ b/src/Types/Parameter.cs @@ -52,7 +52,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (Parameter? Paramet return false; }; - output = (new Parameter(bytes.Slice(sizeof(ushort), (int)sizeRead).ToArray()), null); + output = (new Parameter(bytes.Slice(sizeof(ushort), sizeRead).ToArray()), null); return true; } diff --git a/tests/UnitTests/Transactions/InitContract.cs b/tests/UnitTests/Transactions/InitContract.cs index 50651779..00535178 100644 --- a/tests/UnitTests/Transactions/InitContract.cs +++ b/tests/UnitTests/Transactions/InitContract.cs @@ -11,12 +11,12 @@ public sealed class InitContractTests /// Creates a new instance of the /// transaction. /// - public static InitContract newInitContract() + public static InitContract NewInitContract() { var amount = CcdAmount.FromCcd(100); - var moduleRef = new ModuleReference("0000000000000000000000000000000000000000000000000000000000000000"); - var initName = new InitName("init_name"); - var parameter = new Parameter(new byte[0]); + var moduleRef = new ModuleReference("0000000000000000000000000000000000000000000000000000000000000000"); + var initName = new InitName("init_name"); + var parameter = new Parameter(System.Array.Empty()); return new InitContract(amount, moduleRef, initName, parameter); } @@ -27,60 +27,60 @@ public void ToBytes_ReturnsCorrectValue() // The expected payload was generated using the Concordium Rust SDK. var expectedBytes = new byte[] { - 1, - 0, - 0, - 0, - 0, - 5, - 245, - 225, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 105, - 110, - 105, - 116, - 95, - 110, - 97, - 109, - 101, - 0, - 0 + 1, + 0, + 0, + 0, + 0, + 5, + 245, + 225, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 105, + 110, + 105, + 116, + 95, + 110, + 97, + 109, + 101, + 0, + 0 }; newInitContract().ToBytes().Should().BeEquivalentTo(expectedBytes); } diff --git a/tests/UnitTests/Types/InitName.cs b/tests/UnitTests/Types/InitName.cs index 8bc8ae69..e8b8eee9 100644 --- a/tests/UnitTests/Types/InitName.cs +++ b/tests/UnitTests/Types/InitName.cs @@ -1,7 +1,4 @@ using System; -using System.Buffers.Binary; -using System.Linq; -using System.Text; using Concordium.Sdk.Types; using FluentAssertions; using Xunit; @@ -11,7 +8,10 @@ namespace Concordium.Sdk.Tests.UnitTests.Types; public sealed class InitNameTests { [Fact] - public void IsAlphaNumeric() => new InitName("init_some_ascii_here"); + public void IsAlphaNumeric() + { + var init = new InitName("init_some_ascii_here"); + } [Fact] public void IsAlphaNumeric_Negative() => Assert.Throws(() => new InitName("init_åå")); @@ -20,7 +20,7 @@ public sealed class InitNameTests public void Max100Length() { var name = "init______1_________2_________3_________4_________5_________6_________7_________8_________9_________"; - new InitName(name); + var init = new InitName(name); Assert.Equal(100, name.Length); Assert.Throws(() => new InitName("init_åå")); } @@ -42,7 +42,10 @@ public void StartsWithInit_Negative() } [Fact] - public void CanContainPunctuation() => new InitName("init_,;:'\"(){}[]?!"); + public void CanContainPunctuation() + { + var init = new InitName("init_,;:'\"(){}[]?!"); + } [Fact] public void DeserializesCorrectly() From 6702de74ec868f4819dcf6133038d28527607c41 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 19 Dec 2023 17:11:50 +0100 Subject: [PATCH 03/12] Updated example. Should work, need to test. --- examples/InitContract/InitContract.csproj | 4 ++ examples/InitContract/Program.cs | 67 +++++++++++++++----- tests/UnitTests/Transactions/InitContract.cs | 6 +- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/examples/InitContract/InitContract.csproj b/examples/InitContract/InitContract.csproj index f9d1b111..5e61ee63 100644 --- a/examples/InitContract/InitContract.csproj +++ b/examples/InitContract/InitContract.csproj @@ -11,6 +11,10 @@ + + + + diff --git a/examples/InitContract/Program.cs b/examples/InitContract/Program.cs index 5a004085..874ec78c 100644 --- a/examples/InitContract/Program.cs +++ b/examples/InitContract/Program.cs @@ -1,6 +1,8 @@ +using System; using CommandLine; using Concordium.Sdk.Client; using Concordium.Sdk.Types; +using Concordium.Sdk.Wallets; // We disable these warnings since CommandLine needs to set properties in options // but we don't want to give default values. @@ -9,15 +11,27 @@ namespace InitContract; internal sealed class InitContractOptions { + [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 = "http://node.testnet.concordium.com:20000/")] public string Endpoint { get; set; } - [Option( - 'b', - "block-hash", - HelpText = "Block hash of the block. Defaults to LastFinal." - )] - public string BlockHash { get; set; } + [Option('a', "amount", HelpText = "Amount of CCD to transfer.", Required = true)] + public ulong Amount { get; set; } = 0; + + [Option('m', "module-ref", HelpText = "The module reference of the smart contract.", Required = true)] + public string ModuleRef { get; set; } + + [Option('i', "init-name", HelpText = "The init_name of the module.", Required = true)] + public string InitName { get; set; } + + [Option('e', "max-energy", HelpText = "The maximum energy to spend on the module.", Required = true)] + public string MaxEnergy { get; set; } } public static class Program @@ -32,18 +46,39 @@ await Parser.Default private static async Task Run(InitContractOptions o) { - using var client = new ConcordiumClient(new Uri(o.Endpoint), new ConcordiumClientOptions()); + // Read the account keys from a file. + var walletData = File.ReadAllText(o.WalletKeysFile); + var account = WalletAccount.FromWalletKeyExportFormat(walletData); - IBlockHashInput bi = o.BlockHash != null ? new Given(BlockHash.From(o.BlockHash)) : new LastFinal(); - - var blockItems = await client.GetBlockItems(bi); - - Console.WriteLine($"All block items in block {blockItems.BlockHash}: ["); - await foreach (var item in blockItems.Response) + // Construct the client. + var clientOptions = new ConcordiumClientOptions { - Console.WriteLine($"{item},"); - } - Console.WriteLine("]"); + Timeout = TimeSpan.FromSeconds(10) + }; + using var client = new ConcordiumClient(new Uri(o.Endpoint), clientOptions); + + // Create the transfer transaction. + var amount = CcdAmount.FromCcd(o.Amount); + var moduleRef = new ModuleReference(o.ModuleRef); + var initName = new InitName(o.InitName); + var param = new Parameter(new byte[0]); + var maxEnergy = new EnergyAmount(UInt32.Parse(o.MaxEnergy)); + var transferPayload = new Concordium.Sdk.Transactions.InitContract(amount, moduleRef, initName, param); + + // Prepare the transaction for signing. + var sender = account.AccountAddress; + var sequenceNumber = client.GetNextAccountSequenceNumber(sender).Item1; + var expiry = Expiry.AtMinutesFromNow(30); + var preparedTransfer = transferPayload.Prepare(sender, sequenceNumber, expiry, maxEnergy); + + // Sign the transaction using the account keys. + var signedTransfer = preparedTransfer.Sign(account); + + // Submit the transaction. + var txHash = client.SendAccountTransaction(signedTransfer); + + // Print the transaction hash. + Console.WriteLine($"Successfully submitted transfer transaction with hash {txHash}"); } } diff --git a/tests/UnitTests/Transactions/InitContract.cs b/tests/UnitTests/Transactions/InitContract.cs index 00535178..0ad302ba 100644 --- a/tests/UnitTests/Transactions/InitContract.cs +++ b/tests/UnitTests/Transactions/InitContract.cs @@ -82,15 +82,15 @@ public void ToBytes_ReturnsCorrectValue() 0, 0 }; - newInitContract().ToBytes().Should().BeEquivalentTo(expectedBytes); + NewInitContract().ToBytes().Should().BeEquivalentTo(expectedBytes); } [Fact] public void ToBytes_InverseOfFromBytes() { - if (InitContract.TryDeserial(newInitContract().ToBytes(), out var deserial)) + if (InitContract.TryDeserial(NewInitContract().ToBytes(), out var deserial)) { - newInitContract().Should().Be(deserial.InitContract); + NewInitContract().Should().Be(deserial.InitContract); } else { From c38c92925bdc10bf0595b766c02b3095523b99e9 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 23 Jan 2024 16:04:32 +0100 Subject: [PATCH 04/12] Format --- ConcordiumNetSdk.sln | 7 +++++++ examples/InitContract/Program.cs | 13 +++++++------ src/Transactions/InitContract.cs | 16 +++++++++------- src/Types/EnergyAmount.cs | 3 ++- src/Types/ModuleReference.cs | 4 ++-- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/ConcordiumNetSdk.sln b/ConcordiumNetSdk.sln index b291f93c..ebd78cef 100644 --- a/ConcordiumNetSdk.sln +++ b/ConcordiumNetSdk.sln @@ -77,6 +77,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetBranches", "examples\Get EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetFinalizedBlocks", "examples\GetFinalizedBlocks\GetFinalizedBlocks.csproj", "{E2CC6AD7-98CE-41F5-8C66-AE8781F29C77}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InitContract", "examples\InitContract\InitContract.csproj", "{732169A9-2B64-4D38-9157-FF7954EEF730}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -227,6 +229,10 @@ Global {E2CC6AD7-98CE-41F5-8C66-AE8781F29C77}.Debug|Any CPU.Build.0 = Debug|Any CPU {E2CC6AD7-98CE-41F5-8C66-AE8781F29C77}.Release|Any CPU.ActiveCfg = Release|Any CPU {E2CC6AD7-98CE-41F5-8C66-AE8781F29C77}.Release|Any CPU.Build.0 = Release|Any CPU + {732169A9-2B64-4D38-9157-FF7954EEF730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {732169A9-2B64-4D38-9157-FF7954EEF730}.Debug|Any CPU.Build.0 = Debug|Any CPU + {732169A9-2B64-4D38-9157-FF7954EEF730}.Release|Any CPU.ActiveCfg = Release|Any CPU + {732169A9-2B64-4D38-9157-FF7954EEF730}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -269,5 +275,6 @@ Global {79E97788-D084-487E-8F34-0BA1911C452A} = {FD2CDD9F-4650-4705-9CA2-98CC81F8891D} {26417CD7-2897-47BA-BA9B-C4475187331A} = {FD2CDD9F-4650-4705-9CA2-98CC81F8891D} {E2CC6AD7-98CE-41F5-8C66-AE8781F29C77} = {FD2CDD9F-4650-4705-9CA2-98CC81F8891D} + {732169A9-2B64-4D38-9157-FF7954EEF730} = {FD2CDD9F-4650-4705-9CA2-98CC81F8891D} EndGlobalSection EndGlobal diff --git a/examples/InitContract/Program.cs b/examples/InitContract/Program.cs index 874ec78c..b8cd3ebb 100644 --- a/examples/InitContract/Program.cs +++ b/examples/InitContract/Program.cs @@ -1,4 +1,4 @@ -using System; +using System.Globalization; using CommandLine; using Concordium.Sdk.Client; using Concordium.Sdk.Types; @@ -10,7 +10,8 @@ namespace InitContract; -internal sealed class InitContractOptions { +internal sealed class InitContractOptions +{ [Option( 'k', "keys", @@ -21,8 +22,8 @@ internal sealed class InitContractOptions { [Option(HelpText = "URL representing the endpoint where the gRPC V2 API is served.", Default = "http://node.testnet.concordium.com:20000/")] public string Endpoint { get; set; } - [Option('a', "amount", HelpText = "Amount of CCD to transfer.", Required = true)] - public ulong Amount { get; set; } = 0; + [Option('a', "amount", HelpText = "Amount of CCD to deposit.", Default = 0)] + public ulong Amount { get; set; } [Option('m', "module-ref", HelpText = "The module reference of the smart contract.", Required = true)] public string ModuleRef { get; set; } @@ -61,8 +62,8 @@ private static async Task Run(InitContractOptions o) var amount = CcdAmount.FromCcd(o.Amount); var moduleRef = new ModuleReference(o.ModuleRef); var initName = new InitName(o.InitName); - var param = new Parameter(new byte[0]); - var maxEnergy = new EnergyAmount(UInt32.Parse(o.MaxEnergy)); + var param = new Parameter(Array.Empty()); + var maxEnergy = new EnergyAmount(uint.Parse(o.MaxEnergy, CultureInfo.InvariantCulture)); var transferPayload = new Concordium.Sdk.Transactions.InitContract(amount, moduleRef, initName, param); // Prepare the transaction for signing. diff --git a/src/Transactions/InitContract.cs b/src/Transactions/InitContract.cs index b98f25c8..3ab5c9e6 100644 --- a/src/Transactions/InitContract.cs +++ b/src/Transactions/InitContract.cs @@ -4,11 +4,11 @@ namespace Concordium.Sdk.Transactions; /// -/// Represents a "transfer" account transaction. +/// Represents an "init_contract" transaction. /// -/// Used for transferring CCD from one account to another. +/// Used for initializing deployed smart contracts. /// -/// Amount to send. +/// Deposit this amount of CCD. /// The smart contract module reference. /// The init name of the smart contract. /// The parameters for the smart contract. @@ -43,7 +43,7 @@ public PreparedAccountTransaction Prepare( AccountSequenceNumber sequenceNumber, Expiry expiry, EnergyAmount energy - ) => new(sender, sequenceNumber, expiry, this._transactionBaseCost + energy, this); + ) => new(sender, sequenceNumber, expiry, new EnergyAmount(TrxBaseCost) + energy, this); /// /// The base transaction specific cost for submitting this type of @@ -52,7 +52,7 @@ EnergyAmount energy /// This should reflect the transaction-specific costs defined here: /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - private readonly EnergyAmount _transactionBaseCost = new(300); + private const ushort TrxBaseCost = 300; /// /// Gets the size (number of bytes) of the payload. @@ -118,7 +118,9 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitContract? Init if (amount.Amount == null || moduleRef.Ref == null || param.Parameter == null) { - throw new DeserialInvalidResultException(); + var msg = $"Amount, ModuleRef or Parameter were null, but did not produce an error"; + output = (null, msg); + return false; } output = (new InitContract(amount.Amount.Value, moduleRef.Ref, name.Name, param.Parameter), null); @@ -126,7 +128,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitContract? Init } /// - /// Copies the "transfer" account transaction in the binary format expected by the node to a byte array. + /// Copies the "init_contract" transaction in the binary format expected by the node to a byte array. /// public override byte[] ToBytes() { diff --git a/src/Types/EnergyAmount.cs b/src/Types/EnergyAmount.cs index 2a3ef139..7b0d1a12 100644 --- a/src/Types/EnergyAmount.cs +++ b/src/Types/EnergyAmount.cs @@ -8,6 +8,7 @@ namespace Concordium.Sdk.Types; /// Value of the energy amount. public readonly record struct EnergyAmount(ulong Value) { + ///Byte length of Energy. Used for serialization. public const uint BytesLength = sizeof(ulong); /// @@ -36,7 +37,7 @@ public readonly record struct EnergyAmount(ulong Value) } /// - /// Subtract CCD amounts. + /// Subtract Energy amounts. /// /// The result does not fit in public static EnergyAmount operator -(EnergyAmount a, EnergyAmount b) diff --git a/src/Types/ModuleReference.cs b/src/Types/ModuleReference.cs index 338b26d3..111b87c1 100644 --- a/src/Types/ModuleReference.cs +++ b/src/Types/ModuleReference.cs @@ -15,9 +15,9 @@ internal ModuleReference(ByteString byteString) : base(byteString.ToArray()) internal ModuleRef Into() => new() { Value = ByteString.CopyFrom(this.AsSpan()) }; /// - /// Create a parameter from a byte array. + /// Create a module reference from a byte array. /// - /// The serialized parameters. + /// The serialized module reference. /// Where to write the result of the operation. public static bool TryDeserial(ReadOnlySpan bytes, out (ModuleReference? Ref, string? Error) output) { From 100fc295f19dd453b35ae98e6cd9916f0a56edee Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 23 Jan 2024 16:22:16 +0100 Subject: [PATCH 05/12] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75223e7b..c6f42b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Added - New GRPC-endpoint: `GetBlockItems` - New transaction `DeployModule` + - New transaction `InitContract` - The function `Prepare` has been removed from the `AccountTransactionPayload` class, but is implemented for all subclasses except `RawPayload`. - Added serialization and deserialization for all instances of `AccountTransactionPayload` From 1d6fa81a1f69ef50b9e11a56749304def2f3d24e Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 23 Jan 2024 16:31:02 +0100 Subject: [PATCH 06/12] Builds now --- src/Transactions/InitContract.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Transactions/InitContract.cs b/src/Transactions/InitContract.cs index 3ab5c9e6..d8200c67 100644 --- a/src/Transactions/InitContract.cs +++ b/src/Transactions/InitContract.cs @@ -1,4 +1,3 @@ -using Concordium.Sdk.Exceptions; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -106,7 +105,9 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitContract? Init }; if (name.Name == null) { - throw new DeserialInvalidResultException(); + var msg = $"Name was null, but did not produce an error"; + output = (null, msg); + return false; } var paramBytes = bytes[(int)(name.Name.SerializedLength() + Hash.BytesLength + CcdAmount.BytesLength + sizeof(TransactionType))..]; From eb69144adc583fdf297e02e1e263f1067fda580f Mon Sep 17 00:00:00 2001 From: Rasmus Kirk Date: Thu, 1 Feb 2024 08:15:04 +0000 Subject: [PATCH 07/12] Update src/Types/ModuleReference.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Emil Holm Gjørup --- src/Types/ModuleReference.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Types/ModuleReference.cs b/src/Types/ModuleReference.cs index 111b87c1..19065a7b 100644 --- a/src/Types/ModuleReference.cs +++ b/src/Types/ModuleReference.cs @@ -23,7 +23,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (ModuleReference? R { if (bytes.Length < BytesLength) { - var msg = $"Invalid length of input in `Hash.TryDeserial`. Expected at least {BytesLength}, found {bytes.Length}"; + var msg = $"Invalid length of input in `ModuleReference.TryDeserial`. Expected at least {BytesLength}, found {bytes.Length}"; output = (null, msg); return false; }; From 2263b1b428af369b22d8ea7d72f7131668c1ff53 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Thu, 1 Feb 2024 09:45:51 +0100 Subject: [PATCH 08/12] Addressed some comments --- examples/InitContract/Program.cs | 7 ++++++- src/Transactions/AccountTransactionPayload.cs | 5 +++++ src/Types/Parameter.cs | 12 ++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/examples/InitContract/Program.cs b/examples/InitContract/Program.cs index b8cd3ebb..2478dd42 100644 --- a/examples/InitContract/Program.cs +++ b/examples/InitContract/Program.cs @@ -38,7 +38,12 @@ internal sealed class InitContractOptions public static class Program { /// - /// Example how to use + /// Example demonstrating how to submit a smart contract initialization + /// transaction. + /// + /// 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. /// public static async Task Main(string[] args) => await Parser.Default diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index 67617473..4b444cd6 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -84,6 +84,11 @@ private static AccountTransactionPayload ParseRawPayload(Google.Protobuf.ByteStr break; } case TransactionType.InitContract: + { + InitContract.TryDeserial(payload.ToArray(), out var output); + parsedPayload = output; + break; + } case TransactionType.Update: case TransactionType.AddBaker: case TransactionType.RemoveBaker: diff --git a/src/Types/Parameter.cs b/src/Types/Parameter.cs index a2312eaf..de1b7ad6 100644 --- a/src/Types/Parameter.cs +++ b/src/Types/Parameter.cs @@ -18,6 +18,11 @@ public sealed record Parameter(byte[] Param) : IEquatable /// internal const uint MinSerializedLength = sizeof(ushort); + /// + /// Gets the maximum serialized length (number of bytes) of the parameter. + /// + internal const uint MaxSerializedLength = 65535; + /// /// Copies the parameters to a byte array which has the length preprended. /// @@ -42,7 +47,14 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (Parameter? Paramet output = (null, msg); return false; }; + if (bytes.Length > MaxSerializedLength) + { + var msg = $"Invalid length of input in `Parameter.TryDeserial`. Expected at most {MaxSerializedLength}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + // The below call can throw, but never will due to check above. var sizeRead = BinaryPrimitives.ReadUInt16BigEndian(bytes); var size = sizeRead + MinSerializedLength; if (size > bytes.Length) From 912d39d98d137a78e3125fcd6ed80c3e27d02258 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 6 Feb 2024 10:55:24 +0100 Subject: [PATCH 09/12] Removed initname --- examples/InitContract/Program.cs | 8 +- src/Transactions/InitContract.cs | 18 +-- src/Types/ContractName.cs | 71 ++++++++++ src/Types/InitName.cs | 129 ------------------- tests/UnitTests/Transactions/InitContract.cs | 4 +- tests/UnitTests/Types/ContractNameTests.cs | 40 ++++++ tests/UnitTests/Types/InitName.cs | 84 ------------ 7 files changed, 128 insertions(+), 226 deletions(-) delete mode 100644 src/Types/InitName.cs delete mode 100644 tests/UnitTests/Types/InitName.cs diff --git a/examples/InitContract/Program.cs b/examples/InitContract/Program.cs index 2478dd42..1b6a3d96 100644 --- a/examples/InitContract/Program.cs +++ b/examples/InitContract/Program.cs @@ -64,12 +64,16 @@ private static async Task Run(InitContractOptions o) using var client = new ConcordiumClient(new Uri(o.Endpoint), clientOptions); // Create the transfer transaction. + var successfulParse = ContractName.TryParse(o.InitName, out var parsed); + if (!successfulParse) { + throw new ArgumentException("Error parsing (" + o.InitName + "): " + parsed.Error.ToString()); + }; + var amount = CcdAmount.FromCcd(o.Amount); var moduleRef = new ModuleReference(o.ModuleRef); - var initName = new InitName(o.InitName); var param = new Parameter(Array.Empty()); var maxEnergy = new EnergyAmount(uint.Parse(o.MaxEnergy, CultureInfo.InvariantCulture)); - var transferPayload = new Concordium.Sdk.Transactions.InitContract(amount, moduleRef, initName, param); + var transferPayload = new Concordium.Sdk.Transactions.InitContract(amount, moduleRef, parsed.ContractName!, param); // Prepare the transaction for signing. var sender = account.AccountAddress; diff --git a/src/Transactions/InitContract.cs b/src/Transactions/InitContract.cs index d8200c67..a540e69c 100644 --- a/src/Transactions/InitContract.cs +++ b/src/Transactions/InitContract.cs @@ -9,9 +9,9 @@ namespace Concordium.Sdk.Transactions; /// /// Deposit this amount of CCD. /// The smart contract module reference. -/// The init name of the smart contract. +/// The init name of the smart contract. /// The parameters for the smart contract. -public sealed record InitContract(CcdAmount Amount, ModuleReference ModuleRef, InitName InitName, Parameter Parameter) : AccountTransactionPayload +public sealed record InitContract(CcdAmount Amount, ModuleReference ModuleRef, ContractName ContractName, Parameter Parameter) : AccountTransactionPayload { /// /// The init contract transaction type to be used in the serialized payload. @@ -24,7 +24,7 @@ public sealed record InitContract(CcdAmount Amount, ModuleReference ModuleRef, I internal const uint MinSerializedLength = CcdAmount.BytesLength + Hash.BytesLength + - InitName.MinSerializedLength + + ContractName.MinSerializedLength + Parameter.MinSerializedLength; /// @@ -60,7 +60,7 @@ EnergyAmount energy sizeof(TransactionType) + CcdAmount.BytesLength + Hash.BytesLength + - this.InitName.SerializedLength() + + this.ContractName.SerializedLength() + this.Parameter.SerializedLength()); /// @@ -98,19 +98,19 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitContract? Init }; var nameBytes = bytes[(int)(Hash.BytesLength + CcdAmount.BytesLength + sizeof(TransactionType))..]; - if (!InitName.TryDeserial(nameBytes, out var name)) + if (!ContractName.TryDeserial(nameBytes, out var name)) { output = (null, name.Error); return false; }; - if (name.Name == null) + if (name.ContractName == null) { var msg = $"Name was null, but did not produce an error"; output = (null, msg); return false; } - var paramBytes = bytes[(int)(name.Name.SerializedLength() + Hash.BytesLength + CcdAmount.BytesLength + sizeof(TransactionType))..]; + var paramBytes = bytes[(int)(name.ContractName.SerializedLength() + Hash.BytesLength + CcdAmount.BytesLength + sizeof(TransactionType))..]; if (!Parameter.TryDeserial(paramBytes, out var param)) { output = (null, param.Error); @@ -124,7 +124,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitContract? Init return false; } - output = (new InitContract(amount.Amount.Value, moduleRef.Ref, name.Name, param.Parameter), null); + output = (new InitContract(amount.Amount.Value, moduleRef.Ref, name.ContractName, param.Parameter), null); return true; } @@ -137,7 +137,7 @@ public override byte[] ToBytes() memoryStream.WriteByte(TransactionType); memoryStream.Write(this.Amount.ToBytes()); memoryStream.Write(this.ModuleRef.ToBytes()); - memoryStream.Write(this.InitName.ToBytes()); + memoryStream.Write(this.ContractName.ToBytes()); memoryStream.Write(this.Parameter.ToBytes()); return memoryStream.ToArray(); } diff --git a/src/Types/ContractName.cs b/src/Types/ContractName.cs index f72e148b..8a170db4 100644 --- a/src/Types/ContractName.cs +++ b/src/Types/ContractName.cs @@ -1,3 +1,5 @@ +using System.Buffers.Binary; +using System.Text; using Concordium.Sdk.Helpers; namespace Concordium.Sdk.Types; @@ -17,6 +19,16 @@ public sealed record ContractName /// public string Name { get; init; } + /// + /// Gets the minimum serialized length (number of bytes) of the init name. + /// + internal const uint MinSerializedLength = sizeof(ushort); + + /// + /// Gets the serialized length (number of bytes) of the init name. + /// + internal uint SerializedLength() => sizeof(ushort) + (uint)this.Name.Length; + private ContractName(string name) => this.Name = name; internal static ContractName From(Grpc.V2.InitName initName) => new(initName.Value); @@ -36,6 +48,59 @@ public static bool TryParse(string name, out (ContractName? ContractName, Valida return validate; } + /// + /// Copies the init name to a byte array which has the length preprended. + /// + public byte[] ToBytes() + { + var bytes = Encoding.ASCII.GetBytes(this.Name); + + using var memoryStream = new MemoryStream((int)this.SerializedLength()); + memoryStream.Write(Serialization.ToBytes((ushort)bytes.Length)); + memoryStream.Write(bytes); + return memoryStream.ToArray(); + } + + /// + /// Deserialize an init name from a serialized byte array. + /// + /// The serialized init name. + /// Where to write the result of the operation. + public static bool TryDeserial(ReadOnlySpan bytes, out (ContractName? ContractName, string? Error) output) + { + if (bytes.Length < MinSerializedLength) + { + var msg = $"Invalid length of input in `InitName.TryDeserial`. Expected at least {MinSerializedLength}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + + var sizeRead = BinaryPrimitives.ReadUInt16BigEndian(bytes); + var size = sizeRead + MinSerializedLength; + if (size > bytes.Length) + { + var msg = $"Invalid length of input in `InitName.TryDeserial`. Expected array of size at least {size}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + + try + { + var initNameBytes = bytes.Slice(sizeof(ushort), sizeRead).ToArray(); + var ascii = Encoding.ASCII.GetString(initNameBytes); + + var correctlyParsed = TryParse(ascii, out var parseOut); + output = correctlyParsed ? (parseOut.ContractName, null) : (null, "Error parsing contract name (" + ascii + "): " + parseOut.Error.ToString()); + return correctlyParsed; + } + catch (ArgumentException e) + { + var msg = $"Invalid InitName in `InitName.TryDeserial`: {e.Message}"; + output = (null, msg); + return false; + }; + } + /// /// Get the contract name part of . /// @@ -79,4 +144,10 @@ private static bool IsValid(string name, out ValidationError? error) error = null; return true; } + + /// Check for equality. + public bool Equals(ContractName? other) => other != null && this.Name == other.Name; + + /// Gets hash code. + public override int GetHashCode() => this.Name.GetHashCode(); } diff --git a/src/Types/InitName.cs b/src/Types/InitName.cs deleted file mode 100644 index 900b4e38..00000000 --- a/src/Types/InitName.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System.Buffers.Binary; -using System.Text; -using System.Text.RegularExpressions; -using Concordium.Sdk.Helpers; - -namespace Concordium.Sdk.Types; -/// -/// A contract init name. -/// -public sealed record InitName : IEquatable -{ - /// A contract init name. - public string Name { get; } - - private readonly byte[] _bytes; - - /// - /// Gets the serialized length (number of bytes) of the init name. - /// - internal uint SerializedLength() => sizeof(ushort) + (uint)this._bytes.Length; - - /// - /// Gets the minimum serialized length (number of bytes) of the init name. - /// - internal const uint MinSerializedLength = sizeof(ushort); - - /// - /// Copies the init name to a byte array which has the length preprended. - /// - public byte[] ToBytes() - { - using var memoryStream = new MemoryStream((int)this.SerializedLength()); - memoryStream.Write(Serialization.ToBytes((ushort)this._bytes.Length)); - memoryStream.Write(this._bytes); - return memoryStream.ToArray(); - } - - /// - /// Deserialize an init name from a serialized byte array. - /// - /// The serialized init name. - /// Where to write the result of the operation. - public static bool TryDeserial(ReadOnlySpan bytes, out (InitName? Name, string? Error) output) - { - if (bytes.Length < MinSerializedLength) - { - var msg = $"Invalid length of input in `InitName.TryDeserial`. Expected at least {MinSerializedLength}, found {bytes.Length}"; - output = (null, msg); - return false; - }; - - var sizeRead = BinaryPrimitives.ReadUInt16BigEndian(bytes); - var size = sizeRead + MinSerializedLength; - if (size > bytes.Length) - { - var msg = $"Invalid length of input in `InitName.TryDeserial`. Expected array of size at least {size}, found {bytes.Length}"; - output = (null, msg); - return false; - }; - - try - { - var initNameBytes = bytes.Slice(sizeof(ushort), sizeRead).ToArray(); - var ascii = Encoding.ASCII.GetString(initNameBytes); - var initName = new InitName(ascii); - output = (initName, null); - return true; - } - catch (ArgumentException e) - { - var msg = $"Invalid InitName in `InitName.TryDeserial`: {e.Message}"; - output = (null, msg); - return false; - }; - } - - /// - /// Creates an instance from a string. - /// - /// - /// The init name of a smart contract function. Expected format: - /// `init_<contract_name>`. It must only consist of atmost 100 ASCII alphanumeric - /// or punctuation characters, must not contain a '.' and must start with `init_`. - /// - public InitName(string name) - { - var containsDot = name.Contains('.'); - var longerThan100 = name.Length > 100; - var startsWithInit = new Regex(@"^init_").IsMatch(name); - var alphanumericOrPunctuation = name.All(c => char.IsLetterOrDigit(c) || char.IsPunctuation(c)); - var isAscii = name.All(char.IsAscii); - - if (containsDot) - { - throw new ArgumentException($"InitName must not contain a '.'"); - } - else if (longerThan100) - { - throw new ArgumentException($"InitName must not exceed 100 characters."); - } - else if (!startsWithInit) - { - throw new ArgumentException($"InitName must start with \"init_\""); - } - else if (!alphanumericOrPunctuation) - { - throw new ArgumentException($"InitName must only contain alphanumeric characters or punctuation characters"); - } - else if (!isAscii) - { - throw new ArgumentException($"InitName must only contain alphanumeric characters or punctuation characters"); - } - else - { - this._bytes = Encoding.ASCII.GetBytes(name); - this.Name = name; - } - } - - /// Check for equality. - public bool Equals(InitName? other) => other != null && this._bytes.SequenceEqual(other._bytes); - - /// Gets hash code. - public override int GetHashCode() - { - var paramHash = Helpers.HashCode.GetHashCodeByteArray(this._bytes); - return paramHash; - } -} diff --git a/tests/UnitTests/Transactions/InitContract.cs b/tests/UnitTests/Transactions/InitContract.cs index 0ad302ba..b6f6aa1d 100644 --- a/tests/UnitTests/Transactions/InitContract.cs +++ b/tests/UnitTests/Transactions/InitContract.cs @@ -15,10 +15,10 @@ public static InitContract NewInitContract() { var amount = CcdAmount.FromCcd(100); var moduleRef = new ModuleReference("0000000000000000000000000000000000000000000000000000000000000000"); - var initName = new InitName("init_name"); + var contractName = ContractName.TryParse("init_name", out var parsed); var parameter = new Parameter(System.Array.Empty()); - return new InitContract(amount, moduleRef, initName, parameter); + return new InitContract(amount, moduleRef, parsed.ContractName!, parameter); } [Fact] diff --git a/tests/UnitTests/Types/ContractNameTests.cs b/tests/UnitTests/Types/ContractNameTests.cs index 1794d90c..4919bb5d 100644 --- a/tests/UnitTests/Types/ContractNameTests.cs +++ b/tests/UnitTests/Types/ContractNameTests.cs @@ -54,4 +54,44 @@ public void WhenGetContractNamePart_ThenReturnContractName() // Assert actual.ContractName.Should().Be(expected); } + + [Fact] + public void DeserializesCorrectly() + { + var success = ContractName.TryParse("init_name", out var parsed); + if (!success) { + Assert.Fail(parsed.Error.ToString()); + } + + var bytes = new byte[] { + 0, + 9, + 105, + 110, + 105, + 116, + 95, + 110, + 97, + 109, + 101 + }; + Assert.Equal(parsed.ContractName!.ToBytes(), bytes); + Assert.Equal(parsed.ContractName!.SerializedLength(), (uint)bytes.Length); + } + + [Fact] + public void SerializeDeserialize() + { + var parseSuccess = ContractName.TryParse("init_some_name", out var parsed); + if (!parseSuccess) { + Assert.Fail(parsed.Error.ToString()); + } + var deserialSuccess = ContractName.TryDeserial(parsed.ContractName!.ToBytes(), out var deserial); + if (!deserialSuccess) { + Assert.Fail(deserial.Error); + } + deserial.ContractName.Should().Be(parsed.ContractName); + Assert.Equal(parsed.ContractName!.SerializedLength(), (uint)parsed.ContractName!.ToBytes().Length); + } } diff --git a/tests/UnitTests/Types/InitName.cs b/tests/UnitTests/Types/InitName.cs deleted file mode 100644 index e8b8eee9..00000000 --- a/tests/UnitTests/Types/InitName.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using Concordium.Sdk.Types; -using FluentAssertions; -using Xunit; - -namespace Concordium.Sdk.Tests.UnitTests.Types; - -public sealed class InitNameTests -{ - [Fact] - public void IsAlphaNumeric() - { - var init = new InitName("init_some_ascii_here"); - } - - [Fact] - public void IsAlphaNumeric_Negative() => Assert.Throws(() => new InitName("init_åå")); - - [Fact] - public void Max100Length() - { - var name = "init______1_________2_________3_________4_________5_________6_________7_________8_________9_________"; - var init = new InitName(name); - Assert.Equal(100, name.Length); - Assert.Throws(() => new InitName("init_åå")); - } - - [Fact] - public void Max100Length_Negative() - { - var name = "init______1_________2_________3_________4_________5_________6_________7_________8_________9_________0"; - Assert.Equal(101, name.Length); - Assert.Throws(() => new InitName(name)); - } - - [Fact] - public void StartsWithInit_Negative() - { - Assert.Throws(() => new InitName("ini_contract")); - Assert.Throws(() => new InitName("nit_contract")); - Assert.Throws(() => new InitName("initcontract")); - } - - [Fact] - public void CanContainPunctuation() - { - var init = new InitName("init_,;:'\"(){}[]?!"); - } - - [Fact] - public void DeserializesCorrectly() - { - var name = new InitName("init_name"); - var bytes = new byte[] { - 0, - 9, - 105, - 110, - 105, - 116, - 95, - 110, - 97, - 109, - 101 - }; - Assert.Equal(name.ToBytes(), bytes); - } - - [Fact] - public void SerializeDeserialize() - { - var name = new InitName("init_name"); - var x = InitName.TryDeserial(name.ToBytes(), out var deserial); - if (x) - { - name.Should().Be(deserial.Name); - } - else - { - Assert.Fail(deserial.Error); - } - } -} From 4ff97f3b5ac1d65517090fd2aa550cbe5327b218 Mon Sep 17 00:00:00 2001 From: Rasmus Kirk Date: Tue, 6 Feb 2024 11:52:36 +0000 Subject: [PATCH 10/12] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Emil Holm Gjørup --- examples/InitContract/Program.cs | 8 ++++---- src/Transactions/InitContract.cs | 2 +- src/Types/ContractName.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/InitContract/Program.cs b/examples/InitContract/Program.cs index 4bf39612..f8779379 100644 --- a/examples/InitContract/Program.cs +++ b/examples/InitContract/Program.cs @@ -10,7 +10,7 @@ namespace InitContract; -internal sealed class InitContractOptions +internal sealed class InitContractOptions { [Option( 'k', @@ -20,7 +20,7 @@ internal sealed class InitContractOptions )] public string WalletKeysFile { get; set; } [Option(HelpText = "URL representing the endpoint where the gRPC V2 API is served.", - Default = "http://node.testnet.concordium.com:20000/")] + Default = "http://grpc.testnet.concordium.com:20000/")] public string Endpoint { get; set; } [Option('a', "amount", HelpText = "Amount of CCD to deposit.", Default = 0)] public ulong Amount { get; set; } @@ -38,7 +38,7 @@ internal sealed class InitContractOptions public static class Program { /// - /// Example demonstrating how to submit a smart contract initialization + /// Example demonstrating how to submit a smart contract initialization /// transaction. /// /// The example assumes you have your account key information stored @@ -65,7 +65,7 @@ private static async Task Run(InitContractOptions o) // Create the transfer transaction. var successfulParse = ContractName.TryParse(o.InitName, out var parsed); - if (!successfulParse) + if (!successfulParse) { throw new ArgumentException("Error parsing (" + o.InitName + "): " + parsed.Error.ToString()); }; diff --git a/src/Transactions/InitContract.cs b/src/Transactions/InitContract.cs index a540e69c..0111f737 100644 --- a/src/Transactions/InitContract.cs +++ b/src/Transactions/InitContract.cs @@ -42,7 +42,7 @@ public PreparedAccountTransaction Prepare( AccountSequenceNumber sequenceNumber, Expiry expiry, EnergyAmount energy - ) => new(sender, sequenceNumber, expiry, new EnergyAmount(TrxBaseCost) + energy, this); + ) => new(sender, sequenceNumber, expiry, energy, this); /// /// The base transaction specific cost for submitting this type of diff --git a/src/Types/ContractName.cs b/src/Types/ContractName.cs index 8a170db4..585c89a6 100644 --- a/src/Types/ContractName.cs +++ b/src/Types/ContractName.cs @@ -76,7 +76,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (ContractName? Cont }; var sizeRead = BinaryPrimitives.ReadUInt16BigEndian(bytes); - var size = sizeRead + MinSerializedLength; + var size = sizeRead + sizeof(ushort); if (size > bytes.Length) { var msg = $"Invalid length of input in `InitName.TryDeserial`. Expected array of size at least {size}, found {bytes.Length}"; From 4c96c50b2e1b71b3ffa960d69bf33c60b0c0caa6 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 6 Feb 2024 13:11:24 +0100 Subject: [PATCH 11/12] Addressed more comments --- examples/InitContract/Program.cs | 12 ++++++------ src/Transactions/InitContract.cs | 25 ++++++++----------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/examples/InitContract/Program.cs b/examples/InitContract/Program.cs index f8779379..1a30f8aa 100644 --- a/examples/InitContract/Program.cs +++ b/examples/InitContract/Program.cs @@ -63,7 +63,7 @@ private static async Task Run(InitContractOptions o) }; using var client = new ConcordiumClient(new Uri(o.Endpoint), clientOptions); - // Create the transfer transaction. + // Create the init transaction. var successfulParse = ContractName.TryParse(o.InitName, out var parsed); if (!successfulParse) { @@ -74,22 +74,22 @@ private static async Task Run(InitContractOptions o) var moduleRef = new ModuleReference(o.ModuleRef); var param = new Parameter(Array.Empty()); var maxEnergy = new EnergyAmount(uint.Parse(o.MaxEnergy, CultureInfo.InvariantCulture)); - var transferPayload = new Concordium.Sdk.Transactions.InitContract(amount, moduleRef, parsed.ContractName!, param); + var payload = new Concordium.Sdk.Transactions.InitContract(amount, moduleRef, parsed.ContractName!, param); // Prepare the transaction for signing. var sender = account.AccountAddress; var sequenceNumber = client.GetNextAccountSequenceNumber(sender).Item1; var expiry = Expiry.AtMinutesFromNow(30); - var preparedTransfer = transferPayload.Prepare(sender, sequenceNumber, expiry, maxEnergy); + var preparedPayload = payload.Prepare(sender, sequenceNumber, expiry, maxEnergy); // Sign the transaction using the account keys. - var signedTransfer = preparedTransfer.Sign(account); + var signedTrx = preparedPayload.Sign(account); // Submit the transaction. - var txHash = client.SendAccountTransaction(signedTransfer); + var txHash = client.SendAccountTransaction(signedTrx); // Print the transaction hash. - Console.WriteLine($"Successfully submitted transfer transaction with hash {txHash}"); + Console.WriteLine($"Successfully submitted init-contract transaction with hash {txHash}"); } } diff --git a/src/Transactions/InitContract.cs b/src/Transactions/InitContract.cs index 0111f737..e6ef5b20 100644 --- a/src/Transactions/InitContract.cs +++ b/src/Transactions/InitContract.cs @@ -44,15 +44,6 @@ public PreparedAccountTransaction Prepare( EnergyAmount energy ) => new(sender, sequenceNumber, expiry, energy, this); - /// - /// The base transaction specific cost for submitting this type of - /// transaction to the chain. - /// - /// This should reflect the transaction-specific costs defined here: - /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs - /// - private const ushort TrxBaseCost = 300; - /// /// Gets the size (number of bytes) of the payload. /// @@ -83,22 +74,22 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitContract? Init return false; }; - var amountBytes = bytes[sizeof(TransactionType)..]; - if (!CcdAmount.TryDeserial(amountBytes, out var amount)) + var remainingBytes = bytes[sizeof(TransactionType)..]; + if (!CcdAmount.TryDeserial(remainingBytes, out var amount)) { output = (null, amount.Error); return false; }; - var refBytes = bytes[(int)(CcdAmount.BytesLength + sizeof(TransactionType))..]; - if (!ModuleReference.TryDeserial(refBytes, out var moduleRef)) + remainingBytes = bytes[(int)CcdAmount.BytesLength..]; + if (!ModuleReference.TryDeserial(remainingBytes, out var moduleRef)) { output = (null, moduleRef.Error); return false; }; - var nameBytes = bytes[(int)(Hash.BytesLength + CcdAmount.BytesLength + sizeof(TransactionType))..]; - if (!ContractName.TryDeserial(nameBytes, out var name)) + remainingBytes = bytes[Hash.BytesLength..]; + if (!ContractName.TryDeserial(remainingBytes, out var name)) { output = (null, name.Error); return false; @@ -110,8 +101,8 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitContract? Init return false; } - var paramBytes = bytes[(int)(name.ContractName.SerializedLength() + Hash.BytesLength + CcdAmount.BytesLength + sizeof(TransactionType))..]; - if (!Parameter.TryDeserial(paramBytes, out var param)) + remainingBytes = bytes[(int)name.ContractName.SerializedLength()..]; + if (!Parameter.TryDeserial(remainingBytes, out var param)) { output = (null, param.Error); return false; From c0f0d2c00e2e819f7f3e588766fef175522e0c96 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 6 Feb 2024 13:31:32 +0100 Subject: [PATCH 12/12] Fixed blunder --- src/Transactions/InitContract.cs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/Transactions/InitContract.cs b/src/Transactions/InitContract.cs index e6ef5b20..af9fcadf 100644 --- a/src/Transactions/InitContract.cs +++ b/src/Transactions/InitContract.cs @@ -21,9 +21,9 @@ public sealed record InitContract(CcdAmount Amount, ModuleReference ModuleRef, C /// /// The minimum serialized length in the serialized payload. /// - internal const uint MinSerializedLength = + internal const uint MinSerializedLength = CcdAmount.BytesLength + - Hash.BytesLength + + Hash.BytesLength + // ModuleRef ContractName.MinSerializedLength + Parameter.MinSerializedLength; @@ -50,7 +50,7 @@ EnergyAmount energy internal override PayloadSize Size() => new( sizeof(TransactionType) + CcdAmount.BytesLength + - Hash.BytesLength + + Hash.BytesLength + // ModuleRef this.ContractName.SerializedLength() + this.Parameter.SerializedLength()); @@ -75,42 +75,37 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (InitContract? Init }; var remainingBytes = bytes[sizeof(TransactionType)..]; + if (!CcdAmount.TryDeserial(remainingBytes, out var amount)) { output = (null, amount.Error); return false; }; + remainingBytes = remainingBytes[(int)CcdAmount.BytesLength..]; - remainingBytes = bytes[(int)CcdAmount.BytesLength..]; if (!ModuleReference.TryDeserial(remainingBytes, out var moduleRef)) { output = (null, moduleRef.Error); return false; }; + remainingBytes = remainingBytes[Hash.BytesLength..]; // ModuleRef - remainingBytes = bytes[Hash.BytesLength..]; if (!ContractName.TryDeserial(remainingBytes, out var name)) { output = (null, name.Error); return false; }; - if (name.ContractName == null) - { - var msg = $"Name was null, but did not produce an error"; - output = (null, msg); - return false; - } + remainingBytes = remainingBytes[(int)name.ContractName!.SerializedLength()..]; - remainingBytes = bytes[(int)name.ContractName.SerializedLength()..]; if (!Parameter.TryDeserial(remainingBytes, out var param)) { output = (null, param.Error); return false; }; - if (amount.Amount == null || moduleRef.Ref == null || param.Parameter == null) + if (amount.Amount == null || moduleRef.Ref == null || name.ContractName == null || param.Parameter == null) { - var msg = $"Amount, ModuleRef or Parameter were null, but did not produce an error"; + var msg = $"Amount, ModuleRef, ContractName or Parameter were null, but did not produce an error"; output = (null, msg); return false; }