From 022aed53659ae01dd9d9fb02930ed5f2197942d3 Mon Sep 17 00:00:00 2001 From: Shargon Date: Tue, 13 Feb 2024 13:01:37 +0100 Subject: [PATCH] new Testing environment (#890) * Creating source from nef * change to LF * Fix _deploy in NEP17 template * change comment * Replace SetOwner * Remove extra file * Remove _deploy and _initialize * change to _ * Add args * Separate safe from unsafe * abstract Class * format * Only source * Generate events * Mock * starting native artifacts * CryptoLib * Rename project to avoid old one * LF * Escape name * Avoid generation native artifacts * TestNativeContracts * Redirect Logs and Notifications to the mocked smart contract * Mock notifications * Converts * format * LoadFromJson * Initialize native contracts * Partially mocked * Mocked with args * Fix Scope * Found bug in MemoryStore * TriggerType.Application * clean and fixes * format * Remove utartifact * Fix FromHash * Clean code * Store current script in transaction * Allow to set the Gas * Clean code * Reduce changes * Change Snapshot * Change to Artifact to ArtifactExtensions * Clean code * Move to Extensions folder * Fix Native initialization * Allow Custom mocks * Format * some vars * Add some summaries * Allow to mock undeployed contracts * Test undeployed mocked contracts * Change init native to true by default * Clean using * Allow null in deploy * Modify ContractManagement artifact * Readme * Fix menu * Resume example using native access * Clean storage example * fix readme * Fix readme * Fix readme and move CommitteeAddress * Add event testing to readme * Allow properties! * Allow properties if the method is without args and safe * User uppercase for first letter (conflicts with events and methods in native contracts) * All events start with "On" (avoid native conflicts) * Rename log event * Avoid re-query contract id if it was checked * format * Update Readme examples * Improve Mock.OnSysCall * Some small changes * Generate compiled artifacts (.dll) * LF * is 3 not 2 * Update src/Neo.SmartContract.Testing/README.md * Update src/Neo.SmartContract.Testing/NativeArtifacts.cs * Sign by default with Validators and committee * Rename to ValidatorsAddress * Allow to get address without initialize * Clean comment * Allow deploy with byte[] * allow null on deploy and update * Update readme * Fix static event * Allow native init notifications * Belane's feedback * Nullable when ends in object argument * Speedup InvokeOnNotify * Increase protected words * Allow Export and Import contract storage * format * Fix summary * Known limitations * Fix comments * Fix "Value" string * devcontainer ready for testing PR * Revert "devcontainer ready for testing PR" This reverts commit 04cc1ef12ac1ec266c54638a7b827e173d0309e7. * Add more ways to set the signers easy * Avoid base64 strings * cschuchardt88's feedback * Change to IList when Array * Clean Check initialized --------- Co-authored-by: Vitor Nazario Coelho --- neo-devpack-dotnet.sln | 34 +- .../Neo.Compiler.CSharp.csproj | 1 + src/Neo.Compiler.CSharp/Options.cs | 1 + src/Neo.Compiler.CSharp/Program.cs | 63 +++ src/Neo.SmartContract.Testing/CustomMock.cs | 17 + .../Extensions/ArtifactExtensions.cs | 325 ++++++++++++ .../Extensions/MockExtensions.cs | 108 ++++ .../Extensions/TestExtensions.cs | 108 ++++ .../Native/ContractManagement.cs | 72 +++ .../Native/CryptoLib.cs | 65 +++ .../Native/GasToken.cs | 37 ++ .../Native/LedgerContract.cs | 49 ++ .../Native/NeoToken.cs | 79 +++ .../Native/OracleContract.cs | 37 ++ .../Native/PolicyContract.cs | 47 ++ .../Native/RoleManagement.cs | 32 ++ .../Native/StdLib.cs | 120 +++++ .../NativeArtifacts.cs | 149 ++++++ .../Neo.SmartContract.Testing.csproj | 23 + src/Neo.SmartContract.Testing/README.md | 290 +++++++++++ .../SmartContract.cs | 112 ++++ .../SmartContractInitialize.cs | 20 + .../SmartContractStorage.cs | 178 +++++++ src/Neo.SmartContract.Testing/TestEngine.cs | 488 ++++++++++++++++++ src/Neo.SmartContract.Testing/TestStorage.cs | 143 +++++ .../TestingApplicationEngine.cs | 82 +++ .../Extensions/ArtifactExtensionsTests.cs | 97 ++++ .../NativeArtifactsTests.cs | 106 ++++ ...Neo.SmartContract.Testing.UnitTests.csproj | 11 + .../SmartContractStorageTests.cs | 58 +++ .../TestEngineTests.cs | 127 +++++ .../TestStorageTests.cs | 70 +++ 32 files changed, 3139 insertions(+), 10 deletions(-) create mode 100644 src/Neo.SmartContract.Testing/CustomMock.cs create mode 100644 src/Neo.SmartContract.Testing/Extensions/ArtifactExtensions.cs create mode 100644 src/Neo.SmartContract.Testing/Extensions/MockExtensions.cs create mode 100644 src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs create mode 100644 src/Neo.SmartContract.Testing/Native/ContractManagement.cs create mode 100644 src/Neo.SmartContract.Testing/Native/CryptoLib.cs create mode 100644 src/Neo.SmartContract.Testing/Native/GasToken.cs create mode 100644 src/Neo.SmartContract.Testing/Native/LedgerContract.cs create mode 100644 src/Neo.SmartContract.Testing/Native/NeoToken.cs create mode 100644 src/Neo.SmartContract.Testing/Native/OracleContract.cs create mode 100644 src/Neo.SmartContract.Testing/Native/PolicyContract.cs create mode 100644 src/Neo.SmartContract.Testing/Native/RoleManagement.cs create mode 100644 src/Neo.SmartContract.Testing/Native/StdLib.cs create mode 100644 src/Neo.SmartContract.Testing/NativeArtifacts.cs create mode 100644 src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj create mode 100644 src/Neo.SmartContract.Testing/README.md create mode 100644 src/Neo.SmartContract.Testing/SmartContract.cs create mode 100644 src/Neo.SmartContract.Testing/SmartContractInitialize.cs create mode 100644 src/Neo.SmartContract.Testing/SmartContractStorage.cs create mode 100644 src/Neo.SmartContract.Testing/TestEngine.cs create mode 100644 src/Neo.SmartContract.Testing/TestStorage.cs create mode 100644 src/Neo.SmartContract.Testing/TestingApplicationEngine.cs create mode 100644 tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs create mode 100644 tests/Neo.SmartContract.Testing.UnitTests/NativeArtifactsTests.cs create mode 100644 tests/Neo.SmartContract.Testing.UnitTests/Neo.SmartContract.Testing.UnitTests.csproj create mode 100644 tests/Neo.SmartContract.Testing.UnitTests/SmartContractStorageTests.cs create mode 100644 tests/Neo.SmartContract.Testing.UnitTests/TestEngineTests.cs create mode 100644 tests/Neo.SmartContract.Testing.UnitTests/TestStorageTests.cs diff --git a/neo-devpack-dotnet.sln b/neo-devpack-dotnet.sln index ab0501653..2a5c1b4cd 100644 --- a/neo-devpack-dotnet.sln +++ b/neo-devpack-dotnet.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28803.452 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34003.232 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.SmartContract.Framework", "src\Neo.SmartContract.Framework\Neo.SmartContract.Framework.csproj", "{C30B5859-D4B9-46E8-A797-6B0A1B49B590}" EndProject @@ -16,22 +16,26 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{79389FC0-C62 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D5266066-0AFD-44D5-A83E-2F73668A63C8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Compiler.CSharp.TestContracts", "tests\Neo.Compiler.CSharp.TestContracts\Neo.Compiler.CSharp.TestContracts.csproj", "{8D67DD5A-D683-481F-915E-98683EA38791}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Compiler.CSharp.TestContracts", "tests\Neo.Compiler.CSharp.TestContracts\Neo.Compiler.CSharp.TestContracts.csproj", "{8D67DD5A-D683-481F-915E-98683EA38791}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.SmartContract.Framework.TestContracts", "tests\Neo.SmartContract.Framework.TestContracts\Neo.SmartContract.Framework.TestContracts.csproj", "{A372F1D6-51FF-472C-9508-FDAF7E6FEB13}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.SmartContract.Framework.TestContracts", "tests\Neo.SmartContract.Framework.TestContracts\Neo.SmartContract.Framework.TestContracts.csproj", "{A372F1D6-51FF-472C-9508-FDAF7E6FEB13}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.SmartContract.TestEngine", "tests\Neo.SmartContract.TestEngine\Neo.SmartContract.TestEngine.csproj", "{D0153204-6AEF-4D94-B0E1-8124C38C91D4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.SmartContract.TestEngine", "tests\Neo.SmartContract.TestEngine\Neo.SmartContract.TestEngine.csproj", "{D0153204-6AEF-4D94-B0E1-8124C38C91D4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo", "neo\src\Neo\Neo.csproj", "{73223FBD-C562-4FA0-9722-C7F1C382A9DE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo", "neo\src\Neo\Neo.csproj", "{73223FBD-C562-4FA0-9722-C7F1C382A9DE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Cryptography.BLS12_381", "neo\src\Neo.Cryptography.BLS12_381\Neo.Cryptography.BLS12_381.csproj", "{D541BCE9-65BC-475B-94E5-19B6BFFF2B8E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Cryptography.BLS12_381", "neo\src\Neo.Cryptography.BLS12_381\Neo.Cryptography.BLS12_381.csproj", "{D541BCE9-65BC-475B-94E5-19B6BFFF2B8E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Json", "neo\src\Neo.Json\Neo.Json.csproj", "{35A34EBD-F2BF-4D83-A096-D5F007B12732}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Json", "neo\src\Neo.Json\Neo.Json.csproj", "{35A34EBD-F2BF-4D83-A096-D5F007B12732}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.VM", "neo\src\Neo.VM\Neo.VM.csproj", "{D6D53889-5A10-46A4-BA66-E78B56EC1881}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.VM", "neo\src\Neo.VM\Neo.VM.csproj", "{D6D53889-5A10-46A4-BA66-E78B56EC1881}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependency", "Dependency", "{49D5873D-7B38-48A5-B853-85146F032091}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.SmartContract.Testing", "src\Neo.SmartContract.Testing\Neo.SmartContract.Testing.csproj", "{648DCE6F-A0BA-4032-951B-20CF5BBFD998}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.SmartContract.Testing.UnitTests", "tests\Neo.SmartContract.Testing.UnitTests\Neo.SmartContract.Testing.UnitTests.csproj", "{B772B8A9-9362-4C6F-A6D3-2A4138439B2C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +90,14 @@ Global {D6D53889-5A10-46A4-BA66-E78B56EC1881}.Debug|Any CPU.Build.0 = Debug|Any CPU {D6D53889-5A10-46A4-BA66-E78B56EC1881}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6D53889-5A10-46A4-BA66-E78B56EC1881}.Release|Any CPU.Build.0 = Release|Any CPU + {648DCE6F-A0BA-4032-951B-20CF5BBFD998}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {648DCE6F-A0BA-4032-951B-20CF5BBFD998}.Debug|Any CPU.Build.0 = Debug|Any CPU + {648DCE6F-A0BA-4032-951B-20CF5BBFD998}.Release|Any CPU.ActiveCfg = Release|Any CPU + {648DCE6F-A0BA-4032-951B-20CF5BBFD998}.Release|Any CPU.Build.0 = Release|Any CPU + {B772B8A9-9362-4C6F-A6D3-2A4138439B2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B772B8A9-9362-4C6F-A6D3-2A4138439B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B772B8A9-9362-4C6F-A6D3-2A4138439B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B772B8A9-9362-4C6F-A6D3-2A4138439B2C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -101,8 +113,10 @@ Global {D0153204-6AEF-4D94-B0E1-8124C38C91D4} = {D5266066-0AFD-44D5-A83E-2F73668A63C8} {73223FBD-C562-4FA0-9722-C7F1C382A9DE} = {49D5873D-7B38-48A5-B853-85146F032091} {D541BCE9-65BC-475B-94E5-19B6BFFF2B8E} = {49D5873D-7B38-48A5-B853-85146F032091} - {D6D53889-5A10-46A4-BA66-E78B56EC1881} = {49D5873D-7B38-48A5-B853-85146F032091} {35A34EBD-F2BF-4D83-A096-D5F007B12732} = {49D5873D-7B38-48A5-B853-85146F032091} + {D6D53889-5A10-46A4-BA66-E78B56EC1881} = {49D5873D-7B38-48A5-B853-85146F032091} + {648DCE6F-A0BA-4032-951B-20CF5BBFD998} = {79389FC0-C621-4CEA-AD2B-6074C32E7BCA} + {B772B8A9-9362-4C6F-A6D3-2A4138439B2C} = {D5266066-0AFD-44D5-A83E-2F73668A63C8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DA935E1-C674-4364-B087-F1B511B79215} diff --git a/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj b/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj index 815ad92bc..7bfafc132 100644 --- a/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj +++ b/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj @@ -28,6 +28,7 @@ scfx + diff --git a/src/Neo.Compiler.CSharp/Options.cs b/src/Neo.Compiler.CSharp/Options.cs index 19a2c449b..a6df13210 100644 --- a/src/Neo.Compiler.CSharp/Options.cs +++ b/src/Neo.Compiler.CSharp/Options.cs @@ -22,6 +22,7 @@ public class Options public bool Checked { get; set; } public bool Debug { get; set; } public bool Assembly { get; set; } + public bool NoArtifacts { get; set; } public bool NoOptimize { get; set; } public bool NoInline { get; set; } public byte AddressVersion { get; set; } diff --git a/src/Neo.Compiler.CSharp/Program.cs b/src/Neo.Compiler.CSharp/Program.cs index dc1924bfd..0b121fe46 100644 --- a/src/Neo.Compiler.CSharp/Program.cs +++ b/src/Neo.Compiler.CSharp/Program.cs @@ -9,15 +9,19 @@ // modifications are permitted. using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; using Neo.IO; using Neo.Json; using Neo.Optimizer; using Neo.SmartContract; using Neo.SmartContract.Manifest; +using Neo.SmartContract.Testing.Extensions; using System; using System.CommandLine; using System.CommandLine.Invocation; using System.CommandLine.NamingConventionBinder; +using System.ComponentModel; using System.IO; using System.IO.Compression; using System.Linq; @@ -38,6 +42,7 @@ static int Main(string[] args) new Option("--checked", "Indicates whether to check for overflow and underflow."), new Option(new[] { "-d", "--debug" }, "Indicates whether to generate debugging information."), new Option("--assembly", "Indicates whether to generate assembly."), + new Option("--no-artifacts", "Instruct the compiler not to generate artifacts."), new Option("--no-optimize", "Instruct the compiler not to optimize the code."), new Option("--no-inline", "Instruct the compiler not to insert inline code."), new Option("--address-version", () => ProtocolSettings.Default.AddressVersion, "Indicates the address version used by the compiler.") @@ -176,6 +181,64 @@ private static int ProcessOutputs(Options options, string folder, CompilationCon return 1; } Console.WriteLine($"Created {path}"); + if (!options.NoArtifacts) + { + var artifact = manifest.Abi.GetArtifactsSource(baseName); + path = Path.Combine(outputFolder, $"{baseName}.artifacts.cs"); + File.WriteAllText(path, artifact); + Console.WriteLine($"Created {path}"); + + try + { + // Try to compile the artifacts into a dll + + string coreDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + + var syntaxTree = CSharpSyntaxTree.ParseText(artifact); + var references = new MetadataReference[] + { + MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.dll")), + MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.InteropServices.dll")), + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DisplayNameAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Numerics.BigInteger).Assembly.Location), + MetadataReference.CreateFromFile(typeof(UInt160).Assembly.Location), + MetadataReference.CreateFromFile(typeof(SmartContract.Testing.SmartContract).Assembly.Location) + }; + + var compilation = CSharpCompilation.Create(baseName, new[] { syntaxTree }, references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using var ms = new MemoryStream(); + EmitResult result = compilation.Emit(ms); + + if (!result.Success) + { + var failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == DiagnosticSeverity.Error); + + foreach (var diagnostic in failures) + { + Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage()); + } + } + else + { + ms.Seek(0, SeekOrigin.Begin); + + // Write dll + + path = Path.Combine(outputFolder, $"{baseName}.artifacts.dll"); + File.WriteAllBytes(path, ms.ToArray()); + Console.WriteLine($"Created {path}"); + } + } + catch + { + Console.Error.WriteLine("Artifacts compilation error."); + } + } if (options.Debug) { path = Path.Combine(outputFolder, $"{baseName}.nefdbgnfo"); diff --git a/src/Neo.SmartContract.Testing/CustomMock.cs b/src/Neo.SmartContract.Testing/CustomMock.cs new file mode 100644 index 000000000..903f702c1 --- /dev/null +++ b/src/Neo.SmartContract.Testing/CustomMock.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace Neo.SmartContract.Testing +{ + internal class CustomMock + { + /// + /// Mocked contract + /// + public required SmartContract Contract { get; init; } + + /// + /// Mocked method + /// + public required MethodInfo Method { get; init; } + } +} diff --git a/src/Neo.SmartContract.Testing/Extensions/ArtifactExtensions.cs b/src/Neo.SmartContract.Testing/Extensions/ArtifactExtensions.cs new file mode 100644 index 000000000..e91810bac --- /dev/null +++ b/src/Neo.SmartContract.Testing/Extensions/ArtifactExtensions.cs @@ -0,0 +1,325 @@ +using Neo.SmartContract.Manifest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Neo.SmartContract.Testing.Extensions +{ + public static class ArtifactExtensions + { + static readonly string[] _protectedWords = new string[] { + "abstract", "as", "base", "bool", "break", "byte", + "case", "catch", "char", "checked", "class", "const", + "continue", "decimal", "default", "delegate", "do", "double", + "else", "enum", "event", "explicit", "extern", "false", + "finally", "fixed", "float", "for", "foreach", "goto", + "if", "implicit", "in", "int", "interface", "internal", + "is", "lock", "long", "namespace", "new", "null", + "object", "operator", "out", "override", "params", "private", + "protected", "public", "readonly", "ref", "return", "sbyte", + "sealed", "short", "sizeof", "stackalloc", "static", "string", + "struct", "switch", "this", "throw", "true", "try", + "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", + "using", "virtual", "void", "volatile", "while" + }; + + /// + /// Get source code from contract Abi + /// + /// Abi + /// Contract name + /// Generate properties + /// Source + public static string GetArtifactsSource(this ContractAbi abi, string name, bool generateProperties = true) + { + var builder = new StringBuilder(); + using var sourceCode = new StringWriter(builder) + { + NewLine = "\n" + }; + + sourceCode.WriteLine("using Neo.Cryptography.ECC;"); + sourceCode.WriteLine("using System.Collections.Generic;"); + sourceCode.WriteLine("using System.ComponentModel;"); + sourceCode.WriteLine("using System.Numerics;"); + sourceCode.WriteLine(""); + sourceCode.WriteLine("namespace Neo.SmartContract.Testing;"); + sourceCode.WriteLine(""); + sourceCode.WriteLine($"public abstract class {name} : Neo.SmartContract.Testing.SmartContract"); + sourceCode.WriteLine("{"); + + // Crete events + + if (abi.Events.Any()) + { + sourceCode.WriteLine(" #region Events"); + + foreach (var ev in abi.Events.OrderBy(u => u.Name)) + { + sourceCode.Write(CreateSourceEventFromManifest(ev)); + } + + sourceCode.WriteLine(" #endregion"); + } + + // Create methods + + var methods = abi.Methods; + + if (generateProperties) + { + (methods, var properties) = ProcessAbiMethods(abi.Methods); + + if (properties.Any()) + { + sourceCode.WriteLine(" #region Properties"); + + foreach (var property in properties.OrderBy(u => u.getter.Name)) + { + sourceCode.Write(CreateSourcePropertyFromManifest(property.getter, property.setter)); + } + + sourceCode.WriteLine(" #endregion"); + } + } + + if (methods.Any(u => u.Safe)) + { + sourceCode.WriteLine(" #region Safe methods"); + + foreach (var method in methods.Where(u => u.Safe).OrderBy(u => u.Name)) + { + // This method can't be called, so avoid them + + if (method.Name.StartsWith("_")) continue; + + sourceCode.Write(CreateSourceMethodFromManifest(method)); + } + + sourceCode.WriteLine(" #endregion"); + } + + if (methods.Any(u => !u.Safe)) + { + sourceCode.WriteLine(" #region Unsafe methods"); + + foreach (var method in methods.Where(u => !u.Safe).OrderBy(u => u.Name)) + { + // This method can't be called, so avoid them + + if (method.Name.StartsWith("_")) continue; + + sourceCode.Write(CreateSourceMethodFromManifest(method)); + } + sourceCode.WriteLine(" #endregion"); + } + + // Create constructor + + sourceCode.WriteLine(" #region Constructor for internal use only"); + sourceCode.WriteLine($" protected {name}(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) {{ }}"); + sourceCode.WriteLine(" #endregion"); + + sourceCode.WriteLine("}"); + + return sourceCode.ToString().TrimEnd(); + } + + private static (ContractMethodDescriptor[] methods, (ContractMethodDescriptor getter, ContractMethodDescriptor? setter)[] properties) + ProcessAbiMethods(ContractMethodDescriptor[] methods) + { + List methodList = new(methods); + List<(ContractMethodDescriptor, ContractMethodDescriptor?)> properties = new(); + + // Detect and extract properties, first find Safe && 0 args && return != void + + foreach (ContractMethodDescriptor getter in methods.Where(u => u.Safe && u.Parameters.Length == 0 && u.ReturnType != ContractParameterType.Void).ToArray()) + { + // Find setter: setXXX && one arg && not safe && parameter = getter.return && return == void + + var setter = getter.Name.StartsWith("get") ? // Only find setter if start with get + methodList.FirstOrDefault( + u => + u.Name == "set" + getter.Name[3..] && + !u.Safe && + u.Parameters.Length == 1 && + u.Parameters[0].Type == getter.ReturnType && + u.ReturnType == ContractParameterType.Void + ) : null; + + properties.Add((getter, setter)); + methodList.Remove(getter); + + if (setter != null) + { + methodList.Remove(setter); + } + } + + return (methodList.ToArray(), properties.ToArray()); + } + + /// + /// Create source code from event + /// + /// Event + /// Source + private static string CreateSourceEventFromManifest(ContractEventDescriptor ev) + { + var evName = TongleLowercase(EscapeName(ev.Name)); + if (!evName.StartsWith("On")) evName = "On" + evName; + + var builder = new StringBuilder(); + using var sourceCode = new StringWriter(builder) + { + NewLine = "\n" + }; + + sourceCode.Write($" public delegate void del{ev.Name}("); + + var isFirst = true; + foreach (var arg in ev.Parameters) + { + if (!isFirst) sourceCode.Write(", "); + else isFirst = false; + + sourceCode.Write($"{TypeToSource(arg.Type)} {EscapeName(arg.Name)}"); + } + + sourceCode.WriteLine(");"); + if (ev.Name != evName) + { + sourceCode.WriteLine($" [DisplayName(\"{ev.Name}\")]"); + } + sourceCode.WriteLine($" public event del{ev.Name}? {evName};"); + + return builder.ToString(); + } + + /// + /// Create source code from manifest property + /// + /// Getter + /// Setter + /// Source + private static string CreateSourcePropertyFromManifest(ContractMethodDescriptor getter, ContractMethodDescriptor? setter) + { + var propertyName = TongleLowercase(EscapeName(getter.Name.StartsWith("get") ? getter.Name[3..] : getter.Name)); + var getset = setter is not null ? $"{{ [DisplayName(\"{getter.Name}\")] get; [DisplayName(\"{setter.Name}\")] set; }}" : $"{{ [DisplayName(\"{getter.Name}\")] get; }}"; + + var builder = new StringBuilder(); + using var sourceCode = new StringWriter(builder) + { + NewLine = "\n" + }; + sourceCode.WriteLine($" public abstract {TypeToSource(getter.ReturnType)} {propertyName} {getset}"); + + return builder.ToString(); + } + + /// + /// Create source code from manifest method + /// + /// Contract method + /// Source + private static string CreateSourceMethodFromManifest(ContractMethodDescriptor method) + { + var methodName = TongleLowercase(EscapeName(method.Name)); + + var builder = new StringBuilder(); + using var sourceCode = new StringWriter(builder) + { + NewLine = "\n" + }; + + sourceCode.WriteLine($" /// "); + sourceCode.WriteLine($" /// {(method.Safe ? "Safe method" : "Unsafe method")}"); + sourceCode.WriteLine($" /// "); + if (method.Name != methodName) + { + sourceCode.WriteLine($" [DisplayName(\"{method.Name}\")]"); + } + sourceCode.Write($" public abstract {TypeToSource(method.ReturnType)} {methodName}("); + + var isFirst = true; + for (int x = 0; x < method.Parameters.Length; x++) + { + if (!isFirst) sourceCode.Write(", "); + else isFirst = false; + + var isLast = x == method.Parameters.Length - 1; + var arg = method.Parameters[x]; + + if (isLast && arg.Type == ContractParameterType.Any) + { + // it will be object X, we can add a default value + + sourceCode.Write($"{TypeToSource(arg.Type)}? {EscapeName(arg.Name)} = null"); + } + else + { + sourceCode.Write($"{TypeToSource(arg.Type)} {EscapeName(arg.Name)}"); + } + } + + + sourceCode.WriteLine(");"); + + return builder.ToString(); + } + + private static string TongleLowercase(string value) + { + if (value.Length == 0) + { + return value; + } + + if (char.IsLower(value[0])) + { + return value[0].ToString().ToUpperInvariant() + value[1..]; + } + + return value; + } + + /// + /// Escape name + /// + /// Name + /// Escaped name + private static string EscapeName(string name) + { + if (_protectedWords.Contains(name)) + return "@" + name; + + return name; + } + + /// + /// Type to source + /// + /// Type + /// c# type + private static string TypeToSource(ContractParameterType type) + { + return type switch + { + ContractParameterType.Boolean => "bool", + ContractParameterType.Integer => "BigInteger", + ContractParameterType.String => "string", + ContractParameterType.Hash160 => "UInt160", + ContractParameterType.Hash256 => "UInt256", + ContractParameterType.PublicKey => "ECPoint", + ContractParameterType.ByteArray => "byte[]", + ContractParameterType.Signature => "byte[]", + ContractParameterType.Array => "IList", + ContractParameterType.Map => "IDictionary", + ContractParameterType.Void => "void", + _ => "object", + }; + } + } +} diff --git a/src/Neo.SmartContract.Testing/Extensions/MockExtensions.cs b/src/Neo.SmartContract.Testing/Extensions/MockExtensions.cs new file mode 100644 index 000000000..857456612 --- /dev/null +++ b/src/Neo.SmartContract.Testing/Extensions/MockExtensions.cs @@ -0,0 +1,108 @@ +using Moq; +using System; +using System.ComponentModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Neo.SmartContract.Testing.Extensions +{ + internal static class MockExtensions + { + private static readonly Type methodCallType = typeof(Mock).Assembly.GetType("Moq.MethodCall")!; + private static readonly MethodInfo isAnyMethod = typeof(It).GetMethod(nameof(It.IsAny), BindingFlags.Public | BindingFlags.Static)!; + + public static bool IsMocked(this Mock mock, MethodInfo method) + where T : SmartContract + { + var property = methodCallType.GetProperty("Method")!; + + foreach (var setup in mock.Setups) + { + if (setup.GetType() != methodCallType) continue; + + if (method.Equals(property.GetValue(setup))) + { + return true; + } + } + + return false; + } + + private static MethodCallExpression BuildIsAnyExpressions(Type type) + { + return Expression.Call(isAnyMethod.MakeGenericMethod(type)); + } + + private static Expression BuildIsAnyExpressions(Mock mock, string name, Type[] args) + where T : SmartContract + { + var mockType = mock.Object.GetType().BaseType!; + var expArgs = args.Select(BuildIsAnyExpressions).ToArray(); + + var instanceParam = Expression.Parameter(mockType, "x"); + + var metodoInfo = mockType.GetMethod(name, args)!; + var callExpression = Expression.Call(instanceParam, metodoInfo, expArgs); + var parameterExpression = Expression.Parameter(mockType, "x"); + + return Expression.Lambda(callExpression, parameterExpression); + } + + public static void MockFunction(this Mock mock, string name, Type[] args, Type returnType) + where T : SmartContract + { + Expression exp = BuildIsAnyExpressions(mock, name, args); + + var setupMethod = mock.GetType() + .GetMethods(BindingFlags.Instance | BindingFlags.Public) + .First(u => u.Name == nameof(Mock.Setup) && + u.GetParameters().Length == 1 && + u.GetParameters()[0].ParameterType.ToString().Contains("[System.Func`") + ) + .MakeGenericMethod(returnType); + + var setup = setupMethod.Invoke(mock, new object[] { exp })!; + + var retMethod = setup.GetType() + .GetMethod("Returns", new Type[] { typeof(InvocationFunc) })!; + + _ = retMethod.Invoke(setup, new object[] { new InvocationFunc(invocation => + { + var display = invocation.Method.GetCustomAttribute(); + var name = display is not null ? display.DisplayName : invocation.Method.Name; + + return mock.Object.Invoke(name, invocation.Arguments.ToArray()).ConvertTo(returnType)!; + }) + }); + } + + public static void MockAction(this Mock mock, string name, Type[] args) + where T : SmartContract + { + Expression exp = BuildIsAnyExpressions(mock, name, args); + + var setupMethod = mock.GetType() + .GetMethods(BindingFlags.Instance | BindingFlags.Public) + .First(u => u.Name == nameof(Mock.Setup) && + u.GetParameters().Length == 1 && + u.GetParameters()[0].ParameterType.ToString().Contains("[System.Action`") + ); + + var setup = setupMethod.Invoke(mock, new object[] { exp })!; + + var retMethod = setup.GetType() + .GetMethod("Callback", new Type[] { typeof(InvocationAction) })!; + + _ = retMethod.Invoke(setup, new object[] { new InvocationAction(invocation => + { + var display = invocation.Method.GetCustomAttribute(); + var name = display is not null ? display.DisplayName : invocation.Method.Name; + + mock.Object.Invoke(name, invocation.Arguments.ToArray()); + }) + }); + } + } +} diff --git a/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs b/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs new file mode 100644 index 000000000..379927630 --- /dev/null +++ b/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs @@ -0,0 +1,108 @@ +using Neo.IO; +using Neo.VM.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Reflection; + +namespace Neo.SmartContract.Testing.Extensions +{ + public static class TestExtensions + { + /// + /// Convert dotnet type to stack item + /// + /// Data + /// StackItem + public static StackItem ConvertToStackItem(this object? data) + { + return data switch + { + null => StackItem.Null, + bool b => (VM.Types.Boolean)b, + string s => (ByteString)s, + byte[] d => (ByteString)d, + ReadOnlyMemory r => (ByteString)r, + byte by => (Integer)by, + sbyte sby => (Integer)sby, + short i16 => (Integer)i16, + ushort ui16 => (Integer)ui16, + int i32 => (Integer)i32, + uint ui32 => (Integer)ui32, + long i64 => (Integer)i64, + ulong ui64 => (Integer)ui64, + BigInteger bi => (Integer)bi, + UInt160 u160 => (ByteString)u160.ToArray(), + UInt256 u256 => (ByteString)u256.ToArray(), + Cryptography.ECC.ECPoint ec => (ByteString)ec.ToArray(), + object[] arr => new VM.Types.Array(arr.Select(ConvertToStackItem)), + IEnumerable iarr => new VM.Types.Array(iarr.Select(ConvertToStackItem)), + _ => StackItem.Null, + }; + } + + /// + /// Convert Array stack item to dotnet array + /// + /// Item + /// Parameters + /// Object + public static object?[]? ConvertTo(this VM.Types.Array state, ParameterInfo[] parameters) + { + if (parameters.Length > 0) + { + object?[] args = new object[parameters.Length]; + + for (int x = 0; x < parameters.Length; x++) + { + args[x] = state[x].ConvertTo(parameters[x].ParameterType); + } + + return args; + } + + return null; + } + + /// + /// Convert stack item to dotnet + /// + /// Item + /// Type + /// Object + public static object? ConvertTo(this StackItem stackItem, Type type) + { + if (stackItem is null || stackItem.IsNull) return null; + + return type switch + { + _ when type == typeof(bool) => stackItem.GetBoolean(), + _ when type == typeof(string) => Utility.StrictUTF8.GetString(stackItem.GetSpan()), + _ when type == typeof(byte[]) => stackItem.GetSpan().ToArray(), + _ when type == typeof(byte) => (byte)stackItem.GetInteger(), + _ when type == typeof(sbyte) => (sbyte)stackItem.GetInteger(), + _ when type == typeof(short) => (short)stackItem.GetInteger(), + _ when type == typeof(ushort) => (ushort)stackItem.GetInteger(), + _ when type == typeof(int) => (int)stackItem.GetInteger(), + _ when type == typeof(uint) => (uint)stackItem.GetInteger(), + _ when type == typeof(long) => (long)stackItem.GetInteger(), + _ when type == typeof(ulong) => (ulong)stackItem.GetInteger(), + _ when type == typeof(BigInteger) => stackItem.GetInteger(), + _ when type == typeof(UInt160) => new UInt160(stackItem.GetSpan().ToArray()), + _ when type == typeof(UInt256) => new UInt256(stackItem.GetSpan().ToArray()), + _ when type == typeof(Cryptography.ECC.ECPoint) => Cryptography.ECC.ECPoint.FromBytes(stackItem.GetSpan().ToArray(), Cryptography.ECC.ECCurve.Secp256r1), + _ when type == typeof(List) && stackItem is CompoundType cp => new List(cp.SubItems), // SubItems in StackItem type + _ when typeof(IInteroperable).IsAssignableFrom(type) => CreateInteroperable(stackItem, type), + _ => throw new FormatException($"Impossible to convert {stackItem} to {type}"), + }; + } + + private static IInteroperable CreateInteroperable(StackItem stackItem, Type type) + { + var interoperable = (IInteroperable)Activator.CreateInstance(type)!; + interoperable.FromStackItem(stackItem); + return interoperable; + } + } +} diff --git a/src/Neo.SmartContract.Testing/Native/ContractManagement.cs b/src/Neo.SmartContract.Testing/Native/ContractManagement.cs new file mode 100644 index 000000000..b348e26d1 --- /dev/null +++ b/src/Neo.SmartContract.Testing/Native/ContractManagement.cs @@ -0,0 +1,72 @@ +using Neo.Cryptography.ECC; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class ContractManagement : Neo.SmartContract.Testing.SmartContract +{ + #region Events + public delegate void delDeploy(UInt160 Hash); + [DisplayName("Deploy")] + public event delDeploy? OnDeploy; + public delegate void delDestroy(UInt160 Hash); + [DisplayName("Destroy")] + public event delDestroy? OnDestroy; + public delegate void delUpdate(UInt160 Hash); + [DisplayName("Update")] + public event delUpdate? OnUpdate; + #endregion + #region Properties + public abstract object ContractHashes { [DisplayName("getContractHashes")] get; } + public abstract BigInteger MinimumDeploymentFee { [DisplayName("getMinimumDeploymentFee")] get; [DisplayName("setMinimumDeploymentFee")] set; } + #endregion + #region Safe methods + /// + /// Safe method + /// + [DisplayName("getContract")] + public abstract ContractState GetContract(UInt160 hash); + /// + /// Safe method + /// + [DisplayName("getContractById")] + public abstract ContractState GetContractById(BigInteger id); + /// + /// Safe method + /// + [DisplayName("hasMethod")] + public abstract bool HasMethod(UInt160 hash, string method, BigInteger pcount); + #endregion + #region Unsafe methods + /// + /// Unsafe method + /// + [DisplayName("deploy")] + public abstract ContractState Deploy(byte[] nefFile, byte[] manifest); + /// + /// Unsafe method + /// + [DisplayName("deploy")] + public abstract ContractState Deploy(byte[] nefFile, byte[] manifest, object? data = null); + /// + /// Unsafe method + /// + [DisplayName("destroy")] + public abstract void Destroy(); + /// + /// Unsafe method + /// + [DisplayName("update")] + public abstract void Update(byte[] nefFile, byte[] manifest); + /// + /// Unsafe method + /// + [DisplayName("update")] + public abstract void Update(byte[] nefFile, byte[] manifest, object? data = null); + #endregion + #region Constructor for internal use only + protected ContractManagement(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion +} diff --git a/src/Neo.SmartContract.Testing/Native/CryptoLib.cs b/src/Neo.SmartContract.Testing/Native/CryptoLib.cs new file mode 100644 index 000000000..a6498111e --- /dev/null +++ b/src/Neo.SmartContract.Testing/Native/CryptoLib.cs @@ -0,0 +1,65 @@ +using Neo.Cryptography.ECC; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class CryptoLib : Neo.SmartContract.Testing.SmartContract +{ + #region Safe methods + /// + /// Safe method + /// + [DisplayName("bls12381Add")] + public abstract object Bls12381Add(object x, object y); + /// + /// Safe method + /// + [DisplayName("bls12381Deserialize")] + public abstract object Bls12381Deserialize(byte[] data); + /// + /// Safe method + /// + [DisplayName("bls12381Equal")] + public abstract bool Bls12381Equal(object x, object y); + /// + /// Safe method + /// + [DisplayName("bls12381Mul")] + public abstract object Bls12381Mul(object x, byte[] mul, bool neg); + /// + /// Safe method + /// + [DisplayName("bls12381Pairing")] + public abstract object Bls12381Pairing(object g1, object g2); + /// + /// Safe method + /// + [DisplayName("bls12381Serialize")] + public abstract byte[] Bls12381Serialize(object g); + /// + /// Safe method + /// + [DisplayName("murmur32")] + public abstract byte[] Murmur32(byte[] data, BigInteger seed); + /// + /// Safe method + /// + [DisplayName("ripemd160")] + public abstract byte[] Ripemd160(byte[] data); + /// + /// Safe method + /// + [DisplayName("sha256")] + public abstract byte[] Sha256(byte[] data); + /// + /// Safe method + /// + [DisplayName("verifyWithECDsa")] + public abstract bool VerifyWithECDsa(byte[] message, byte[] pubkey, byte[] signature, BigInteger curve); + #endregion + #region Constructor for internal use only + protected CryptoLib(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion +} diff --git a/src/Neo.SmartContract.Testing/Native/GasToken.cs b/src/Neo.SmartContract.Testing/Native/GasToken.cs new file mode 100644 index 000000000..9816da823 --- /dev/null +++ b/src/Neo.SmartContract.Testing/Native/GasToken.cs @@ -0,0 +1,37 @@ +using Neo.Cryptography.ECC; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class GasToken : Neo.SmartContract.Testing.SmartContract +{ + #region Events + public delegate void delTransfer(UInt160 from, UInt160 to, BigInteger amount); + [DisplayName("Transfer")] + public event delTransfer? OnTransfer; + #endregion + #region Properties + public abstract BigInteger Decimals { [DisplayName("decimals")] get; } + public abstract string Symbol { [DisplayName("symbol")] get; } + public abstract BigInteger TotalSupply { [DisplayName("totalSupply")] get; } + #endregion + #region Safe methods + /// + /// Safe method + /// + [DisplayName("balanceOf")] + public abstract BigInteger BalanceOf(UInt160 account); + #endregion + #region Unsafe methods + /// + /// Unsafe method + /// + [DisplayName("transfer")] + public abstract bool Transfer(UInt160 from, UInt160 to, BigInteger amount, object? data = null); + #endregion + #region Constructor for internal use only + protected GasToken(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion +} diff --git a/src/Neo.SmartContract.Testing/Native/LedgerContract.cs b/src/Neo.SmartContract.Testing/Native/LedgerContract.cs new file mode 100644 index 000000000..ac6c663b1 --- /dev/null +++ b/src/Neo.SmartContract.Testing/Native/LedgerContract.cs @@ -0,0 +1,49 @@ +using Neo.Cryptography.ECC; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class LedgerContract : Neo.SmartContract.Testing.SmartContract +{ + #region Properties + public abstract UInt256 CurrentHash { [DisplayName("currentHash")] get; } + public abstract BigInteger CurrentIndex { [DisplayName("currentIndex")] get; } + #endregion + #region Safe methods + /// + /// Safe method + /// + [DisplayName("getBlock")] + public abstract IList GetBlock(byte[] indexOrHash); + /// + /// Safe method + /// + [DisplayName("getTransaction")] + public abstract IList GetTransaction(UInt256 hash); + /// + /// Safe method + /// + [DisplayName("getTransactionFromBlock")] + public abstract IList GetTransactionFromBlock(byte[] blockIndexOrHash, BigInteger txIndex); + /// + /// Safe method + /// + [DisplayName("getTransactionHeight")] + public abstract BigInteger GetTransactionHeight(UInt256 hash); + /// + /// Safe method + /// + [DisplayName("getTransactionSigners")] + public abstract IList GetTransactionSigners(UInt256 hash); + /// + /// Safe method + /// + [DisplayName("getTransactionVMState")] + public abstract BigInteger GetTransactionVMState(UInt256 hash); + #endregion + #region Constructor for internal use only + protected LedgerContract(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion +} diff --git a/src/Neo.SmartContract.Testing/Native/NeoToken.cs b/src/Neo.SmartContract.Testing/Native/NeoToken.cs new file mode 100644 index 000000000..79e5599e4 --- /dev/null +++ b/src/Neo.SmartContract.Testing/Native/NeoToken.cs @@ -0,0 +1,79 @@ +using Neo.Cryptography.ECC; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class NeoToken : Neo.SmartContract.Testing.SmartContract +{ + #region Events + public delegate void delCandidateStateChanged(ECPoint pubkey, bool registered, BigInteger votes); + [DisplayName("CandidateStateChanged")] + public event delCandidateStateChanged? OnCandidateStateChanged; + public delegate void delTransfer(UInt160 from, UInt160 to, BigInteger amount); + [DisplayName("Transfer")] + public event delTransfer? OnTransfer; + public delegate void delVote(UInt160 account, ECPoint from, ECPoint to, BigInteger amount); + [DisplayName("Vote")] + public event delVote? OnVote; + #endregion + #region Properties + public abstract BigInteger Decimals { [DisplayName("decimals")] get; } + public abstract object AllCandidates { [DisplayName("getAllCandidates")] get; } + public abstract IList Candidates { [DisplayName("getCandidates")] get; } + public abstract IList Committee { [DisplayName("getCommittee")] get; } + public abstract BigInteger GasPerBlock { [DisplayName("getGasPerBlock")] get; [DisplayName("setGasPerBlock")] set; } + public abstract IList NextBlockValidators { [DisplayName("getNextBlockValidators")] get; } + public abstract BigInteger RegisterPrice { [DisplayName("getRegisterPrice")] get; [DisplayName("setRegisterPrice")] set; } + public abstract string Symbol { [DisplayName("symbol")] get; } + public abstract BigInteger TotalSupply { [DisplayName("totalSupply")] get; } + #endregion + #region Safe methods + /// + /// Safe method + /// + [DisplayName("balanceOf")] + public abstract BigInteger BalanceOf(UInt160 account); + /// + /// Safe method + /// + [DisplayName("getAccountState")] + public abstract IList GetAccountState(UInt160 account); + /// + /// Safe method + /// + [DisplayName("getCandidateVote")] + public abstract BigInteger GetCandidateVote(ECPoint pubKey); + /// + /// Safe method + /// + [DisplayName("unclaimedGas")] + public abstract BigInteger UnclaimedGas(UInt160 account, BigInteger end); + #endregion + #region Unsafe methods + /// + /// Unsafe method + /// + [DisplayName("registerCandidate")] + public abstract bool RegisterCandidate(ECPoint pubkey); + /// + /// Unsafe method + /// + [DisplayName("transfer")] + public abstract bool Transfer(UInt160 from, UInt160 to, BigInteger amount, object? data = null); + /// + /// Unsafe method + /// + [DisplayName("unregisterCandidate")] + public abstract bool UnregisterCandidate(ECPoint pubkey); + /// + /// Unsafe method + /// + [DisplayName("vote")] + public abstract bool Vote(UInt160 account, ECPoint voteTo); + #endregion + #region Constructor for internal use only + protected NeoToken(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion +} diff --git a/src/Neo.SmartContract.Testing/Native/OracleContract.cs b/src/Neo.SmartContract.Testing/Native/OracleContract.cs new file mode 100644 index 000000000..1964d8dfb --- /dev/null +++ b/src/Neo.SmartContract.Testing/Native/OracleContract.cs @@ -0,0 +1,37 @@ +using Neo.Cryptography.ECC; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class OracleContract : Neo.SmartContract.Testing.SmartContract +{ + #region Events + public delegate void delOracleRequest(BigInteger Id, UInt160 RequestContract, string Url, string Filter); + [DisplayName("OracleRequest")] + public event delOracleRequest? OnOracleRequest; + public delegate void delOracleResponse(BigInteger Id, UInt256 OriginalTx); + [DisplayName("OracleResponse")] + public event delOracleResponse? OnOracleResponse; + #endregion + #region Properties + public abstract BigInteger Price { [DisplayName("getPrice")] get; [DisplayName("setPrice")] set; } + public abstract bool Verify { [DisplayName("verify")] get; } + #endregion + #region Unsafe methods + /// + /// Unsafe method + /// + [DisplayName("finish")] + public abstract void Finish(); + /// + /// Unsafe method + /// + [DisplayName("request")] + public abstract void Request(string url, string filter, string callback, object userData, BigInteger gasForResponse); + #endregion + #region Constructor for internal use only + protected OracleContract(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion +} diff --git a/src/Neo.SmartContract.Testing/Native/PolicyContract.cs b/src/Neo.SmartContract.Testing/Native/PolicyContract.cs new file mode 100644 index 000000000..6be01a1b8 --- /dev/null +++ b/src/Neo.SmartContract.Testing/Native/PolicyContract.cs @@ -0,0 +1,47 @@ +using Neo.Cryptography.ECC; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class PolicyContract : Neo.SmartContract.Testing.SmartContract +{ + #region Properties + public abstract BigInteger ExecFeeFactor { [DisplayName("getExecFeeFactor")] get; [DisplayName("setExecFeeFactor")] set; } + public abstract BigInteger FeePerByte { [DisplayName("getFeePerByte")] get; [DisplayName("setFeePerByte")] set; } + public abstract BigInteger StoragePrice { [DisplayName("getStoragePrice")] get; [DisplayName("setStoragePrice")] set; } + #endregion + #region Safe methods + /// + /// Safe method + /// + [DisplayName("getAttributeFee")] + public abstract BigInteger GetAttributeFee(BigInteger attributeType); + /// + /// Safe method + /// + [DisplayName("isBlocked")] + public abstract bool IsBlocked(UInt160 account); + #endregion + #region Unsafe methods + /// + /// Unsafe method + /// + [DisplayName("blockAccount")] + public abstract bool BlockAccount(UInt160 account); + /// + /// Unsafe method + /// + [DisplayName("setAttributeFee")] + public abstract void SetAttributeFee(BigInteger attributeType, BigInteger value); + /// + /// Unsafe method + /// + [DisplayName("unblockAccount")] + public abstract bool UnblockAccount(UInt160 account); + #endregion + #region Constructor for internal use only + protected PolicyContract(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion +} diff --git a/src/Neo.SmartContract.Testing/Native/RoleManagement.cs b/src/Neo.SmartContract.Testing/Native/RoleManagement.cs new file mode 100644 index 000000000..1bb7aa07d --- /dev/null +++ b/src/Neo.SmartContract.Testing/Native/RoleManagement.cs @@ -0,0 +1,32 @@ +using Neo.Cryptography.ECC; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class RoleManagement : Neo.SmartContract.Testing.SmartContract +{ + #region Events + public delegate void delDesignation(BigInteger Role, BigInteger BlockIndex); + [DisplayName("Designation")] + public event delDesignation? OnDesignation; + #endregion + #region Safe methods + /// + /// Safe method + /// + [DisplayName("getDesignatedByRole")] + public abstract IList GetDesignatedByRole(BigInteger role, BigInteger index); + #endregion + #region Unsafe methods + /// + /// Unsafe method + /// + [DisplayName("designateAsRole")] + public abstract void DesignateAsRole(BigInteger role, IList nodes); + #endregion + #region Constructor for internal use only + protected RoleManagement(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion +} diff --git a/src/Neo.SmartContract.Testing/Native/StdLib.cs b/src/Neo.SmartContract.Testing/Native/StdLib.cs new file mode 100644 index 000000000..04fe6f613 --- /dev/null +++ b/src/Neo.SmartContract.Testing/Native/StdLib.cs @@ -0,0 +1,120 @@ +using Neo.Cryptography.ECC; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class StdLib : Neo.SmartContract.Testing.SmartContract +{ + #region Safe methods + /// + /// Safe method + /// + [DisplayName("atoi")] + public abstract BigInteger Atoi(string value); + /// + /// Safe method + /// + [DisplayName("atoi")] + public abstract BigInteger Atoi(string value, BigInteger @base); + /// + /// Safe method + /// + [DisplayName("base58CheckDecode")] + public abstract byte[] Base58CheckDecode(string s); + /// + /// Safe method + /// + [DisplayName("base58CheckEncode")] + public abstract string Base58CheckEncode(byte[] data); + /// + /// Safe method + /// + [DisplayName("base58Decode")] + public abstract byte[] Base58Decode(string s); + /// + /// Safe method + /// + [DisplayName("base58Encode")] + public abstract string Base58Encode(byte[] data); + /// + /// Safe method + /// + [DisplayName("base64Decode")] + public abstract byte[] Base64Decode(string s); + /// + /// Safe method + /// + [DisplayName("base64Encode")] + public abstract string Base64Encode(byte[] data); + /// + /// Safe method + /// + [DisplayName("deserialize")] + public abstract object Deserialize(byte[] data); + /// + /// Safe method + /// + [DisplayName("itoa")] + public abstract string Itoa(BigInteger value); + /// + /// Safe method + /// + [DisplayName("itoa")] + public abstract string Itoa(BigInteger value, BigInteger @base); + /// + /// Safe method + /// + [DisplayName("jsonDeserialize")] + public abstract object JsonDeserialize(byte[] json); + /// + /// Safe method + /// + [DisplayName("jsonSerialize")] + public abstract byte[] JsonSerialize(object? item = null); + /// + /// Safe method + /// + [DisplayName("memoryCompare")] + public abstract BigInteger MemoryCompare(byte[] str1, byte[] str2); + /// + /// Safe method + /// + [DisplayName("memorySearch")] + public abstract BigInteger MemorySearch(byte[] mem, byte[] value); + /// + /// Safe method + /// + [DisplayName("memorySearch")] + public abstract BigInteger MemorySearch(byte[] mem, byte[] value, BigInteger start); + /// + /// Safe method + /// + [DisplayName("memorySearch")] + public abstract BigInteger MemorySearch(byte[] mem, byte[] value, BigInteger start, bool backward); + /// + /// Safe method + /// + [DisplayName("serialize")] + public abstract byte[] Serialize(object? item = null); + /// + /// Safe method + /// + [DisplayName("stringSplit")] + public abstract IList StringSplit(string str, string separator); + /// + /// Safe method + /// + [DisplayName("stringSplit")] + public abstract IList StringSplit(string str, string separator, bool removeEmptyEntries); + /// + /// Safe method + /// + [DisplayName("strLen")] + public abstract BigInteger StrLen(string str); + #endregion + #region Constructor for internal use only + protected StdLib(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion +} diff --git a/src/Neo.SmartContract.Testing/NativeArtifacts.cs b/src/Neo.SmartContract.Testing/NativeArtifacts.cs new file mode 100644 index 000000000..caa7b5f73 --- /dev/null +++ b/src/Neo.SmartContract.Testing/NativeArtifacts.cs @@ -0,0 +1,149 @@ +using Neo.Persistence; +using System; +using System.Reflection; + +namespace Neo.SmartContract.Testing +{ + /// + /// NativeArtifacts makes it easier to access native contracts + /// + public class NativeArtifacts + { + private readonly TestEngine _engine; + + /// + /// ContractManagement + /// + public ContractManagement ContractManagement { get; } + + /// + /// CryptoLib + /// + public CryptoLib CryptoLib { get; } + + /// + /// GasToken + /// + public GasToken GAS { get; } + + /// + /// NeoToken + /// + public NeoToken NEO { get; } + + /// + /// LedgerContract + /// + public LedgerContract Ledger { get; } + + /// + /// OracleContract + /// + public OracleContract Oracle { get; } + + /// + /// PolicyContract + /// + public PolicyContract Policy { get; } + + /// + /// RoleManagement + /// + public RoleManagement RoleManagement { get; } + + /// + /// OracleContract + /// + public StdLib StdLib { get; } + + /// + /// Constructor + /// + /// Engine + public NativeArtifacts(TestEngine engine) + { + _engine = engine; + + ContractManagement = _engine.FromHash(Native.NativeContract.ContractManagement.Hash, Native.NativeContract.ContractManagement.Id); + CryptoLib = _engine.FromHash(Native.NativeContract.CryptoLib.Hash, Native.NativeContract.CryptoLib.Id); + GAS = _engine.FromHash(Native.NativeContract.GAS.Hash, Native.NativeContract.GAS.Id); + NEO = _engine.FromHash(Native.NativeContract.NEO.Hash, Native.NativeContract.NEO.Id); + Ledger = _engine.FromHash(Native.NativeContract.Ledger.Hash, Native.NativeContract.Ledger.Id); + Oracle = _engine.FromHash(Native.NativeContract.Oracle.Hash, Native.NativeContract.Oracle.Id); + Policy = _engine.FromHash(Native.NativeContract.Policy.Hash, Native.NativeContract.Policy.Id); + RoleManagement = _engine.FromHash(Native.NativeContract.RoleManagement.Hash, Native.NativeContract.RoleManagement.Id); + StdLib = _engine.FromHash(Native.NativeContract.StdLib.Hash, Native.NativeContract.StdLib.Id); + } + + /// + /// Initialize native contracts + /// + /// Initialize native contracts + public void Initialize(bool commit = false) + { + _engine.Transaction.Script = Array.Empty(); // Store the script in the current transaction + + var genesis = NeoSystem.CreateGenesisBlock(_engine.ProtocolSettings); + + // Attach to static event + + ApplicationEngine.Log += _engine.ApplicationEngineLog; + ApplicationEngine.Notify += _engine.ApplicationEngineNotify; + + // Process native contracts + + foreach (var native in new Native.NativeContract[] + { + Native.NativeContract.ContractManagement, + Native.NativeContract.Ledger, + Native.NativeContract.NEO, + Native.NativeContract.GAS + } + ) + { + // Mock Native.OnPersist + + var method = native.GetType().GetMethod("OnPersist", BindingFlags.NonPublic | BindingFlags.Instance); + + DataCache clonedSnapshot = _engine.Storage.Snapshot.CreateSnapshot(); + using (var engine = new TestingApplicationEngine(_engine, TriggerType.OnPersist, genesis, clonedSnapshot, genesis)) + { + engine.LoadScript(Array.Empty()); + if (method!.Invoke(native, new object[] { engine }) is not ContractTask task) + throw new Exception($"Error casting {native.Name}.OnPersist to ContractTask"); + + task.GetAwaiter().GetResult(); + if (engine.Execute() != VM.VMState.HALT) + throw new Exception($"Error executing {native.Name}.OnPersist"); + } + + // Mock Native.PostPersist + + method = native.GetType().GetMethod("PostPersist", BindingFlags.NonPublic | BindingFlags.Instance); + + using (var engine = new TestingApplicationEngine(_engine, TriggerType.OnPersist, genesis, clonedSnapshot, genesis)) + { + engine.LoadScript(Array.Empty()); + if (method!.Invoke(native, new object[] { engine }) is not ContractTask task) + throw new Exception($"Error casting {native.Name}.PostPersist to ContractTask"); + + task.GetAwaiter().GetResult(); + if (engine.Execute() != VM.VMState.HALT) + throw new Exception($"Error executing {native.Name}.PostPersist"); + } + + clonedSnapshot.Commit(); + } + + if (commit) + { + _engine.Storage.Commit(); + } + + // Detach to static event + + ApplicationEngine.Log -= _engine.ApplicationEngineLog; + ApplicationEngine.Notify -= _engine.ApplicationEngineNotify; + } + } +} diff --git a/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj b/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj new file mode 100644 index 000000000..33e7bcd3c --- /dev/null +++ b/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj @@ -0,0 +1,23 @@ + + + + Neo.SmartContract.Testing + Neo.SmartContract.Testing + NEO;Blockchain;Smart Contract + TestEngine for NEO smart contract testing. + true + false + content + enable + $(NoWarn);NU5128 + + + + + + + + + + + diff --git a/src/Neo.SmartContract.Testing/README.md b/src/Neo.SmartContract.Testing/README.md new file mode 100644 index 000000000..7cd03e004 --- /dev/null +++ b/src/Neo.SmartContract.Testing/README.md @@ -0,0 +1,290 @@ +# Neo.SmartContract.Testing + +The **Neo.SmartContract.Testing** project is designed to facilitate the development of unit tests for smart contract developers in neo, it does not require the project to be done in C#, as it is possible to export artifacts from an `Abi`. + +## Table of Contents + +- [Installation and configuration](#installation-and-configuration) + - [Generating Artifacts](#generating-artifacts) + - [Example of use](#example-of-use) +- [TestEngine](#testengine) + - [Properties](#properties) + - [Methods](#methods) + - [Example of use](#example-of-use) +- [NativeArtifacts](#nativeartifacts) + - [Methods](#methods) + - [Example of use](#example-of-use) +- [SmartContractStorage](#smartcontractstorage) + - [Methods](#methods) + - [Example of use](#example-of-use) +- [Custom mocks](#custom-mocks) + - [Example of use](#example-of-use) +- [Forging signatures](#forging-signatures) + - [Example of use](#example-of-use) +- [Event testing](#event-testing) + - [Example of use](#example-of-use) +- [Known limitations](#known-limitations) + +### Installation and configuration + +#### Generating Artifacts + +The process of generating the artifacts, or the source code necessary to interact with the contract, is extremely simple. There are two main ways to do it: + +1. Using the `ABI` of a contract, the necessary source code to interact with the contract can be generated by calling the `GetArtifactsSource` method available in the `Neo.SmartContract.Testing.Extensions` namespace, we will only have to specify the name of our resulting class, which will usually be the same as the one existing in the `Name` field of the manifest. + +2. Through the Neo C# compiler, automatically when compiling a contract in C#, the necessary source code to interact with the contract is generated. This is available in the same path as the generated .nef file, and its extension are `.artifacts.cs` and `.artifacts.dll`. + +##### Example of use + +```csharp +[TestClass] +public class MyUnitTestClass +{ + [TestMethod] + public void GenerateNativeArtifacts() + { + foreach (var n in Native.NativeContract.Contracts) + { + var manifest = n.Manifest; + var source = manifest.Abi.GetArtifactsSource(manifest.Name); + + File.WriteAllText($"{manifest.Name}.cs", source); + } + } +} +``` + +### TestEngine + +The `TestEngine` class is the main class of the library, providing a simple and intuitive interface for testing smart contracts. + +#### Properties + +The publicly exposed read-only properties are as follows: + +- **ProtocolSettings**: Assigned during the construction of the `TestEngine` and defines the configuration values of the test environment. It defaults to the current blockchain protocol. +- **Sender**: Returns the script hash of the transaction sender, which corresponds to the first `Signer` defined in the `Transaction` object. +- **Native**: Allows access to the native contracts, and their state. It facilitates access to the chain's native contracts through some precompiled artifacts. This point is further detailed in [NativeArtifacts](#nativeartifacts). +- **ValidatorsAddress**: Defines the address for the validators of the defined *ProtocolSettings*. +- **CommitteeAddress**: Returns the address of the current chain's committee. +- **Transaction**: Defines the transaction that will be used as `ScriptContainer` for the neo virtual machine, by default it updates the script of the same as calls are composed and executed, and the `Signers` will be used as validators for the `CheckWitness`, regardless of whether the signature is correct or not, so if you want to test with different wallets or scopes, you do not need to sign the transaction correctly, just set the desired signers. +- **CurrentBlock**: Defaults to `Genesis` for the defined `ProtocolSettings`, but the height has been incremented by 1 to avoid issues related to the generation of gas from native contracts. + +For initialize, we have: + +- **Storage**: Abstracts access to storage, allowing for easy `Snapshots` as well as reverting them. It can only be set during the initialization of the class, and allows access to the storage of contracts, as well as manually altering their state. + +And for read and write, we have: + +- **Gas**: Sets the gas execution limit for contract calls. Sets the `NetworkFee` of the `Transaction` object. + +#### Methods + +It has four methods: + +- **Execute(script)**: Executes a script on the neo virtual machine and returns the execution result. +- **Deploy(nef, manifest, data, customMock)**: Deploys the smart contract by calling the native method `ContractManagement.deploy`. It allows setting [custom mocks](#custom-mocks), which will be detailed later. And returns the instance of the contract that has been deployed. +- **FromHash(hash, customMocks, checkExistence)**: Creates an instance without needing a `NefFile` or `Manifest`, only requiring the contract's hash. It does not consider whether the contract exists on the chain unless `checkExistence` is set to `true`. +- **SetTransactionSigners(signers)**: Set the `Signer` of the `Transaction`. +- **GetNewSigner(scope)**: A static method that provides us with a random `Signer` signed by default by `CalledByEntry`. + +#### Example of use + +```csharp +// Create the engine initializing the native contracts + +var engine = new TestEngine(true); + +// Instantiate neo contract from native hash, (not necessary if we use engine.Native.NEO) + +var neo = engine.FromHash(engine.Native.NEO.Hash, false); + +// Ensure that the main address contains the totalSupply + +Assert.AreEqual(100_000_000, neo.TotalSupply); +Assert.AreEqual(neo.TotalSupply, neo.BalanceOf(engine.ValidatorsAddress)); +``` + +### NativeArtifacts + +This class provides precompiled artifacts for neo's native contracts, thereby simplifying and facilitating calls to native contracts. + +#### Methods + +It has only one method: + +- **Initialize(bool commit = false)**: Initializes the native contract with the necessary parameters for its operation. It's important to note that this step must usually be performed, or deploying contracts won't be possible. However, if using a `Storage` that already contains chain data and these contracts have been initialized, calling this method should be avoided. The `commit` argument determines whether to commit to the active `Snapshot` of the `TestStorage` (default is `false`). + +#### Example of use + +```csharp +// Create the engine initializing the native contracts + +var engine = new TestEngine(true); + +// Ensure that the main address contains the totalSupply + +Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); +Assert.AreEqual(engine.Native.NEO.TotalSupply, engine.Native.NEO.BalanceOf(engine.ValidatorsAddress)); +``` + +### SmartContractStorage + +Avoids dealing with prefixes foreign to the internal behavior of the storage, focusing the developer solely on accessing the storage of the contract, just as it is managed by the smart contract itself, allowing reading, injecting, and deleting entries of the contract in question. + +#### Methods + +Mainly exposes the methods `Export`, `Import`, `Contains`, `Get`, `Put`, and `Remove`, all of them responsible for reading and manipulating the contract's information. + +#### Example of use + +```csharp +// Defines the prefix used to store the registration price in neo + +byte[] registerPricePrefix = new byte[] { 13 }; + +// Create engine and initialize native contracts + +TestEngine engine = new(true); + +// Check previous data + +Assert.AreEqual(100000000000, engine.Native.NEO.RegisterPrice); + +// Alter data + +engine.Native.NEO.Storage.Put(registerPricePrefix, BigInteger.MinusOne); + +// Check altered data + +Assert.AreEqual(BigInteger.MinusOne, engine.Native.NEO.RegisterPrice); +``` + +### Custom mocks + +Custom mocks allow redirecting certain calls to smart contracts so that instead of calling the underlying contract, the logic is redirected to a method in .NET, allowing the developer to test in complex environments without significant issues. + +Imagine that our project checks that our account has a balance of 123 NEO. It would be enough to redirect the calls to the NEO `balanceOf` method in the following way, so that it always returns 123. + +It's important to note that all syscalls going to this contract will also be redirected, not only the calls to the method in .NET. + + +#### Example of use + +```csharp +// Initialize TestEngine and native smart contracts + +TestEngine engine = new(true); + +// Get neo token smart contract and mock balanceOf to always return 123 + +var neo = engine.FromHash(engine.Native.NEO.Hash, + mock => mock.Setup(o => o.BalanceOf(It.IsAny())).Returns(123), + false); + +// Test direct call + +Assert.AreEqual(123, neo.BalanceOf(engine.ValidatorsAddress)); + +// Test vm call + +using (ScriptBuilder script = new()) +{ + script.EmitDynamicCall(neo.Hash, nameof(neo.BalanceOf), engine.ValidatorsAddress); + + Assert.AreEqual(123, engine.Execute(script.ToArray()).GetInteger()); +} +``` + +### Forging signatures + +To fake signatures and allow testing our contracts in authorized and unauthorized environments, it's enough to replace the signers of the `Transaction` object in our `TestEngine`. This way, we can simulate the signatures of other users. It's worth noting that it's not necessary to modify the `Witnesses` since it's not checked whether the transaction is well-formed. + +#### Example of use + +```csharp +// Initialize out TestEngine + +var engine = new TestEngine(true); + +// Check initial value of getRegisterPrice + +Assert.AreEqual(100000000000, engine.Native.NEO.RegisterPrice); + +// Fake Committee Signature + +engine.SetTransactionSigners(new Network.P2P.Payloads.Signer() +{ + Account = engine.CommitteeAddress, + Scopes = Network.P2P.Payloads.WitnessScope.Global +}); + +// Change RegisterPrice to 123 + +engine.Native.NEO.RegisterPrice = 123; + +Assert.AreEqual(123, engine.Native.NEO.RegisterPrice); + +// Now test it without this signature + +engine.SetTransactionSigners(TestEngine.GetNewSigner()); + +Assert.ThrowsException(() => engine.Native.NEO.RegisterPrice = 123); +``` + +### Event testing + +Testing that our events have been triggered has never been so easy. Simply when a contract notification is launched, the corresponding event will be invoked, making it easier to capture and detect. + +#### Example of use + +```csharp +// Create and initialize TestEngine + +var engine = new TestEngine(true); + +// Fake signature of ValidatorsAddress + +engine.SetTransactionSigners(new Network.P2P.Payloads.Signer() +{ + Account = engine.ValidatorsAddress, + Scopes = Network.P2P.Payloads.WitnessScope.Global +}); + +// Define address to transfer funds + +UInt160 addressTo = UInt160.Parse("0x1230000000000000000000000000000000000000"); + +// Attach to Transfer event + +var raisedEvent = false; + +engine.Native.NEO.OnTransfer += (UInt160 from, UInt160 to, BigInteger amount) => +{ + Assert.AreEqual(engine.Transaction.Sender, from); + Assert.AreEqual(addressTo, to); + Assert.AreEqual(123, amount); + + // If the event is raised, the variable will be changed + raisedEvent = true; +}; + + +Assert.AreEqual(0, engine.Native.NEO.BalanceOf(addressTo)); + +// Transfer funds + +Assert.IsTrue(engine.Native.NEO.Transfer(engine.Transaction.Sender, addressTo, 123, null)); + +// Ensure that we have balance and the event was raised + +Assert.IsTrue(raisedEvent); +Assert.AreEqual(123, engine.Native.NEO.BalanceOf(addressTo)); +``` + +### Known limitations + +The currently known limitations are: + +- Receive events during the deploy, because the object is returned after performing the deploy, it is not possible to intercept notifications for the deploy unless the contract is previously created with `FromHash` knowing the hash of the contract to be created. diff --git a/src/Neo.SmartContract.Testing/SmartContract.cs b/src/Neo.SmartContract.Testing/SmartContract.cs new file mode 100644 index 000000000..21fb37847 --- /dev/null +++ b/src/Neo.SmartContract.Testing/SmartContract.cs @@ -0,0 +1,112 @@ +using Neo.SmartContract.Testing.Extensions; +using Neo.VM; +using Neo.VM.Types; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace Neo.SmartContract.Testing +{ + public class SmartContract + { + internal readonly TestEngine Engine; + private readonly Type _contractType; + private readonly Dictionary _notifyCache = new(); + + public delegate void OnRuntimeLogDelegate(string message); + public event OnRuntimeLogDelegate? OnRuntimeLog; + + /// + /// Contract hash + /// + public UInt160 Hash { get; } + + /// + /// Storage for this contract + /// + public SmartContractStorage Storage { get; } + + /// + /// Constructor + /// + /// Initialize object + protected SmartContract(SmartContractInitialize initialize) + { + Engine = initialize.Engine; + Hash = initialize.Hash; + Storage = new SmartContractStorage(this, initialize.ContractId); + _contractType = GetType().BaseType ?? GetType(); // Mock + } + + /// + /// Invoke to NeoVM + /// + /// Method name + /// Arguments + /// Object + internal StackItem Invoke(string methodName, params object[] args) + { + // Compose script + + using ScriptBuilder script = new(); + script.EmitDynamicCall(Hash, methodName, args); + + // Execute + + return Engine.Execute(script.ToArray()); + } + + /// + /// Invoke OnRuntimeLog + /// + /// Message + internal void InvokeOnRuntimeLog(string message) + { + OnRuntimeLog?.Invoke(message); + } + + /// + /// Invoke on notify + /// + /// Event name + /// State + internal void InvokeOnNotify(string eventName, VM.Types.Array state) + { + if (!_notifyCache.TryGetValue(eventName, out var evField)) + { + var ev = _contractType.GetEvent(eventName); + if (ev is null) + { + ev = _contractType.GetEvents().FirstOrDefault(u => u.GetCustomAttribute()?.DisplayName == eventName); + if (ev is null) + { + _notifyCache[eventName] = null; + return; + } + } + + _notifyCache[eventName] = evField = _contractType.GetField(ev.Name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField); + } + + // Not found + if (evField is null) return; + if (evField.GetValue(this) is not Delegate del) return; + + // Avoid parse if is not needed + + var invocations = del.GetInvocationList(); + if (invocations.Length == 0) return; + + // Invoke + + var args = state.ConvertTo(del.Method.GetParameters()); + + foreach (var handler in invocations) + { + handler.Method.Invoke(handler.Target, args); + } + } + } +} diff --git a/src/Neo.SmartContract.Testing/SmartContractInitialize.cs b/src/Neo.SmartContract.Testing/SmartContractInitialize.cs new file mode 100644 index 000000000..9a7d72609 --- /dev/null +++ b/src/Neo.SmartContract.Testing/SmartContractInitialize.cs @@ -0,0 +1,20 @@ +namespace Neo.SmartContract.Testing +{ + public class SmartContractInitialize + { + /// + /// Engine + /// + public required TestEngine Engine { get; init; } + + /// + /// Hash + /// + public required UInt160 Hash { get; init; } + + /// + /// ContractId + /// + internal int? ContractId { get; init; } + } +} diff --git a/src/Neo.SmartContract.Testing/SmartContractStorage.cs b/src/Neo.SmartContract.Testing/SmartContractStorage.cs new file mode 100644 index 000000000..c23248a1f --- /dev/null +++ b/src/Neo.SmartContract.Testing/SmartContractStorage.cs @@ -0,0 +1,178 @@ +using Neo.Json; +using System; +using System.Buffers.Binary; +using System.Numerics; + +namespace Neo.SmartContract.Testing +{ + public class SmartContractStorage + { + private readonly SmartContract _smartContract; + private int? _contractId; + + /// + /// Constructor + /// + /// Smart Contract + /// Contract id, can be null + internal SmartContractStorage(SmartContract smartContract, int? contractId = null) + { + _smartContract = smartContract; + _contractId = contractId; + } + + private int GetContractId() + { + // If it was not initialized checking the contract, we need to query the contract id + _contractId ??= _smartContract.Engine.Native.ContractManagement.GetContract(_smartContract.Hash).Id; + return _contractId.Value; + } + + /// + /// Check if the entry exist + /// + /// Key + public bool Contains(ReadOnlyMemory key) + { + var skey = new StorageKey() { Id = GetContractId(), Key = key }; + var entry = _smartContract.Engine.Storage.Snapshot.TryGet(skey); + return entry != null; + } + + /// + /// Read an entry from the smart contract storage + /// + /// Key + public ReadOnlyMemory Get(ReadOnlyMemory key) + { + var skey = new StorageKey() { Id = GetContractId(), Key = key }; + var entry = _smartContract.Engine.Storage.Snapshot.TryGet(skey); + + if (entry != null) + { + return entry.Value; + } + + return null; + } + + /// + /// Put an entry in the smart contract storage + /// + /// Key + /// Value + public void Put(ReadOnlyMemory key, ReadOnlyMemory value) + { + var skey = new StorageKey() { Id = GetContractId(), Key = key }; + + var entry = _smartContract.Engine.Storage.Snapshot.GetAndChange(skey, () => new StorageItem() { Value = value }); + entry.Value = value; + } + + /// + /// Put an entry in the smart contract storage + /// + /// Key + /// Value + public void Put(ReadOnlyMemory key, BigInteger value) + { + var skey = new StorageKey() { Id = GetContractId(), Key = key }; + + var entry = _smartContract.Engine.Storage.Snapshot.GetAndChange(skey, () => new StorageItem(value)); + entry.Set(value); + } + + /// + /// Remove an entry from the smart contract storage + /// + /// Key + public void Remove(ReadOnlyMemory key) + { + var skey = new StorageKey() { Id = GetContractId(), Key = key }; + + _smartContract.Engine.Storage.Snapshot.Delete(skey); + } + + /// + /// Import data from json, expected data (in base64): + /// - "prefix" : { "key":"value" } + /// + /// Json Object + public void Import(string json) + { + if (JToken.Parse(json) is not JObject jo) + { + throw new FormatException("The json is not a valid JObject"); + } + + Import(jo); + } + + /// + /// Import data from json, expected data (in base64): + /// - "prefix" : { "key":"value" } + /// + /// Json Object + public void Import(JObject json) + { + var buffer = new byte[(sizeof(int))]; + BinaryPrimitives.WriteInt32LittleEndian(buffer, GetContractId()); + var keyId = Convert.ToBase64String(buffer); + + JObject prefix; + + // Find prefix + + if (json.ContainsProperty(keyId)) + { + if (json[keyId] is not JObject jo) + { + throw new FormatException("Invalid json"); + } + + prefix = jo; + } + else + { + return; + } + + // Read values + + foreach (var entry in prefix.Properties) + { + if (entry.Value is JString str) + { + // "key":"value" in base64 + + Put(Convert.FromBase64String(entry.Key), Convert.FromBase64String(str.Value)); + } + } + } + + /// + /// Export data to json + /// + public JObject Export() + { + var buffer = new byte[(sizeof(int))]; + BinaryPrimitives.WriteInt32LittleEndian(buffer, GetContractId()); + var keyId = Convert.ToBase64String(buffer); + + // Write prefix + + JObject ret = new(); + JObject prefix = new(); + ret[keyId] = prefix; + + foreach (var entry in _smartContract.Engine.Storage.Snapshot.Seek(Array.Empty(), Persistence.SeekDirection.Forward)) + { + // "key":"value" in base64 + + prefix[Convert.ToBase64String(entry.Key.Key.ToArray())] = Convert.ToBase64String(entry.Value.Value.ToArray()); + } + + return ret; + } + } +} diff --git a/src/Neo.SmartContract.Testing/TestEngine.cs b/src/Neo.SmartContract.Testing/TestEngine.cs new file mode 100644 index 000000000..37e807a2f --- /dev/null +++ b/src/Neo.SmartContract.Testing/TestEngine.cs @@ -0,0 +1,488 @@ +using Moq; +using Neo.Cryptography.ECC; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Testing.Extensions; +using Neo.VM; +using Neo.VM.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Neo.SmartContract.Testing +{ + public class TestEngine + { + private readonly Dictionary> _contracts = new(); + private readonly Dictionary> _customMocks = new(); + private NativeArtifacts? _native; + + /// + /// Default Protocol Settings + /// + public static readonly ProtocolSettings Default = new() + { + Network = 0x334F454Eu, + AddressVersion = ProtocolSettings.Default.AddressVersion, + StandbyCommittee = new[] + { + //Validators + ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1), + ECPoint.Parse("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", ECCurve.Secp256r1), + ECPoint.Parse("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", ECCurve.Secp256r1), + ECPoint.Parse("02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", ECCurve.Secp256r1), + ECPoint.Parse("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", ECCurve.Secp256r1), + ECPoint.Parse("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", ECCurve.Secp256r1), + ECPoint.Parse("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", ECCurve.Secp256r1), + //Other Members + ECPoint.Parse("023a36c72844610b4d34d1968662424011bf783ca9d984efa19a20babf5582f3fe", ECCurve.Secp256r1), + ECPoint.Parse("03708b860c1de5d87f5b151a12c2a99feebd2e8b315ee8e7cf8aa19692a9e18379", ECCurve.Secp256r1), + ECPoint.Parse("03c6aa6e12638b36e88adc1ccdceac4db9929575c3e03576c617c49cce7114a050", ECCurve.Secp256r1), + ECPoint.Parse("03204223f8c86b8cd5c89ef12e4f0dbb314172e9241e30c9ef2293790793537cf0", ECCurve.Secp256r1), + ECPoint.Parse("02a62c915cf19c7f19a50ec217e79fac2439bbaad658493de0c7d8ffa92ab0aa62", ECCurve.Secp256r1), + ECPoint.Parse("03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0", ECCurve.Secp256r1), + ECPoint.Parse("0288342b141c30dc8ffcde0204929bb46aed5756b41ef4a56778d15ada8f0c6654", ECCurve.Secp256r1), + ECPoint.Parse("020f2887f41474cfeb11fd262e982051c1541418137c02a0f4961af911045de639", ECCurve.Secp256r1), + ECPoint.Parse("0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30", ECCurve.Secp256r1), + ECPoint.Parse("03d281b42002647f0113f36c7b8efb30db66078dfaaa9ab3ff76d043a98d512fde", ECCurve.Secp256r1), + ECPoint.Parse("02504acbc1f4b3bdad1d86d6e1a08603771db135a73e61c9d565ae06a1938cd2ad", ECCurve.Secp256r1), + ECPoint.Parse("0226933336f1b75baa42d42b71d9091508b638046d19abd67f4e119bf64a7cfb4d", ECCurve.Secp256r1), + ECPoint.Parse("03cdcea66032b82f5c30450e381e5295cae85c5e6943af716cc6b646352a6067dc", ECCurve.Secp256r1), + ECPoint.Parse("02cd5a5547119e24feaa7c2a0f37b8c9366216bab7054de0065c9be42084003c8a", ECCurve.Secp256r1) + }, + ValidatorsCount = 7, + SeedList = System.Array.Empty(), + MillisecondsPerBlock = ProtocolSettings.Default.MillisecondsPerBlock, + MaxTransactionsPerBlock = ProtocolSettings.Default.MaxTransactionsPerBlock, + MemoryPoolMaxTransactions = ProtocolSettings.Default.MemoryPoolMaxTransactions, + MaxTraceableBlocks = ProtocolSettings.Default.MaxTraceableBlocks, + InitialGasDistribution = ProtocolSettings.Default.InitialGasDistribution, + Hardforks = ProtocolSettings.Default.Hardforks + }; + + /// + /// Storage + /// + public TestStorage Storage { get; init; } = new TestStorage(new MemoryStore()); + + /// + /// Protocol Settings + /// + public ProtocolSettings ProtocolSettings { get; } + + /// + /// Validators Address + /// + public UInt160 ValidatorsAddress + { + get + { + if (!Storage.IsInitialized) + { + // If is not initialized, return the ProtocolSettings + + var validatorsScript = Contract.CreateMultiSigRedeemScript(ProtocolSettings.StandbyValidators.Count - (ProtocolSettings.StandbyValidators.Count - 1) / 3, ProtocolSettings.StandbyValidators); + return validatorsScript.ToScriptHash(); + } + + var validators = Neo.SmartContract.Native.NativeContract.NEO.ComputeNextBlockValidators(Storage.Snapshot, ProtocolSettings); + return Contract.GetBFTAddress(validators); + } + } + + /// + /// Committee Address + /// + public UInt160 CommitteeAddress + { + get + { + if (!Storage.IsInitialized) + { + // If is not initialized, return the ProtocolSettings + + var committeeScript = Contract.CreateMultiSigRedeemScript(ProtocolSettings.StandbyCommittee.Count - (ProtocolSettings.StandbyCommittee.Count - 1) / 2, ProtocolSettings.StandbyCommittee); + return committeeScript.ToScriptHash(); + } + + return Neo.SmartContract.Native.NativeContract.NEO.GetCommitteeAddress(Storage.Snapshot); + } + } + + /// + /// BFTAddress + /// + public Block CurrentBlock { get; } + + /// + /// Transaction + /// + public Transaction Transaction { get; } + + /// + /// Gas + /// + public long Gas + { + get => Transaction.NetworkFee; + set { Transaction.NetworkFee = value; } + } + + /// + /// Sender + /// + public UInt160 Sender => Transaction.Sender; + + /// + /// Native artifacts + /// + public NativeArtifacts Native + { + get + { + _native ??= new NativeArtifacts(this); + return _native; + } + } + + /// + /// Constructor + /// + /// Initialize native contracts + public TestEngine(bool initializeNativeContracts = true) : this(Default, initializeNativeContracts) { } + + /// + /// Constructor + /// + /// Settings + /// Initialize native contracts + public TestEngine(ProtocolSettings settings, bool initializeNativeContracts = true) + { + ProtocolSettings = settings; + CurrentBlock = NeoSystem.CreateGenesisBlock(ProtocolSettings); + CurrentBlock.Header.Index++; + + var validatorsScript = Contract.CreateMultiSigRedeemScript(settings.StandbyValidators.Count - (settings.StandbyValidators.Count - 1) / 3, settings.StandbyValidators); + var committeeScript = Contract.CreateMultiSigRedeemScript(settings.StandbyCommittee.Count - (settings.StandbyCommittee.Count - 1) / 2, settings.StandbyCommittee); + + Transaction = new Transaction() + { + Attributes = System.Array.Empty(), + Script = System.Array.Empty(), + NetworkFee = ApplicationEngine.TestModeGas, + Signers = new Signer[] + { + new Signer() + { + // ValidatorsAddress + Account = validatorsScript.ToScriptHash(), + Scopes = WitnessScope.Global + }, + new Signer() + { + // CommitteeAddress + Account = committeeScript.ToScriptHash(), + Scopes = WitnessScope.Global + } + }, + Witnesses = System.Array.Empty() // Not required + }; + + if (initializeNativeContracts) + { + Native.Initialize(false); + } + } + + #region Invoke events + + internal void ApplicationEngineNotify(object? sender, NotifyEventArgs e) + { + if (_contracts.TryGetValue(e.ScriptHash, out var contracts)) + { + foreach (var contract in contracts) + { + contract.InvokeOnNotify(e.EventName, e.State); + } + } + } + + internal void ApplicationEngineLog(object? sender, LogEventArgs e) + { + if (_contracts.TryGetValue(e.ScriptHash, out var contracts)) + { + foreach (var contract in contracts) + { + contract.InvokeOnRuntimeLog(e.Message); + } + } + } + + #endregion + + /// + /// Deploy Smart contract + /// + /// Type + /// Nef file + /// Contract manifest + /// Construction data + /// Custom Mock + /// Mocked Smart Contract + public T Deploy(byte[] nef, string manifest, object? data = null, Action>? customMock = null) where T : SmartContract + { + return Deploy(nef.AsSerializable(), ContractManifest.Parse(manifest), data, customMock); + } + + /// + /// Deploy Smart contract + /// + /// Type + /// Nef file + /// Contract manifest + /// Construction data + /// Custom Mock + /// Mocked Smart Contract + public T Deploy(NefFile nef, ContractManifest manifest, object? data = null, Action>? customMock = null) where T : SmartContract + { + // Deploy + + var state = Native.ContractManagement.Deploy(nef.ToArray(), Encoding.UTF8.GetBytes(manifest.ToJson().ToString(false)), data); + + // Mock contract + + //UInt160 hash = Helper.GetContractHash(Sender, nef.CheckSum, manifest.Name); + return MockContract(state.Hash, state.Id, customMock); + } + + /// + /// Smart contract from Hash + /// + /// Type + /// Contract hash + /// Check existence (default: true) + /// Mocked Smart Contract + public T FromHash(UInt160 hash, bool checkExistence = true) where T : SmartContract + { + return FromHash(hash, null, checkExistence); + } + + /// + /// Smart contract from Hash + /// + /// Type + /// Contract hash + /// Custom Mock + /// Check existence (default: true) + /// Mocked Smart Contract + public T FromHash(UInt160 hash, Action>? customMock = null, bool checkExistence = true) where T : SmartContract + { + if (!checkExistence) + { + return MockContract(hash, null, customMock); + } + + var state = Native.ContractManagement.GetContract(hash); + + return MockContract(state.Hash, state.Id, customMock); + } + + /// + /// Used for native artifacts only + /// + /// + /// Contract hash + /// Contract Id + /// Mocked Smart Contract + internal T FromHash(UInt160 hash, int? contractId = null) where T : SmartContract + { + return MockContract(hash, contractId, null); + } + + private T MockContract(UInt160 hash, int? contractId = null, Action>? customMock = null) where T : SmartContract + { + var mock = new Mock(new SmartContractInitialize() { Engine = this, Hash = hash, ContractId = contractId }) + { + CallBase = true + }; + + // User can mock specific calls + + customMock?.Invoke(mock); + + // Mock SmartContract + + foreach (var method in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public)) + { + if (!method.IsAbstract) continue; + + // Avoid to mock already mocked by custom mocks + + if (mock.IsMocked(method)) + { + var mockName = method.Name + ";" + method.GetParameters().Length; + var cm = new CustomMock() { Contract = mock.Object, Method = method }; + + if (_customMocks.TryGetValue(hash, out var mocks)) + { + if (!mocks.TryAdd(mockName, cm)) + { + throw new Exception("The same method can't be mocked twice"); + } + } + else + { + _customMocks.Add(hash, new Dictionary() { { mockName, cm } }); + } + + continue; + } + + // Get args + + Type[] args = method.GetParameters().Select(u => u.ParameterType).ToArray(); + + // Mock by ReturnType + + if (method.ReturnType != typeof(void)) + { + mock.MockFunction(method.Name, args, method.ReturnType); + } + else + { + mock.MockAction(method.Name, args); + } + } + + mock.Verify(); + + // Cache sc + + if (_contracts.TryGetValue(hash, out var result)) + { + result.Add(mock.Object); + } + else + { + _contracts[hash] = new List(new SmartContract[] { mock.Object }); + } + + // return mocked SmartContract + + return mock.Object; + } + + internal bool TryGetCustomMock(UInt160 hash, string method, int rc, [NotNullWhen(true)] out CustomMock? mi) + { + if (_customMocks.TryGetValue(hash, out var mocks)) + { + var mockName = method + ";" + rc; + + if (mocks.TryGetValue(mockName, out mi)) + { + return true; + } + } + + mi = null; + return false; + } + + /// + /// Execute raw script + /// + /// Script + /// StackItem + public StackItem Execute(Script script) + { + // Store the script in current transaction + + Transaction.Script = script; + + // Execute in neo VM + + var snapshot = Storage.Snapshot.CreateSnapshot(); + + using var engine = new TestingApplicationEngine(this, TriggerType.Application, Transaction, snapshot, CurrentBlock); + + engine.LoadScript(script); + + // Attach to static event + + ApplicationEngine.Log += ApplicationEngineLog; + ApplicationEngine.Notify += ApplicationEngineNotify; + + // Execute + + var executionResult = engine.Execute(); + + // Detach to static event + + ApplicationEngine.Log -= ApplicationEngineLog; + ApplicationEngine.Notify -= ApplicationEngineNotify; + + // Process result + + if (executionResult != VMState.HALT) + { + throw engine.FaultException ?? new Exception($"Error while executing the script"); + } + + snapshot.Commit(); + + if (engine.ResultStack.Count == 0) return StackItem.Null; + return engine.ResultStack.Pop(); + } + + /// + /// Set Transaction signers + /// + /// Signers + public void SetTransactionSigners(params Signer[] signers) + { + Transaction.Signers = signers; + } + + /// + /// Set Transaction Signers using CalledByEntry + /// + /// Signers + public void SetTransactionSigners(params UInt160[] signers) + { + Transaction.Signers = signers.Select(u => new Signer() { Account = u, Scopes = WitnessScope.CalledByEntry }).ToArray(); + } + + /// + /// Set Transaction Signers + /// + /// Scope + /// Signers + public void SetTransactionSigners(WitnessScope scope, params UInt160[] signers) + { + Transaction.Signers = signers.Select(u => new Signer() { Account = u, Scopes = scope }).ToArray(); + } + + /// + /// Generate a random new Signers with CalledByEntry scope by default + /// + /// Witness scope + /// Signer + public static Signer GetNewSigner(WitnessScope scope = WitnessScope.CalledByEntry) + { + var rand = new Random(); + var data = new byte[UInt160.Length]; + rand.NextBytes(data); + + return new Signer() + { + Account = new UInt160(data), + Scopes = scope, + }; + } + } +} diff --git a/src/Neo.SmartContract.Testing/TestStorage.cs b/src/Neo.SmartContract.Testing/TestStorage.cs new file mode 100644 index 000000000..5919c25e5 --- /dev/null +++ b/src/Neo.SmartContract.Testing/TestStorage.cs @@ -0,0 +1,143 @@ +using Neo.Json; +using Neo.Persistence; +using System; +using System.Buffers.Binary; +using System.Linq; + +namespace Neo.SmartContract.Testing +{ + /// + /// TestStorage centralizes the storage management of our TestEngine + /// + public class TestStorage + { + // Key to check if native contracts are initialized, by default: Neo.votersCountPrefix + private static readonly StorageKey _initKey = new() { Id = Native.NativeContract.NEO.Id, Key = new byte[] { 1 } }; + + /// + /// Store + /// + public IStore Store { get; init; } = new MemoryStore(); + + /// + /// Snapshot + /// + public SnapshotCache Snapshot { get; private set; } + + /// + /// Return true if native contract are initialized + /// + public bool IsInitialized => Snapshot.Contains(_initKey); + + /// + /// Constructor + /// + /// Store + public TestStorage(IStore store) + { + Store = store; + Snapshot = new SnapshotCache(Store.GetSnapshot()); + } + + /// + /// Commit + /// + public void Commit() + { + Snapshot.Commit(); + } + + /// + /// Rollback + /// + public void Rollback() + { + Snapshot.Dispose(); + Snapshot = new SnapshotCache(Store.GetSnapshot()); + } + + /// + /// Import data from json, expected data (in base64): + /// - "key" : "value" + /// - "prefix" : { "key":"value" } + /// + /// Json Object + public void Import(string json) + { + if (JToken.Parse(json) is not JObject jo) + { + throw new FormatException("The json is not a valid JObject"); + } + + Import(jo); + } + + /// + /// Import data from json, expected data (in base64): + /// - "key" : "value" + /// - "prefix" : { "key":"value" } + /// + /// Json Object + public void Import(JObject json) + { + foreach (var entry in json.Properties) + { + if (entry.Value is JString str) + { + // "key":"value" in base64 + + Snapshot.Add(new StorageKey(Convert.FromBase64String(entry.Key)), new StorageItem(Convert.FromBase64String(str.Value))); + } + else if (entry.Value is JObject obj) + { + // "prefix": { "key":"value" } in base64 + + byte[] prefix = Convert.FromBase64String(entry.Key); + + foreach (var subEntry in obj.Properties) + { + if (subEntry.Value is JString subStr) + { + Snapshot.Add( + new StorageKey(prefix.Concat(Convert.FromBase64String(subEntry.Key)).ToArray()), + new StorageItem(Convert.FromBase64String(subStr.Value)) + ); + } + } + } + } + } + + /// + /// Export data to json + /// + public JObject Export() + { + var buffer = new byte[(sizeof(int))]; + JObject ret = new(); + + foreach (var entry in Snapshot.Seek(Array.Empty(), SeekDirection.Forward)) + { + // "key":"value" in base64 + + JObject prefix; + BinaryPrimitives.WriteInt32LittleEndian(buffer, entry.Key.Id); + var keyId = Convert.ToBase64String(buffer); + + if (ret.ContainsProperty(keyId)) + { + prefix = (JObject)ret[keyId]!; + } + else + { + prefix = new(); + ret[keyId] = prefix; + } + + prefix[Convert.ToBase64String(entry.Key.Key.ToArray())] = Convert.ToBase64String(entry.Value.Value.ToArray()); + } + + return ret; + } + } +} diff --git a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs new file mode 100644 index 000000000..3110c7a68 --- /dev/null +++ b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs @@ -0,0 +1,82 @@ +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract.Testing.Extensions; +using System; + +namespace Neo.SmartContract.Testing +{ + /// + /// TestingApplicationEngine is responsible for redirecting System.Contract.Call syscalls to their corresponding mock if necessary + /// + internal class TestingApplicationEngine : ApplicationEngine + { + /// + /// Testing engine + /// + public TestEngine Engine { get; } + + public TestingApplicationEngine(TestEngine engine, TriggerType trigger, IVerifiable container, DataCache snapshot, Block persistingBlock) + : base(trigger, container, snapshot, persistingBlock, engine.ProtocolSettings, engine.Gas, null) + { + Engine = engine; + } + + protected override void OnSysCall(InteropDescriptor descriptor) + { + // Check if the syscall is a contract call and we need to mock it because it was defined by the user + + if (descriptor.Hash == 1381727586 && descriptor.Name == "System.Contract.Call" && descriptor.Parameters.Count == 4) + { + // Extract args + + if (Convert(Peek(0), descriptor.Parameters[0]) is UInt160 contractHash && + Convert(Peek(1), descriptor.Parameters[1]) is string method && + Convert(Peek(2), descriptor.Parameters[2]) is CallFlags callFlags && + Convert(Peek(3), descriptor.Parameters[3]) is VM.Types.Array args && + Engine.TryGetCustomMock(contractHash, method, args.Count, out var customMock)) + { + // Drop items + + Pop(); Pop(); Pop(); Pop(); + + // Do the same logic as ApplicationEngine + + ValidateCallFlags(descriptor.RequiredCallFlags); + AddGas(descriptor.FixedPrice * ExecFeeFactor); + + if (method.StartsWith('_')) throw new ArgumentException($"Invalid Method Name: {method}"); + if ((callFlags & ~CallFlags.All) != 0) + throw new ArgumentOutOfRangeException(nameof(callFlags)); + + /* Note: we allow to mock undeployed contracts + var contract = NativeContract.ContractManagement.GetContract(Snapshot, contractHash); + if (contract is null) throw new InvalidOperationException($"Called Contract Does Not Exist: {contractHash}"); + var md = contract.Manifest.Abi.GetMethod(method, args.Count); + if (md is null) throw new InvalidOperationException($"Method \"{method}\" with {args.Count} parameter(s) doesn't exist in the contract {contractHash}."); + var hasReturnValue = md.ReturnType != ContractParameterType.Void; + */ + + // Convert args to mocked method + + var methodParameters = customMock.Method.GetParameters(); + var parameters = new object[args.Count]; + for (int i = 0; i < args.Count; i++) + { + parameters[i] = args[i].ConvertTo(methodParameters[i].ParameterType)!; + } + + // Invoke + + var hasReturnValue = customMock.Method.ReturnType != typeof(void); + var returnValue = customMock.Method.Invoke(customMock.Contract, parameters); + if (hasReturnValue) + Push(Convert(returnValue)); + + return; + } + } + + base.OnSysCall(descriptor); + } + } +} diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs new file mode 100644 index 000000000..2062dfa40 --- /dev/null +++ b/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs @@ -0,0 +1,97 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Json; +using Neo.SmartContract.Manifest; +using Neo.SmartContract.Testing.Extensions; + +namespace Neo.SmartContract.TestEngine.UnitTests.Extensions +{ + [TestClass] + public class ArtifactExtensionsTests + { + [TestMethod] + public void TestGetArtifactsSource() + { + var manifest = ContractManifest.FromJson(JToken.Parse( + @"{""name"":""Contract1"",""groups"":[],""features"":{},""supportedstandards"":[""NEP-17""],""abi"":{""methods"":[{""name"":""symbol"",""parameters"":[],""returntype"":""String"",""offset"":1406,""safe"":true},{""name"":""decimals"",""parameters"":[],""returntype"":""Integer"",""offset"":1421,""safe"":true},{""name"":""totalSupply"",""parameters"":[],""returntype"":""Integer"",""offset"":43,""safe"":true},{""name"":""balanceOf"",""parameters"":[{""name"":""owner"",""type"":""Hash160""}],""returntype"":""Integer"",""offset"":85,""safe"":true},{""name"":""transfer"",""parameters"":[{""name"":""from"",""type"":""Hash160""},{""name"":""to"",""type"":""Hash160""},{""name"":""amount"",""type"":""Integer""},{""name"":""data"",""type"":""Any""}],""returntype"":""Boolean"",""offset"":281,""safe"":false},{""name"":""getOwner"",""parameters"":[],""returntype"":""Hash160"",""offset"":711,""safe"":true},{""name"":""setOwner"",""parameters"":[{""name"":""newOwner"",""type"":""Hash160""}],""returntype"":""Void"",""offset"":755,""safe"":false},{""name"":""burn"",""parameters"":[{""name"":""account"",""type"":""Hash160""},{""name"":""amount"",""type"":""Integer""}],""returntype"":""Void"",""offset"":873,""safe"":false},{""name"":""mint"",""parameters"":[{""name"":""to"",""type"":""Hash160""},{""name"":""amount"",""type"":""Integer""}],""returntype"":""Void"",""offset"":915,""safe"":false},{""name"":""withdraw"",""parameters"":[{""name"":""token"",""type"":""Hash160""},{""name"":""to"",""type"":""Hash160""},{""name"":""amount"",""type"":""Integer""}],""returntype"":""Boolean"",""offset"":957,""safe"":false},{""name"":""onNEP17Payment"",""parameters"":[{""name"":""from"",""type"":""Hash160""},{""name"":""amount"",""type"":""Integer""},{""name"":""data"",""type"":""Any""}],""returntype"":""Void"",""offset"":1139,""safe"":false},{""name"":""verify"",""parameters"":[],""returntype"":""Boolean"",""offset"":1203,""safe"":true},{""name"":""myMethod"",""parameters"":[],""returntype"":""String"",""offset"":1209,""safe"":false},{""name"":""_deploy"",""parameters"":[{""name"":""data"",""type"":""Any""},{""name"":""update"",""type"":""Boolean""}],""returntype"":""Void"",""offset"":1229,""safe"":false},{""name"":""update"",""parameters"":[{""name"":""nefFile"",""type"":""ByteArray""},{""name"":""manifest"",""type"":""String""}],""returntype"":""Void"",""offset"":1352,""safe"":false},{""name"":""_initialize"",""parameters"":[],""returntype"":""Void"",""offset"":1390,""safe"":false}],""events"":[{""name"":""Transfer"",""parameters"":[{""name"":""from"",""type"":""Hash160""},{""name"":""to"",""type"":""Hash160""},{""name"":""amount"",""type"":""Integer""}]},{""name"":""SetOwner"",""parameters"":[{""name"":""newOwner"",""type"":""Hash160""}]}]},""permissions"":[{""contract"":""*"",""methods"":""*""}],""trusts"":[],""extra"":{""Author"":""\u003CYour Name Or Company Here\u003E"",""Description"":""\u003CDescription Here\u003E"",""Email"":""\u003CYour Public Email Here\u003E"",""Version"":""\u003CVersion String Here\u003E""}}") as JObject); + + // Create artifacts + + var source = manifest.Abi.GetArtifactsSource(manifest.Name, generateProperties: true); + + Assert.AreEqual(source, @" +using Neo.Cryptography.ECC; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class Contract1 : Neo.SmartContract.Testing.SmartContract +{ + #region Events + public delegate void delSetOwner(UInt160 newOwner); + [DisplayName(""SetOwner"")] + public event delSetOwner? OnSetOwner; + public delegate void delTransfer(UInt160 from, UInt160 to, BigInteger amount); + [DisplayName(""Transfer"")] + public event delTransfer? OnTransfer; + #endregion + #region Properties + public abstract BigInteger Decimals { [DisplayName(""decimals"")] get; } + public abstract UInt160 Owner { [DisplayName(""getOwner"")] get; [DisplayName(""setOwner"")] set; } + public abstract string Symbol { [DisplayName(""symbol"")] get; } + public abstract BigInteger TotalSupply { [DisplayName(""totalSupply"")] get; } + public abstract bool Verify { [DisplayName(""verify"")] get; } + #endregion + #region Safe methods + /// + /// Safe method + /// + [DisplayName(""balanceOf"")] + public abstract BigInteger BalanceOf(UInt160 owner); + #endregion + #region Unsafe methods + /// + /// Unsafe method + /// + [DisplayName(""burn"")] + public abstract void Burn(UInt160 account, BigInteger amount); + /// + /// Unsafe method + /// + [DisplayName(""mint"")] + public abstract void Mint(UInt160 to, BigInteger amount); + /// + /// Unsafe method + /// + [DisplayName(""myMethod"")] + public abstract string MyMethod(); + /// + /// Unsafe method + /// + [DisplayName(""onNEP17Payment"")] + public abstract void OnNEP17Payment(UInt160 from, BigInteger amount, object? data = null); + /// + /// Unsafe method + /// + [DisplayName(""transfer"")] + public abstract bool Transfer(UInt160 from, UInt160 to, BigInteger amount, object? data = null); + /// + /// Unsafe method + /// + [DisplayName(""update"")] + public abstract void Update(byte[] nefFile, string manifest); + /// + /// Unsafe method + /// + [DisplayName(""withdraw"")] + public abstract bool Withdraw(UInt160 token, UInt160 to, BigInteger amount); + #endregion + #region Constructor for internal use only + protected Contract1(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion +} +".Replace("\r\n", "\n").Trim()); + } + } +} diff --git a/tests/Neo.SmartContract.Testing.UnitTests/NativeArtifactsTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/NativeArtifactsTests.cs new file mode 100644 index 000000000..f670241d8 --- /dev/null +++ b/tests/Neo.SmartContract.Testing.UnitTests/NativeArtifactsTests.cs @@ -0,0 +1,106 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; +using System.Numerics; +using System.Reflection; + +namespace Neo.SmartContract.Testing.UnitTests +{ + [TestClass] + public class NativeArtifactsTests + { + [TestMethod] + public void TestInitialize() + { + // Create the engine without initialize the native contracts + + var engine = new TestEngine(false); + + Assert.AreEqual(0, engine.Storage.Store.Seek(System.Array.Empty(), Persistence.SeekDirection.Forward).Count()); + + // Initialize native contracts + + engine.Native.Initialize(false); + + // Check symbols + + Assert.AreEqual("NEO", engine.Native.NEO.Symbol); + Assert.AreEqual("GAS", engine.Native.GAS.Symbol); + + // Ensure that the main address contains the totalSupply + + Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); + Assert.AreEqual(engine.Native.NEO.TotalSupply, engine.Native.NEO.BalanceOf(engine.ValidatorsAddress)); + } + + [TestMethod] + public void TestTransfer() + { + // Create and initialize TestEngine + + var engine = new TestEngine(true); + + // Fake signature of BFTAddress + + engine.SetTransactionSigners(Network.P2P.Payloads.WitnessScope.Global, engine.ValidatorsAddress); + + // Define address to transfer funds + + UInt160 addressTo = UInt160.Parse("0x1230000000000000000000000000000000000000"); + Assert.AreEqual(0, engine.Native.NEO.BalanceOf(addressTo)); + + // Attach to Transfer event + + var raisedEvent = false; + engine.Native.NEO.OnTransfer += (UInt160 from, UInt160 to, BigInteger amount) => + { + Assert.AreEqual(engine.Transaction.Sender, from); + Assert.AreEqual(addressTo, to); + Assert.AreEqual(123, amount); + + // If the event is raised, the variable will be changed + raisedEvent = true; + }; + + // Transfer funds + + Assert.IsTrue(engine.Native.NEO.Transfer(engine.Transaction.Sender, addressTo, 123, null)); + + // Ensure that we have balance and the event was raised + + Assert.IsTrue(raisedEvent); + Assert.AreEqual(123, engine.Native.NEO.BalanceOf(addressTo)); + } + + [TestMethod] + public void TestSignature() + { + // Create and initialize TestEngine + + var engine = new TestEngine(true); + + // Check initial value of getRegisterPrice + + Assert.AreEqual(100000000000, engine.Native.NEO.RegisterPrice); + + // Fake Committee Signature + + engine.SetTransactionSigners(new Network.P2P.Payloads.Signer() + { + Account = engine.CommitteeAddress, + Scopes = Network.P2P.Payloads.WitnessScope.Global + }); + + // Change RegisterPrice to 123 + + engine.Native.NEO.RegisterPrice = 123; + + Assert.AreEqual(123, engine.Native.NEO.RegisterPrice); + + // Now test it without this signature + + engine.SetTransactionSigners(TestEngine.GetNewSigner()); + + Assert.ThrowsException(() => engine.Native.NEO.RegisterPrice = 123); + } + } +} diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Neo.SmartContract.Testing.UnitTests.csproj b/tests/Neo.SmartContract.Testing.UnitTests/Neo.SmartContract.Testing.UnitTests.csproj new file mode 100644 index 000000000..09a33cb2e --- /dev/null +++ b/tests/Neo.SmartContract.Testing.UnitTests/Neo.SmartContract.Testing.UnitTests.csproj @@ -0,0 +1,11 @@ + + + + Neo.SmartContract.TestEngine.UnitTests + + + + + + + diff --git a/tests/Neo.SmartContract.Testing.UnitTests/SmartContractStorageTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/SmartContractStorageTests.cs new file mode 100644 index 000000000..c9dbca420 --- /dev/null +++ b/tests/Neo.SmartContract.Testing.UnitTests/SmartContractStorageTests.cs @@ -0,0 +1,58 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; +using System.Numerics; + +namespace Neo.SmartContract.Testing.UnitTests +{ + [TestClass] + public class SmartContractStorageTests + { + // Defines the prefix used to store the registration price in neo + + private readonly byte[] _registerPricePrefix = new byte[] { 13 }; + + [TestMethod] + public void TestAlterStorage() + { + // Create and initialize TestEngine + + TestEngine engine = new(true); + + // Check previous data + + Assert.AreEqual(100000000000, engine.Native.NEO.RegisterPrice); + + // Alter data + + engine.Native.NEO.Storage.Put(_registerPricePrefix, BigInteger.MinusOne); + + // Check altered data + + Assert.AreEqual(BigInteger.MinusOne, engine.Native.NEO.RegisterPrice); + } + + [TestMethod] + public void TestExportImport() + { + // Create and initialize TestEngine + + TestEngine engine = new(true); + + // Check previous data + + Assert.AreEqual(100000000000, engine.Native.NEO.RegisterPrice); + + var storage = engine.Native.NEO.Storage.Export(); + + // Alter data + + storage[storage.Properties.First().Key.ToString()][Convert.ToBase64String(_registerPricePrefix)] = Convert.ToBase64String(BigInteger.MinusOne.ToByteArray()); + engine.Native.NEO.Storage.Import(storage); + + // Check altered data + + Assert.AreEqual(BigInteger.MinusOne, engine.Native.NEO.RegisterPrice); + } + } +} diff --git a/tests/Neo.SmartContract.Testing.UnitTests/TestEngineTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/TestEngineTests.cs new file mode 100644 index 000000000..dbe763fcf --- /dev/null +++ b/tests/Neo.SmartContract.Testing.UnitTests/TestEngineTests.cs @@ -0,0 +1,127 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Neo.SmartContract.Testing.Extensions; +using Neo.VM; +using System.Collections.Generic; +using System.IO; + +namespace Neo.SmartContract.Testing.UnitTests +{ + [TestClass] + public class TestEngineTests + { + public abstract class MyUndeployedContract : SmartContract + { + public abstract int myReturnMethod(); + protected MyUndeployedContract(SmartContractInitialize initialize) : base(initialize) { } + } + + //[TestMethod] + public void GenerateNativeArtifacts() + { + foreach (var n in Native.NativeContract.Contracts) + { + var manifest = n.Manifest; + var source = manifest.Abi.GetArtifactsSource(manifest.Name, generateProperties: true); + var fullPath = Path.GetFullPath($"../../../../../src/Neo.SmartContract.Testing/Native/{manifest.Name}.cs"); + + File.WriteAllText(fullPath, source); + } + } + + [TestMethod] + public void TestHashExists() + { + TestEngine engine = new(false); + + Assert.ThrowsException(() => engine.FromHash(engine.Native.NEO.Hash, true)); + + engine.Native.Initialize(false); + + Assert.IsInstanceOfType(engine.FromHash(engine.Native.NEO.Hash, true)); + } + + [TestMethod] + public void TestCustomMock() + { + // Initialize TestEngine and native smart contracts + + TestEngine engine = new(true); + + // Get neo token smart contract and mock balanceOf to always return 123 + + var neo = engine.FromHash(engine.Native.NEO.Hash, + mock => mock.Setup(o => o.BalanceOf(It.IsAny())).Returns(123), + false); + + // Test direct call + + Assert.AreEqual(123, neo.BalanceOf(engine.ValidatorsAddress)); + + // Test vm call + + using (ScriptBuilder script = new()) + { + script.EmitDynamicCall(neo.Hash, nameof(neo.BalanceOf), engine.ValidatorsAddress); + + Assert.AreEqual(123, engine.Execute(script.ToArray()).GetInteger()); + } + + // Test mock on undeployed contract + + var undeployed = engine.FromHash(UInt160.Zero, + mock => mock.Setup(o => o.myReturnMethod()).Returns(1234), + false); + + using (ScriptBuilder script = new()) + { + script.EmitDynamicCall(UInt160.Zero, nameof(undeployed.myReturnMethod)); + + Assert.AreEqual(1234, engine.Execute(script.ToArray()).GetInteger()); + } + } + + [TestMethod] + public void TestNativeContracts() + { + TestEngine engine = new(false); + + Assert.AreEqual(engine.Native.ContractManagement.Hash, Native.NativeContract.ContractManagement.Hash); + Assert.AreEqual(engine.Native.StdLib.Hash, Native.NativeContract.StdLib.Hash); + Assert.AreEqual(engine.Native.CryptoLib.Hash, Native.NativeContract.CryptoLib.Hash); + Assert.AreEqual(engine.Native.GAS.Hash, Native.NativeContract.GAS.Hash); + Assert.AreEqual(engine.Native.NEO.Hash, Native.NativeContract.NEO.Hash); + Assert.AreEqual(engine.Native.Oracle.Hash, Native.NativeContract.Oracle.Hash); + Assert.AreEqual(engine.Native.Policy.Hash, Native.NativeContract.Policy.Hash); + Assert.AreEqual(engine.Native.RoleManagement.Hash, Native.NativeContract.RoleManagement.Hash); + } + + [TestMethod] + public void FromHashWithoutCheckTest() + { + UInt160 hash = UInt160.Parse("0x1230000000000000000000000000000000000000"); + TestEngine engine = new(false); + + var contract = engine.FromHash(hash, false); + + Assert.AreEqual(contract.Hash, hash); + } + + [TestMethod] + public void FromHashTest() + { + // Create the engine initializing the native contracts + + var engine = new TestEngine(true); + + // Instantiate neo contract from native hash, (not necessary if we use engine.Native.NEO) + + var neo = engine.FromHash(engine.Native.NEO.Hash, true); + + // Ensure that the main address contains the totalSupply + + Assert.AreEqual(100_000_000, neo.TotalSupply); + Assert.AreEqual(neo.TotalSupply, neo.BalanceOf(engine.ValidatorsAddress)); + } + } +} diff --git a/tests/Neo.SmartContract.Testing.UnitTests/TestStorageTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/TestStorageTests.cs new file mode 100644 index 000000000..8337c3fcb --- /dev/null +++ b/tests/Neo.SmartContract.Testing.UnitTests/TestStorageTests.cs @@ -0,0 +1,70 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Json; +using Neo.Persistence; +using System; +using System.Linq; +using System.Text; + +namespace Neo.SmartContract.Testing.UnitTests +{ + [TestClass] + public class TestStorageTests + { + [TestMethod] + public void LoadExportImport() + { + TestStorage store = new(new MemoryStore()); + + // empty + + var entries = store.Store.Seek(Array.Empty(), SeekDirection.Forward).ToArray(); + Assert.AreEqual(entries.Length, 0); + + // simple object + + var json = @"{""bXlSYXdLZXk="":""dmFsdWU=""}"; + + store.Import((JObject)JToken.Parse(json)); + store.Commit(); + + entries = store.Store.Seek(Array.Empty(), SeekDirection.Forward).ToArray(); + Assert.AreEqual(entries.Length, 1); + + Assert.AreEqual("myRawKey", Encoding.ASCII.GetString(entries[0].Key)); + Assert.AreEqual("value", Encoding.ASCII.GetString(entries[0].Value)); + + // prefix object + + json = @"{""bXk="":{""UmF3S2V5LTI="":""dmFsdWUtMg==""}}"; + + store.Import((JObject)JToken.Parse(json)); + store.Commit(); + + entries = store.Store.Seek(Array.Empty(), SeekDirection.Forward).ToArray(); + Assert.AreEqual(entries.Length, 2); + + Assert.AreEqual("myRawKey", Encoding.ASCII.GetString(entries[0].Key)); + Assert.AreEqual("value", Encoding.ASCII.GetString(entries[0].Value)); + + Assert.AreEqual("myRawKey-2", Encoding.ASCII.GetString(entries[1].Key)); + Assert.AreEqual("value-2", Encoding.ASCII.GetString(entries[1].Value)); + + // Test import + + TestStorage storeCopy = new(new MemoryStore()); + + store.Commit(); + storeCopy.Import(store.Export()); + storeCopy.Commit(); + + entries = storeCopy.Store.Seek(Array.Empty(), SeekDirection.Forward).ToArray(); + Assert.AreEqual(entries.Length, 2); + + Assert.AreEqual("myRawKey", Encoding.ASCII.GetString(entries[0].Key)); + Assert.AreEqual("value", Encoding.ASCII.GetString(entries[0].Value)); + + Assert.AreEqual("myRawKey-2", Encoding.ASCII.GetString(entries[1].Key)); + Assert.AreEqual("value-2", Encoding.ASCII.GetString(entries[1].Value)); + } + } +}