diff --git a/CHANGELOG.md b/CHANGELOG.md
index e2224c24..69624589 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,10 @@
## Unreleased changes
+
- Bugfix
- Switched the GitHub runners from using 'ubuntu-latest' to 'ubuntu-20.04' to ensure compatibility with the default .NET 6 Docker image for the SDK.
- Added
- New GRPC-endpoint: `GetBlockItems`
- - New transaction `DeployModule`
+ - New transactions: `DeployModule` and `UpdateContract`.
- 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`
- Breaking
diff --git a/ConcordiumNetSdk.sln b/ConcordiumNetSdk.sln
index b291f93c..34c135b9 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}") = "Transations.UpdateContract", "examples\UpdateContract\Transations.UpdateContract.csproj", "{DBFBB7D1-E82D-4380-8263-B4B0AC3A6266}"
+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
+ {DBFBB7D1-E82D-4380-8263-B4B0AC3A6266}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DBFBB7D1-E82D-4380-8263-B4B0AC3A6266}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DBFBB7D1-E82D-4380-8263-B4B0AC3A6266}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DBFBB7D1-E82D-4380-8263-B4B0AC3A6266}.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}
+ {DBFBB7D1-E82D-4380-8263-B4B0AC3A6266} = {FD2CDD9F-4650-4705-9CA2-98CC81F8891D}
EndGlobalSection
EndGlobal
diff --git a/examples/GetAccountInfo/Program.cs b/examples/GetAccountInfo/Program.cs
index 8fb5ff6c..dd7d61ba 100644
--- a/examples/GetAccountInfo/Program.cs
+++ b/examples/GetAccountInfo/Program.cs
@@ -11,7 +11,7 @@ namespace GetAccountInfo;
internal sealed class GetAccountInfoOptions
{
[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(
diff --git a/examples/GetAccountList/Program.cs b/examples/GetAccountList/Program.cs
index 43f8122d..1f0c4982 100644
--- a/examples/GetAccountList/Program.cs
+++ b/examples/GetAccountList/Program.cs
@@ -11,7 +11,7 @@ namespace GetAccountList;
internal sealed class GetAccountListOptions
{
[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(
diff --git a/examples/GetAncestors/Program.cs b/examples/GetAncestors/Program.cs
index 5a2b48ea..8e3f70ce 100644
--- a/examples/GetAncestors/Program.cs
+++ b/examples/GetAncestors/Program.cs
@@ -11,7 +11,7 @@ namespace GetAncestors;
internal sealed class GetAncestorsOptions
{
[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(
'm',
diff --git a/examples/GetBakerList/Program.cs b/examples/GetBakerList/Program.cs
index d6a67b3a..72561c3c 100644
--- a/examples/GetBakerList/Program.cs
+++ b/examples/GetBakerList/Program.cs
@@ -11,7 +11,7 @@ namespace GetBakerList;
internal sealed class GetBakerListOptions
{
[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; }
}
diff --git a/examples/GetBlockChainParameters/Program.cs b/examples/GetBlockChainParameters/Program.cs
index 485ac22d..efef55d5 100644
--- a/examples/GetBlockChainParameters/Program.cs
+++ b/examples/GetBlockChainParameters/Program.cs
@@ -13,7 +13,7 @@ namespace GetBlockChainParameters;
internal sealed class GetBlockChainParametersOptions
{
[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; }
}
diff --git a/examples/GetBlockFinalizationSummary/Program.cs b/examples/GetBlockFinalizationSummary/Program.cs
index 0c020cd7..48b418eb 100644
--- a/examples/GetBlockFinalizationSummary/Program.cs
+++ b/examples/GetBlockFinalizationSummary/Program.cs
@@ -11,7 +11,7 @@ namespace GetBlockFinalizationSummary;
internal sealed class GetBlockFinalizationSummaryOptions
{
[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; }
}
diff --git a/examples/GetBlockInfo/Program.cs b/examples/GetBlockInfo/Program.cs
index c859cc9d..d01dc7e7 100644
--- a/examples/GetBlockInfo/Program.cs
+++ b/examples/GetBlockInfo/Program.cs
@@ -11,7 +11,7 @@ namespace GetBlockInfo;
internal sealed class GetBlockInfoOptions
{
[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(
diff --git a/examples/GetBlockItemStatus/Program.cs b/examples/GetBlockItemStatus/Program.cs
index 54a39f65..184d4fe3 100644
--- a/examples/GetBlockItemStatus/Program.cs
+++ b/examples/GetBlockItemStatus/Program.cs
@@ -13,7 +13,7 @@ internal sealed class GetBlockItemSummaryOptions
[Option(HelpText = "Transaction hash to lookup", Required = true)]
public string TransactionHash { get; set; }
- [Option(HelpText = "URL representing the endpoint where the gRPC V2 API is served.", Default = "http://node.testnet.concordium.com:20000")]
+ [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; }
}
@@ -37,7 +37,7 @@ public static class Program
/// An example showing how one can query transaction status from a node.
///
/// GetBlockItemSummaryOptions
- /// Example: --endpoint http://node.testnet.concordium.com:20000 --transactionhash 143ca4183d0bb204000ad08e0fd5792985c808861b97f3b81cb9016ad39d09d2
+ /// Example: --endpoint http://grpc.testnet.concordium.com:20000 --transactionhash 143ca4183d0bb204000ad08e0fd5792985c808861b97f3b81cb9016ad39d09d2
///
public static async Task Main(string[] args)
{
diff --git a/examples/GetBlockItems/Program.cs b/examples/GetBlockItems/Program.cs
index faf45245..f62fc166 100644
--- a/examples/GetBlockItems/Program.cs
+++ b/examples/GetBlockItems/Program.cs
@@ -11,7 +11,7 @@ namespace GetBlockItems;
internal sealed class GetBlocksOptions
{
[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(
'b',
@@ -38,7 +38,7 @@ private static async Task Run(GetBlocksOptions o)
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)
{
diff --git a/examples/GetBlockPendingUpdates/Program.cs b/examples/GetBlockPendingUpdates/Program.cs
index 563ca606..6f897ae2 100644
--- a/examples/GetBlockPendingUpdates/Program.cs
+++ b/examples/GetBlockPendingUpdates/Program.cs
@@ -11,7 +11,7 @@ namespace GetBlockPendingUpdates;
internal sealed class GetBlockPendingUpdatesOptions
{
[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(
'b',
diff --git a/examples/GetBlockSpecialEvents/Program.cs b/examples/GetBlockSpecialEvents/Program.cs
index f08ae2b8..24ebf47f 100644
--- a/examples/GetBlockSpecialEvents/Program.cs
+++ b/examples/GetBlockSpecialEvents/Program.cs
@@ -11,7 +11,7 @@ namespace GetBlockSpecialEvents;
internal sealed class GetBlockSpecialEventsOptions
{
[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(
diff --git a/examples/GetBlockTransactionEvents/Program.cs b/examples/GetBlockTransactionEvents/Program.cs
index 9b6f14af..3a01664f 100644
--- a/examples/GetBlockTransactionEvents/Program.cs
+++ b/examples/GetBlockTransactionEvents/Program.cs
@@ -12,7 +12,7 @@ namespace GetBlockTransactionEvents;
internal sealed class GetBlockTransactionEventsOptions
{
[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; }
}
diff --git a/examples/GetBlocks/Program.cs b/examples/GetBlocks/Program.cs
index ed840f1a..ce9c3b84 100644
--- a/examples/GetBlocks/Program.cs
+++ b/examples/GetBlocks/Program.cs
@@ -10,7 +10,7 @@ namespace GetBlocks;
internal sealed class GetBlocksOptions
{
[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; }
}
diff --git a/examples/GetBlocksAtHeight/Program.cs b/examples/GetBlocksAtHeight/Program.cs
index 2ed1a548..aa69e18f 100644
--- a/examples/GetBlocksAtHeight/Program.cs
+++ b/examples/GetBlocksAtHeight/Program.cs
@@ -11,7 +11,7 @@ namespace GetBlocksAtHeight;
internal sealed class GetBlocksAtHeightOptions
{
[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; }
}
diff --git a/examples/GetBranches/Program.cs b/examples/GetBranches/Program.cs
index e1fbd81a..12f8178c 100644
--- a/examples/GetBranches/Program.cs
+++ b/examples/GetBranches/Program.cs
@@ -11,7 +11,7 @@ namespace GetBranches;
internal sealed class GetBranchesOptions
{
[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; }
}
diff --git a/examples/GetConsensusInfo/Program.cs b/examples/GetConsensusInfo/Program.cs
index d86c5959..4adfe096 100644
--- a/examples/GetConsensusInfo/Program.cs
+++ b/examples/GetConsensusInfo/Program.cs
@@ -13,7 +13,7 @@ namespace GetConsensusInfo;
internal sealed class GetConsensusInfoOptions
{
[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; }
}
diff --git a/examples/GetFinalizedBlocks/Program.cs b/examples/GetFinalizedBlocks/Program.cs
index 2808d64b..fb8a0015 100644
--- a/examples/GetFinalizedBlocks/Program.cs
+++ b/examples/GetFinalizedBlocks/Program.cs
@@ -10,7 +10,7 @@ namespace GetFinalizedBlocks;
internal sealed class GetFinalizedBlocksOptions
{
[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; }
}
diff --git a/examples/GetIdentityProviders/Program.cs b/examples/GetIdentityProviders/Program.cs
index 1916e74e..33ea5f2f 100644
--- a/examples/GetIdentityProviders/Program.cs
+++ b/examples/GetIdentityProviders/Program.cs
@@ -11,7 +11,7 @@ namespace GetIdentityProviders;
internal sealed class GetIdentityProvidersOptions
{
[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; }
}
diff --git a/examples/GetInstanceInfo/Program.cs b/examples/GetInstanceInfo/Program.cs
index 9f46613c..17889f74 100644
--- a/examples/GetInstanceInfo/Program.cs
+++ b/examples/GetInstanceInfo/Program.cs
@@ -13,7 +13,7 @@ namespace GetInstanceInfo;
internal sealed class GetInstanceInfoOptions
{
[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(
diff --git a/examples/GetInstanceList/Program.cs b/examples/GetInstanceList/Program.cs
index ffa27ed1..ec26134d 100644
--- a/examples/GetInstanceList/Program.cs
+++ b/examples/GetInstanceList/Program.cs
@@ -11,7 +11,7 @@ namespace GetInstanceList;
internal sealed class GetInstanceListOptions
{
[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(
diff --git a/examples/GetModuleList/Program.cs b/examples/GetModuleList/Program.cs
index 5ba1a171..bce36fe5 100644
--- a/examples/GetModuleList/Program.cs
+++ b/examples/GetModuleList/Program.cs
@@ -11,7 +11,7 @@ namespace GetModuleList;
internal sealed class GetModuleListOptions
{
[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(
diff --git a/examples/GetModuleSource/Program.cs b/examples/GetModuleSource/Program.cs
index 170b761e..7669af77 100644
--- a/examples/GetModuleSource/Program.cs
+++ b/examples/GetModuleSource/Program.cs
@@ -11,7 +11,7 @@ namespace GetModuleSource;
internal sealed class GetModuleSourceOptions
{
[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(
diff --git a/examples/GetNodeInfo/Program.cs b/examples/GetNodeInfo/Program.cs
index 143fa500..d5b767e9 100644
--- a/examples/GetNodeInfo/Program.cs
+++ b/examples/GetNodeInfo/Program.cs
@@ -10,7 +10,7 @@ namespace GetNodeInfo;
internal sealed class GetNodeInfoOptions
{
[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; }
}
diff --git a/examples/GetPassiveDelegationInfo/Program.cs b/examples/GetPassiveDelegationInfo/Program.cs
index f715b7df..09becb2b 100644
--- a/examples/GetPassiveDelegationInfo/Program.cs
+++ b/examples/GetPassiveDelegationInfo/Program.cs
@@ -11,7 +11,7 @@ namespace GetPassiveDelegationInfo;
internal sealed class GetPassiveDelegationInfoOptions
{
[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(
diff --git a/examples/GetPoolInfo/Program.cs b/examples/GetPoolInfo/Program.cs
index 3bd2bc89..9be61918 100644
--- a/examples/GetPoolInfo/Program.cs
+++ b/examples/GetPoolInfo/Program.cs
@@ -11,7 +11,7 @@ namespace GetPoolInfo;
internal sealed class GetPoolInfoOptions
{
[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(
diff --git a/examples/GetTokenomicsInfo/Program.cs b/examples/GetTokenomicsInfo/Program.cs
index cf4de584..5eacdf69 100644
--- a/examples/GetTokenomicsInfo/Program.cs
+++ b/examples/GetTokenomicsInfo/Program.cs
@@ -10,7 +10,7 @@ namespace GetTokenomicsInfo;
internal sealed class GetTokenomicsInfoOptions
{
[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(
diff --git a/examples/UpdateContract/Program.cs b/examples/UpdateContract/Program.cs
new file mode 100644
index 00000000..e60e5d31
--- /dev/null
+++ b/examples/UpdateContract/Program.cs
@@ -0,0 +1,94 @@
+using CommandLine;
+using Concordium.Sdk.Client;
+using Concordium.Sdk.Types;
+using Concordium.Sdk.Wallets;
+
+namespace Transactions.UpdateContract;
+
+// We disable these warnings since CommandLine needs to set properties in options
+// but we don't want to give default values.
+#pragma warning disable CS8618
+
+internal sealed class UpdateTransactionExampleOptions
+{
+ [Option(
+ 'k',
+ "keys",
+ HelpText = "Path to a file with contents that is in the Concordium browser wallet key export format.",
+ Required = true
+ )]
+ public string WalletKeysFile { get; set; }
+ [Option(HelpText = "URL representing the endpoint where the gRPC V2 API is served.",
+ Default = "https://grpc.testnet.concordium.com:20000/")]
+ public string Endpoint { get; set; }
+ [Option('a', "amount", HelpText = "Amount of CCD to deposit.", Default = 0)]
+ public ulong Amount { get; set; }
+
+ [Option('c', "contract", HelpText = "The index of the smart contract.", Required = true)]
+ public ulong Contract { get; set; }
+
+ [Option('r', "receive-name", HelpText = "The receive_name of the contract to be called.", Required = true)]
+ public string ReceiveName { get; set; }
+
+ [Option('e', "max-energy", HelpText = "The maximum energy to spend on the module.", Required = true)]
+ public ulong MaxEnergy { get; set; }
+}
+
+///
+/// Example demonstrating how to submit a transaction updating a smart contract.
+///
+/// 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.
+///
+/// See for more info
+/// on how to run the program, or refer to the help message.
+///
+internal class Program
+{
+ ///
+ /// Example send a contract update transaction.
+ ///
+ public static async Task Main(string[] args) =>
+ await Parser.Default
+ .ParseArguments(args)
+ .WithParsedAsync(Run);
+
+ private static async Task Run(UpdateTransactionExampleOptions options)
+ {
+ // Read the account keys from a file.
+ var walletData = File.ReadAllText(options.WalletKeysFile);
+ var account = WalletAccount.FromWalletKeyExportFormat(walletData);
+
+ // Construct the client.
+ var clientOptions = new ConcordiumClientOptions
+ {
+ Timeout = TimeSpan.FromSeconds(10)
+ };
+ using var client = new ConcordiumClient(new Uri(options.Endpoint), clientOptions);
+
+ // Create the update transaction.
+ var amount = CcdAmount.FromCcd(options.Amount);
+ var contract = ContractAddress.From(options.Contract, 0);
+ var receiveName = ReceiveName.Parse(options.ReceiveName);
+ var parameter = Parameter.Empty();
+ var maxEnergy = new EnergyAmount(options.MaxEnergy);
+
+ var updatePayload = new Concordium.Sdk.Transactions.UpdateContract(amount, contract, receiveName, parameter);
+
+ // Prepare the transaction for signing.
+ var sender = account.AccountAddress;
+ var sequenceNumber = client.GetNextAccountSequenceNumber(sender).Item1;
+ var expiry = Expiry.AtMinutesFromNow(30);
+ var preparedPayload = updatePayload.Prepare(sender, sequenceNumber, expiry, maxEnergy);
+
+ // Sign the transaction using the account keys.
+ var signedTransaction = preparedPayload.Sign(account);
+
+ // Submit the transaction.
+ var txHash = await client.SendAccountTransactionAsync(signedTransaction);
+
+ // Print the transaction hash.
+ Console.WriteLine($"Successfully submitted transfer transaction with hash {txHash}");
+ }
+}
diff --git a/examples/UpdateContract/Transations.UpdateContract.csproj b/examples/UpdateContract/Transations.UpdateContract.csproj
new file mode 100644
index 00000000..74addacb
--- /dev/null
+++ b/examples/UpdateContract/Transations.UpdateContract.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs
index 67617473..988e37e4 100644
--- a/src/Transactions/AccountTransactionPayload.cs
+++ b/src/Transactions/AccountTransactionPayload.cs
@@ -48,7 +48,12 @@ public Grpc.V2.AccountTransactionPayload ToProto() =>
),
PayloadCase.RawPayload => ParseRawPayload(payload.RawPayload),
PayloadCase.InitContract => throw new NotImplementedException(),
- PayloadCase.UpdateContract => throw new NotImplementedException(),
+ PayloadCase.UpdateContract => new UpdateContract(
+ CcdAmount.From(payload.UpdateContract.Amount),
+ ContractAddress.From(payload.UpdateContract.Address),
+ ReceiveName.From(payload.UpdateContract.ReceiveName),
+ Parameter.From(payload.UpdateContract.Parameter)
+ ),
PayloadCase.None => throw new MissingEnumException(payload.PayloadCase),
_ => throw new MissingEnumException(payload.PayloadCase),
};
@@ -83,8 +88,13 @@ private static AccountTransactionPayload ParseRawPayload(Google.Protobuf.ByteStr
parsedPayload = output;
break;
}
- case TransactionType.InitContract:
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:
diff --git a/src/Transactions/UpdateContract.cs b/src/Transactions/UpdateContract.cs
new file mode 100644
index 00000000..f8763162
--- /dev/null
+++ b/src/Transactions/UpdateContract.cs
@@ -0,0 +1,115 @@
+using Concordium.Sdk.Types;
+
+namespace Concordium.Sdk.Transactions;
+
+/// A deployment of a Wasm smart contract module.
+/// Amount of CCD to send to the instance.
+/// Address of the instance to update.
+/// Name of the receive function to call to update the instance.
+/// Name of the receive function to call to update the instance.
+public sealed record UpdateContract(CcdAmount Amount, ContractAddress Address, ReceiveName ReceiveName, Parameter Parameter) : AccountTransactionPayload
+{
+ ///
+ /// 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, energy, this);
+
+ /// The account transaction type to be used in the serialized payload.
+ private const byte TransactionType = (byte)Types.TransactionType.Update;
+
+ ///
+ /// Gets the size (number of bytes) of the payload.
+ ///
+ internal override PayloadSize Size() => new(sizeof(TransactionType) + CcdAmount.BytesLength + ContractAddress.BytesLength + this.ReceiveName.SerializedLength() + this.Parameter.SerializedLength());
+
+ ///
+ /// Create a "update contract" payload from a serialized as bytes.
+ ///
+ /// The "update contract" payload as bytes.
+ /// Where to write the result of the operation.
+ public static bool TryDeserial(ReadOnlySpan bytes, out (UpdateContract? payload, string? Error) output)
+ {
+ var minLength = sizeof(TransactionType) + CcdAmount.BytesLength + ContractAddress.BytesLength + ReceiveName.MinSerializedLength + Parameter.MinSerializedLength;
+ if (bytes.Length < minLength)
+ {
+ var msg = $"Invalid input length in `UpdateContract.TryDeserial`. Expected at least {minLength}, found {bytes.Length}";
+ output = (null, msg);
+ return false;
+ }
+
+ if (bytes[0] != TransactionType)
+ {
+ var msg = $"Invalid transaction type in `UpdateContract.TryDeserial`. Expected {TransactionType}, found {bytes[0]}";
+ output = (null, msg);
+ return false;
+ }
+
+ var remaining_bytes = bytes[sizeof(TransactionType)..];
+
+ if (!CcdAmount.TryDeserial(remaining_bytes, out var amount))
+ {
+ output = (null, amount.Error);
+ return false;
+ };
+ remaining_bytes = remaining_bytes[(int)CcdAmount.BytesLength..];
+
+ if (!ContractAddress.TryDeserial(remaining_bytes, out var address))
+ {
+ output = (null, address.Error);
+ return false;
+ };
+ remaining_bytes = remaining_bytes[(int)ContractAddress.BytesLength..];
+
+ if (!ReceiveName.TryDeserial(remaining_bytes, out var receiveName))
+ {
+ output = (null, receiveName.Error);
+ return false;
+ };
+ remaining_bytes = remaining_bytes[(int)receiveName.receiveName!.SerializedLength()..];
+
+ if (!Parameter.TryDeserial(remaining_bytes, out var parameter))
+ {
+ output = (null, parameter.Error);
+ return false;
+ };
+
+ if (amount.Amount == null || address.Address == null || receiveName.receiveName == null || parameter.Parameter == null)
+ {
+ var msg = $"Unexpected null pointer when deserializing.";
+ output = (null, msg);
+ return false;
+ }
+
+ var payload = new UpdateContract(amount.Amount.Value, address.Address, receiveName.receiveName, parameter.Parameter);
+ output = (payload, null);
+ return true;
+
+ }
+
+ ///
+ /// Copies the "update contract" 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); // Safe to cast since a payload will never be large enough for this to overflow.
+ memoryStream.WriteByte(TransactionType);
+ memoryStream.Write(this.Amount.ToBytes());
+ memoryStream.Write(this.Address.ToBytes());
+ memoryStream.Write(this.ReceiveName.ToBytes());
+ memoryStream.Write(this.Parameter.ToBytes());
+ return memoryStream.ToArray();
+ }
+}
+
diff --git a/src/Types/CcdAmount.cs b/src/Types/CcdAmount.cs
index a632e18f..9a315f85 100644
--- a/src/Types/CcdAmount.cs
+++ b/src/Types/CcdAmount.cs
@@ -132,7 +132,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (CcdAmount? Amount,
}
///
- /// Copies the CCD amuunt represented in big-endian format to byte array.
+ /// Copies the CCD amount represented in big-endian format to byte array.
///
public byte[] ToBytes() => Serialization.ToBytes(this.Value);
diff --git a/src/Types/ContractAddress.cs b/src/Types/ContractAddress.cs
index a505d0aa..0d5f0437 100644
--- a/src/Types/ContractAddress.cs
+++ b/src/Types/ContractAddress.cs
@@ -1,3 +1,6 @@
+using System.Buffers.Binary;
+using Concordium.Sdk.Helpers;
+
namespace Concordium.Sdk.Types;
///
@@ -20,10 +23,47 @@ public sealed record ContractAddress(ulong Index, ulong SubIndex) : IAddress
internal static ContractAddress From(Grpc.V2.ContractAddress contractAddress) => new(contractAddress.Index, contractAddress.Subindex);
+ ///
+ /// Byte size of .
+ ///
+ public const uint BytesLength = 16;
+
///
/// Converts the contract address to its corresponding protocol buffer message instance.
///
/// This can be used as the input for class methods of .
///
public Grpc.V2.ContractAddress ToProto() => new() { Index = this.Index, Subindex = this.SubIndex };
+
+ ///
+ /// Attempt to deserialize a contract address from a span of bytes.
+ ///
+ /// The span of bytes.
+ /// Where to write the result of the operation.
+ public static bool TryDeserial(ReadOnlySpan bytes, out (ContractAddress? Address, string? Error) output)
+ {
+ if (bytes.Length < BytesLength)
+ {
+ var msg = $"Invalid length of input in `ContractAddress.TryDeserial`. Expected at least {BytesLength} bytes, found {bytes.Length}";
+ output = (null, msg);
+ return false;
+ };
+
+ var index = BinaryPrimitives.ReadUInt64BigEndian(bytes);
+ var subindex = BinaryPrimitives.ReadUInt64BigEndian(bytes);
+
+ output = (new ContractAddress(index, subindex), null);
+ return true;
+ }
+
+ ///
+ /// Serialize the ContractAddress in big-endian format.
+ ///
+ public byte[] ToBytes()
+ {
+ using var memoryStream = new MemoryStream((int)BytesLength); // Safe to cast since we know BytesLength is within the range of int.
+ memoryStream.Write(Serialization.ToBytes(this.Index));
+ memoryStream.Write(Serialization.ToBytes(this.SubIndex));
+ return memoryStream.ToArray();
+ }
}
diff --git a/src/Types/Parameter.cs b/src/Types/Parameter.cs
index 14f4e177..56d389f4 100644
--- a/src/Types/Parameter.cs
+++ b/src/Types/Parameter.cs
@@ -1,3 +1,6 @@
+using System.Buffers.Binary;
+using Concordium.Sdk.Helpers;
+
namespace Concordium.Sdk.Types;
///
@@ -5,6 +8,69 @@ namespace Concordium.Sdk.Types;
///
public sealed record Parameter(byte[] Param)
{
+ ///
+ /// Construct an empty smart contract parameter.
+ ///
+ public static Parameter Empty() => new(Array.Empty());
+
+ private const uint MaxByteLength = 65535;
+ ///
+ /// 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);
+
+ internal static Parameter From(Grpc.V2.Parameter parameter) => new(parameter.Value.ToArray());
+
+ ///
+ /// 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);
+ if (sizeRead > MaxByteLength)
+ {
+ var msg = $"Invalid length of input in `Parameter.TryDeserial`. The parameter size can be at most {MaxByteLength} bytes, found {bytes.Length}";
+ output = (null, msg);
+ return false;
+ }
+
+ var size = sizeof(ushort) + sizeRead;
+ 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;
+ };
+ var parameter = new Parameter(bytes.Slice(sizeof(ushort), sizeRead).ToArray());
+ output = (parameter, null);
+ return true;
+ }
+
///
/// Convert parameters to hex string.
///
diff --git a/src/Types/ReceiveName.cs b/src/Types/ReceiveName.cs
index e7f2f125..b98800d4 100644
--- a/src/Types/ReceiveName.cs
+++ b/src/Types/ReceiveName.cs
@@ -1,3 +1,5 @@
+using System.Buffers.Binary;
+using System.Text;
using Concordium.Sdk.Helpers;
namespace Concordium.Sdk.Types;
@@ -34,6 +36,20 @@ public static bool TryParse(string name, out (ReceiveName? ReceiveName, Validati
return validate;
}
+ ///
+ /// Parse input name against expected format.
+ ///
+ /// Input receive name.
+ /// The parsed receive name
+ public static ReceiveName Parse(string name)
+ {
+ if (!TryParse(name, out var result))
+ {
+ throw new ArgumentException(ValidationErrorToString(result.Error!.Value));
+ }
+ return result.ReceiveName!;
+ }
+
///
/// Get the contract name part of .
///
@@ -56,6 +72,14 @@ public enum ValidationError
InvalidCharacters,
}
+ private static string ValidationErrorToString(ValidationError error) => error switch
+ {
+ ValidationError.MissingDotSeparator => $"Receive name did not include the mandatory '.' character.",
+ ValidationError.TooLong => $"The receive name is more than 100 characters.",
+ ValidationError.InvalidCharacters => $"The receive name contained invalid characters.",
+ _ => throw new NotImplementedException(),
+ };
+
private static bool IsValid(string name, out ValidationError? error)
{
if (!name.Contains('.'))
@@ -76,4 +100,73 @@ private static bool IsValid(string name, out ValidationError? error)
error = null;
return true;
}
+
+ ///
+ /// Attempt to deserialize a span of bytes into a smart contract receive name.
+ ///
+ /// The span of bytes potentially containing a receive name.
+ /// Where to write the result of the operation
+ public static bool TryDeserial(ReadOnlySpan bytes, out (ReceiveName? receiveName, string? Error) output)
+ {
+ if (bytes.Length < MinSerializedLength)
+ {
+ var msg = $"Invalid length of input in `ReceiveName.TryDeserial`. Expected at least {MinSerializedLength}, found {bytes.Length}";
+ output = (null, msg);
+ return false;
+ };
+ var sizeRead = BinaryPrimitives.ReadUInt16BigEndian(bytes); // This should never throw, since we already checked the length.
+ var size = sizeof(ushort) + sizeRead;
+
+ if (size > bytes.Length)
+ {
+ var msg = $"Invalid length of input in `ReceiveName.TryDeserial`. Expected array of size at least {size}, found {bytes.Length}";
+ output = (null, msg);
+ return false;
+ };
+
+ try
+ {
+ var ascii = Encoding.ASCII.GetString(bytes[sizeof(ushort)..sizeRead]);
+
+ if (!TryParse(ascii, out var parseOut))
+ {
+ var error = ValidationErrorToString(parseOut.Error!.Value);
+ output = (null, error);
+ return false;
+
+ }
+ output = (parseOut.ReceiveName, null);
+ return true;
+ }
+ catch (ArgumentException e)
+ {
+ var msg = $"Invalid ReceiveName in `ReceiveName.TryDeserial`: {e.Message}";
+ output = (null, msg);
+ return false;
+ };
+ }
+
+ ///
+ /// Gets the serialized length (number of bytes) of the receive name.
+ ///
+ internal uint SerializedLength() => MinSerializedLength + (uint)this.Receive.Length; // Safe to cast the length since a valid receive name is at most 100.
+
+ ///
+ /// Gets the minimum serialized length (number of bytes) of the receive name.
+ ///
+ internal const uint MinSerializedLength = sizeof(ushort);
+
+ ///
+ /// Serialize the smart contract receive name into 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.Receive.Length)); // Safe since a valid receive name must be within 100 ASCII characters.
+ var bytes = new byte[this.Receive.Length];
+ Encoding.ASCII.GetBytes(this.Receive, bytes);
+ memoryStream.Write(bytes);
+ return memoryStream.ToArray();
+ }
+
}
diff --git a/tests/IntegrationTests/test_configuration.example.json b/tests/IntegrationTests/test_configuration.example.json
index 21b4207f..f464ba61 100644
--- a/tests/IntegrationTests/test_configuration.example.json
+++ b/tests/IntegrationTests/test_configuration.example.json
@@ -1,5 +1,5 @@
{
- "uri": "http://node.testnet.concordium.com:20000/",
+ "uri": "http://grpc.testnet.concordium.com:20000/",
"accountAddress": "someAddress",
"blockHash": "789c17879e4594225a0ae8363d2e9364676f93e7d61d33ec9b5d23345aa156ba",
"walletPath": "...someAddress.export",