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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
7 changes: 7 additions & 0 deletions ConcordiumNetSdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
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>
85 changes: 85 additions & 0 deletions examples/InitContract/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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://node.testnet.concordium.com:20000/")]
rasmus-kirk marked this conversation as resolved.
Show resolved Hide resolved
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 how to use <see cref="ConcordiumClient.GetBlockItems"/>
rasmus-kirk marked this conversation as resolved.
Show resolved Hide resolved
/// </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 transfer transaction.
var amount = CcdAmount.FromCcd(o.Amount);
var moduleRef = new ModuleReference(o.ModuleRef);
var initName = new InitName(o.InitName);
var param = new Parameter(Array.Empty<byte>());
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.
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}");
}
}

144 changes: 144 additions & 0 deletions src/Transactions/InitContract.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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="InitName">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, InitName InitName, 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 +
InitName.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, new EnergyAmount(TrxBaseCost) + energy, this);
rasmus-kirk marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// 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
/// </summary>
private const ushort TrxBaseCost = 300;
rasmus-kirk marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets the size (number of bytes) of the payload.
/// </summary>
internal override PayloadSize Size() => new(
sizeof(TransactionType) +
CcdAmount.BytesLength +
Hash.BytesLength +
rasmus-kirk marked this conversation as resolved.
Show resolved Hide resolved
this.InitName.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 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))..];
rasmus-kirk marked this conversation as resolved.
Show resolved Hide resolved
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)
{
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))..];
if (!Parameter.TryDeserial(paramBytes, out var param))
{
output = (null, param.Error);
return false;
};

if (amount.Amount == null || moduleRef.Ref == null || param.Parameter == null)
{
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);
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.InitName.ToBytes());
memoryStream.Write(this.Parameter.ToBytes());
return memoryStream.ToArray();
}
}
38 changes: 38 additions & 0 deletions src/Types/EnergyAmount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Concordium.Sdk.Types;
/// <param name="Value">Value of the energy amount.</param>
public readonly record struct EnergyAmount(ulong Value)
{
///<summary>Byte length of Energy. Used for serialization.</summary>
public const uint BytesLength = sizeof(ulong);

/// <summary>
Expand All @@ -16,4 +17,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);

/// <summary>
/// Add Energy amounts.
/// </summary>
/// <exception cref="ArgumentException">The result odoes not fit in <see cref="ulong"/></exception>
public static EnergyAmount operator +(EnergyAmount a, EnergyAmount b)
limemloh marked this conversation as resolved.
Show resolved Hide resolved
{
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
);
}
}

/// <summary>
/// Subtract Energy amounts.
/// </summary>
/// <exception cref="ArgumentException">The result does not fit in <see cref="ulong"/></exception>
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
);
}
}

}
Loading