Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Init Contract transaction #89

Merged
merged 15 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## Unreleased changes
- Added
- New transaction `InitContract`

## 4.3.1
- Added
Expand Down
7 changes: 7 additions & 0 deletions ConcordiumNetSdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Transations.UpdateContract"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployModule", "examples\DeployModule\DeployModule.csproj", "{D35681A3-04AE-41BA-86F3-3CF5369D6D97}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InitContract", "examples\InitContract\InitContract.csproj", "{E68CBBAC-7BC9-46D3-AA04-2B7A62BD6921}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -239,6 +241,10 @@ Global
{D35681A3-04AE-41BA-86F3-3CF5369D6D97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D35681A3-04AE-41BA-86F3-3CF5369D6D97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D35681A3-04AE-41BA-86F3-3CF5369D6D97}.Release|Any CPU.Build.0 = Release|Any CPU
{E68CBBAC-7BC9-46D3-AA04-2B7A62BD6921}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -283,5 +289,6 @@ Global
{E2CC6AD7-98CE-41F5-8C66-AE8781F29C77} = {FD2CDD9F-4650-4705-9CA2-98CC81F8891D}
{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}
EndGlobalSection
EndGlobal
22 changes: 22 additions & 0 deletions examples/InitContract/InitContract.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Concordium.Sdk.csproj" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
</ItemGroup>

</Project>
95 changes: 95 additions & 0 deletions examples/InitContract/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Globalization;
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.
#pragma warning disable CS8618

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://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; }

