Skip to content

Commit

Permalink
Merge pull request #89 from Concordium/init-contract
Browse files Browse the repository at this point in the history
Add Init Contract transaction
  • Loading branch information
rasmus-kirk authored Feb 6, 2024
2 parents be6a44e + c0f0d2c commit 580517f
Show file tree
Hide file tree
Showing 13 changed files with 555 additions and 8 deletions.
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

0 comments on commit 580517f

Please sign in to comment.