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",