[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
{
/// <summary>
/// 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.
/// </summary>
public static async Task Main(string[] args) =>
await Parser.Default
.ParseArguments<InitContractOptions>(args)
.WithParsedAsync(Run);

private static async Task Run(InitContractOptions o)
{
// Read the account keys from a file.
var walletData = File.ReadAllText(o.WalletKeysFile);
var account = WalletAccount.FromWalletKeyExportFormat(walletData);

// Construct the client.
var clientOptions = new ConcordiumClientOptions
{
Timeout = TimeSpan.FromSeconds(10)
};
using var client = new ConcordiumClient(new Uri(o.Endpoint), clientOptions);

// Create the init 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 param = new Parameter(Array.Empty<byte>());
var maxEnergy = new EnergyAmount(uint.Parse(o.MaxEnergy, CultureInfo.InvariantCulture));
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 preparedPayload = payload.Prepare(sender, sequenceNumber, expiry, maxEnergy);

// Sign the transaction using the account keys.
var signedTrx = preparedPayload.Sign(account);

// Submit the transaction.
var txHash = client.SendAccountTransaction(signedTrx);

// Print the transaction hash.
Console.WriteLine($"Successfully submitted init-contract transaction with hash {txHash}");
}
}

7 changes: 6 additions & 1 deletion src/Transactions/AccountTransactionPayload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,18 @@ private static AccountTransactionPayload ParseRawPayload(Google.Protobuf.ByteStr
parsedPayload = output;
break;
}
case TransactionType.InitContract:
{
InitContract.TryDeserial(payload.ToArray(), out var output);
parsedPayload = output;
break;
}
case TransactionType.Update:
{
UpdateContract.TryDeserial(payload.ToArray(), out var output);
parsedPayload = output;
break;
}
case TransactionType.InitContract:
case TransactionType.AddBaker:
case TransactionType.RemoveBaker:
case TransactionType.UpdateBakerStake:
Expand Down
130 changes: 130 additions & 0 deletions src/Transactions/InitContract.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using Concordium.Sdk.Types;

namespace Concordium.Sdk.Transactions;

/// <summary>
/// Represents an "init_contract" transaction.
///
/// Used for initializing deployed smart contracts.
/// </summary>
/// <param name="Amount">Deposit this amount of CCD.</param>
/// <param name="ModuleRef">The smart contract module reference.</param>
/// <param name="ContractName">The init name of the smart contract.</param>
/// <param name="Parameter">The parameters for the smart contract.</param>
public sealed record InitContract(CcdAmount Amount, ModuleReference ModuleRef, ContractName ContractName, Parameter Parameter) : AccountTransactionPayload
{
/// <summary>
/// The init contract transaction type to be used in the serialized payload.
/// </summary>
private const byte TransactionType = (byte)Types.TransactionType.InitContract;

/// <summary>
/// The minimum serialized length in the serialized payload.
/// </summary>
internal const uint MinSerializedLength =
CcdAmount.BytesLength +
Hash.BytesLength + // ModuleRef
ContractName.MinSerializedLength +
Parameter.MinSerializedLength;

/// <summary>
/// Prepares the account transaction payload for signing.
/// </summary>
/// <param name="sender">Address of the sender of the transaction.</param>
/// <param name="sequenceNumber">Account sequence number to use for the transaction.</param>
/// <param name="expiry">Expiration time of the transaction.</param>
/// <param name="energy">
/// The amount of energy that can be used for contract execution.
/// The base energy amount for transaction verification will be added to this cost.
/// </param>
public PreparedAccountTransaction Prepare(
AccountAddress sender,
AccountSequenceNumber sequenceNumber,
Expiry expiry,
EnergyAmount energy
) => new(sender, sequenceNumber, expiry, energy, this);

/// <summary>
/// Gets the size (number of bytes) of the payload.
/// </summary>
internal override PayloadSize Size() => new(
sizeof(TransactionType) +
CcdAmount.BytesLength +
Hash.BytesLength + // ModuleRef
this.ContractName.SerializedLength() +
this.Parameter.SerializedLength());

/// <summary>
/// Deserialize a "InitContract" payload from a serialized byte array.
/// </summary>
/// <param name="bytes">The serialized InitContract payload.</param>
/// <param name="output">Where to write the result of the operation.</param>
public static bool TryDeserial(ReadOnlySpan<byte> 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 remainingBytes = bytes[sizeof(TransactionType)..];

if (!CcdAmount.TryDeserial(remainingBytes, out var amount))
{
output = (null, amount.Error);
return false;
};
remainingBytes = remainingBytes[(int)CcdAmount.BytesLength..];

if (!ModuleReference.TryDeserial(remainingBytes, out var moduleRef))
{
output = (null, moduleRef.Error);
return false;
};
remainingBytes = remainingBytes[Hash.BytesLength..]; // ModuleRef

if (!ContractName.TryDeserial(remainingBytes, out var name))
{
output = (null, name.Error);
return false;
};
remainingBytes = remainingBytes[(int)name.ContractName!.SerializedLength()..];

if (!Parameter.TryDeserial(remainingBytes, out var param))
{
output = (null, param.Error);
return false;
};

if (amount.Amount == null || moduleRef.Ref == null || name.ContractName == null || param.Parameter == null)
{
var msg = $"Amount, ModuleRef, ContractName or Parameter were null, but did not produce an error";
output = (null, msg);
return false;
}

output = (new InitContract(amount.Amount.Value, moduleRef.Ref, name.ContractName, param.Parameter), null);
return true;
}

/// <summary>
/// Copies the "init_contract" transaction in the binary format expected by the node to a byte array.
/// </summary>
public override byte[] ToBytes()
{
using var memoryStream = new MemoryStream((int)this.Size().Size);
memoryStream.WriteByte(TransactionType);
memoryStream.Write(this.Amount.ToBytes());
memoryStream.Write(this.ModuleRef.ToBytes());
memoryStream.Write(this.ContractName.ToBytes());
memoryStream.Write(this.Parameter.ToBytes());
return memoryStream.ToArray();
}
}
71 changes: 71 additions & 0 deletions src/Types/ContractName.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Buffers.Binary;
using System.Text;
using Concordium.Sdk.Helpers;

namespace Concordium.Sdk.Types;
Expand All @@ -17,6 +19,16 @@ public sealed record ContractName
/// </summary>
public string Name { get; init; }

/// <summary>
/// Gets the minimum serialized length (number of bytes) of the init name.
/// </summary>
internal const uint MinSerializedLength = sizeof(ushort);

/// <summary>
/// Gets the serialized length (number of bytes) of the init name.
/// </summary>
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);
Expand All @@ -36,6 +48,59 @@ public static bool TryParse(string name, out (ContractName? ContractName, Valida
return validate;
}

/// <summary>
/// Copies the init name to a byte array which has the length preprended.
/// </summary>
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();
}

/// <summary>
/// Deserialize an init name from a serialized byte array.
/// </summary>
/// <param name="bytes">The serialized init name.</param>
/// <param name="output">Where to write the result of the operation.</param>
public static bool TryDeserial(ReadOnlySpan<byte> 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 + 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}";
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;
};
}

/// <summary>
/// Get the contract name part of <see cref="Name"/>.
/// </summary>
Expand Down Expand Up @@ -79,4 +144,10 @@ private static bool IsValid(string name, out ValidationError? error)
error = null;
return true;
}

/// <summary>Check for equality.</summary>
public bool Equals(ContractName? other) => other != null && this.Name == other.Name;

/// <summary>Gets hash code.</summary>
public override int GetHashCode() => this.Name.GetHashCode();
}
Loading
Loading