From 98e9105569a75b52d62bcb4e4206f95ef89dbe85 Mon Sep 17 00:00:00 2001 From: Suho Lee Date: Wed, 10 Jul 2024 15:43:47 +0900 Subject: [PATCH 01/15] exp: actionbase --- Libplanet.SDK.Action.Tests/BarAction.cs | 19 +++++ Libplanet.SDK.Action.Tests/FooAction.cs | 31 +++++++++ .../Libplanet.SDK.Action.Tests.csproj | 32 +++++++++ .../MockActionContext.cs | 69 +++++++++++++++++++ .../Action/ActionAttribute.cs | 14 ++++ .../Action/ActionBase/ActionBase.API.cs | 56 +++++++++++++++ .../Action/ActionBase/ActionBase.Context.cs | 15 ++++ .../Action/ActionBase/ActionBase.Fields.cs | 25 +++++++ .../Action/ActionBase/ActionBase.Methods.cs | 35 ++++++++++ .../Action/ActionBase/ActionBase.Params.cs | 46 +++++++++++++ .../Action/ActionBase/ActionBase.Statics.cs | 18 +++++ .../Action/ActionBase/ActionBase.cs | 65 +++++++++++++++++ Libplanet.SDK.Action/Action/ActionSchema.json | 7 ++ .../Action/ActionSerializer.cs | 6 ++ Libplanet.SDK.Action/Action/FieldAttribute.cs | 12 ++++ .../Action/SchemaAttribute.cs | 13 ++++ Libplanet.SDK.Action/Interfaces/ICallable.cs | 5 ++ .../Libplanet.SDK.Action.csproj | 17 +++++ Libplanet.sln | 50 ++++++++++++++ .../Consensus/ContextTest.cs | 1 + 20 files changed, 536 insertions(+) create mode 100644 Libplanet.SDK.Action.Tests/BarAction.cs create mode 100644 Libplanet.SDK.Action.Tests/FooAction.cs create mode 100644 Libplanet.SDK.Action.Tests/Libplanet.SDK.Action.Tests.csproj create mode 100644 Libplanet.SDK.Action.Tests/MockActionContext.cs create mode 100644 Libplanet.SDK.Action/Action/ActionAttribute.cs create mode 100644 Libplanet.SDK.Action/Action/ActionBase/ActionBase.API.cs create mode 100644 Libplanet.SDK.Action/Action/ActionBase/ActionBase.Context.cs create mode 100644 Libplanet.SDK.Action/Action/ActionBase/ActionBase.Fields.cs create mode 100644 Libplanet.SDK.Action/Action/ActionBase/ActionBase.Methods.cs create mode 100644 Libplanet.SDK.Action/Action/ActionBase/ActionBase.Params.cs create mode 100644 Libplanet.SDK.Action/Action/ActionBase/ActionBase.Statics.cs create mode 100644 Libplanet.SDK.Action/Action/ActionBase/ActionBase.cs create mode 100644 Libplanet.SDK.Action/Action/ActionSchema.json create mode 100644 Libplanet.SDK.Action/Action/ActionSerializer.cs create mode 100644 Libplanet.SDK.Action/Action/FieldAttribute.cs create mode 100644 Libplanet.SDK.Action/Action/SchemaAttribute.cs create mode 100644 Libplanet.SDK.Action/Interfaces/ICallable.cs create mode 100644 Libplanet.SDK.Action/Libplanet.SDK.Action.csproj diff --git a/Libplanet.SDK.Action.Tests/BarAction.cs b/Libplanet.SDK.Action.Tests/BarAction.cs new file mode 100644 index 00000000000..ec9f4c4e0cb --- /dev/null +++ b/Libplanet.SDK.Action.Tests/BarAction.cs @@ -0,0 +1,19 @@ +using Libplanet.Crypto; +using Libplanet.SDK.Action; +using Libplanet.SDK.Action.ActionBase; +using Libplanet.SDK.Interfaces; + +namespace Libplanet.SDK +{ + [Action("Bar")] + public class BarAction : ActionBase, ICallable + { + public override Address StorageAddress => + new Address("0xB179B5b2C06C52B6650F25E9ff9A335044Cf590F"); + + public void HelloWorld() + { + Console.WriteLine("Hello, World!"); + } + } +} diff --git a/Libplanet.SDK.Action.Tests/FooAction.cs b/Libplanet.SDK.Action.Tests/FooAction.cs new file mode 100644 index 00000000000..2f8bbe9f650 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/FooAction.cs @@ -0,0 +1,31 @@ +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.SDK.Action; +using Libplanet.SDK.Action.ActionBase; +using static System.Reflection.BindingFlags; + +namespace Libplanet.SDK; + +[Action("Foo")] +public class FooAction : ActionBase +{ + public override Address StorageAddress + => new Address("0xf54E82560aAE66C7db7eCF6960716B06ae6F8EBc"); + + public void Foo() + { + Console.WriteLine("Foo"); + } + + public void Execute() + { + var key = new Address("0xAFb51D00c4a2C853E1B9e2ab42E299352DF36190"); + SetState(key, (Text)"Hello, World!"); + } + + public void CallBar() + { + var barActionAddress = new Address("0xB179B5b2C06C52B6650F25E9ff9A335044Cf590F"); + var value = Call(barActionAddress, "HelloWorld"); + } +} diff --git a/Libplanet.SDK.Action.Tests/Libplanet.SDK.Action.Tests.csproj b/Libplanet.SDK.Action.Tests/Libplanet.SDK.Action.Tests.csproj new file mode 100644 index 00000000000..56e89f87852 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/Libplanet.SDK.Action.Tests.csproj @@ -0,0 +1,32 @@ + + + + net6.0 + enable + enable + + false + + Libplanet.SDK.Tests + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/Libplanet.SDK.Action.Tests/MockActionContext.cs b/Libplanet.SDK.Action.Tests/MockActionContext.cs new file mode 100644 index 00000000000..0e990d252e7 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/MockActionContext.cs @@ -0,0 +1,69 @@ +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Tx; + +namespace Libplanet.SDK.Tests; + +public class MockActionContext : IActionContext +{ + public Address Signer + { + get; + } + + public TxId? TxId + { + get; + } + + public Address Miner + { + get; + } + + public long BlockIndex + { + get; + } + + public int BlockProtocolVersion + { + get; + } + + public IWorld PreviousState + { + get; + set; + } + + public int RandomSeed + { + get; + } + + public bool BlockAction + { + get; + } + + public IReadOnlyList Txs + { + get; + } + + public void UseGas(long gas) + { + throw new NotSupportedException(); + } + + public IRandom GetRandom() => + throw new NotSupportedException(); + + public long GasUsed() => + throw new NotSupportedException(); + + public long GasLimit() => + throw new NotSupportedException(); +} diff --git a/Libplanet.SDK.Action/Action/ActionAttribute.cs b/Libplanet.SDK.Action/Action/ActionAttribute.cs new file mode 100644 index 00000000000..8368d02caee --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionAttribute.cs @@ -0,0 +1,14 @@ +namespace Libplanet.SDK.Action +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class ActionAttribute : SchemaAttribute + { + public ActionAttribute(string name) + : base(name) + { + Version = 1.0; + } + + public double Version; + } +} diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.API.cs new file mode 100644 index 00000000000..a0fb75f23bf --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.API.cs @@ -0,0 +1,56 @@ +using System.Reflection; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.SDK.Interfaces; + +namespace Libplanet.SDK.Action.ActionBase +{ + public partial class ActionBase + { + protected IValue? GetState(Address address) + => World.GetAccount(StorageAddress).GetState(address); + + protected IReadOnlyList GetStates(IReadOnlyList
addresses) + => World.GetAccount(StorageAddress).GetStates(addresses); + + protected void SetState(Address address, IValue value) + { + _world = World.SetAccount( + StorageAddress, + World.GetAccount(StorageAddress).SetState(address, value) + ); + } + + protected IValue? Call(Address address, string method, object?[]? args = null) + where T : ActionBase, ICallable + { + if (World.GetAccount(address).GetState(MetadataAddress) is not Dictionary metadata) + { + throw new Exception("Action cannot be found."); + } + + string? name = typeof(T).GetCustomAttribute()?.Name; + + if (name is null || metadata["name"] is not Text t || t.Value != name) + { + throw new Exception("Action cannot be found."); + } + + if ((T?)Activator.CreateInstance(typeof(T), address) is not { } callAction) + { + throw new Exception("Action cannot be found."); + } + + callAction.LoadContext(ActionContext, World); + + MethodInfo? methodInfo = typeof(T).GetMethod(method); + if (methodInfo is null) + { + throw new Exception("Method cannot be found."); + } + + return (IValue?)methodInfo.Invoke(callAction, args); + } + } +} + diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Context.cs b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Context.cs new file mode 100644 index 00000000000..d48839600c8 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Context.cs @@ -0,0 +1,15 @@ +using Libplanet.Action; +using Libplanet.Crypto; + +namespace Libplanet.SDK.Action.ActionBase +{ + public partial class ActionBase + { + protected Address Signer => ActionContext.Signer; + + protected Address Miner => ActionContext.Miner; + + protected IRandom Random => ActionContext.GetRandom(); + } +} + diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Fields.cs b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Fields.cs new file mode 100644 index 00000000000..e0e7ecbb6ac --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Fields.cs @@ -0,0 +1,25 @@ +using System.Security; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Libplanet.SDK.Action.ActionBase +{ + public partial class ActionBase + { + private static readonly Address MetadataAddress + = new Address("999999cf1046e68e36E1aA2E0E07105eDDD1f08E"); + + + private IValue? _args = null; + private string? _call = null; + + [SecurityCritical] + private IActionContext? _actionContext = null; + + [SecurityCritical] + private IWorld? _world = null; + } +} + diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Methods.cs b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Methods.cs new file mode 100644 index 00000000000..467830bd5f4 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Methods.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Libplanet.SDK.Action.ActionBase +{ + public partial class ActionBase + { + private void LoadContext(IActionContext context, IWorld world) + { + _actionContext = context; + _world = world; + } + + private IWorld RegisterStorage(Address storageAddress, IWorld world) + { + string name = GetType().GetCustomAttribute()?.Name + ?? throw new Exception("Name is not set."); + + Dictionary metadata = Dictionary.Empty + .Add("name", name) + .Add("version", 1); + + return world.SetAccount( + storageAddress, + world + .GetAccount(storageAddress) + .SetState(MetadataAddress, metadata) + ); + } + } +} + diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Params.cs b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Params.cs new file mode 100644 index 00000000000..9858546661b --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Params.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Libplanet.SDK.Action.ActionBase +{ + public partial class ActionBase + { + public bool IsLoaded { get; private set; } = false; + + public abstract Address StorageAddress { get; } + + private MethodInfo[] CallableMethods => + GetType() + .GetMethods() + .Where(IsCallableMethod) + .ToArray(); + + private IActionContext ActionContext + { + get + { + if (_actionContext == null) + { + throw new InvalidOperationException("ActionContext is not set."); + } + + return _actionContext; + } + } + + private IWorld World + { + get + { + if (_world == null) + { + throw new InvalidOperationException("State is not set."); + } + + return _world; + } + } + } +} diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Statics.cs b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Statics.cs new file mode 100644 index 00000000000..07a785f5982 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Statics.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Libplanet.SDK.Action.ActionBase +{ + public partial class ActionBase + { + private static bool IsCallableMethod(MethodInfo method) => + method.IsPublic; + + private static bool ValidateStorage(Address storageAddress, IWorld world) => + world + .GetAccount(storageAddress) + .GetState(MetadataAddress) is Dictionary; + } +} diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.cs b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.cs new file mode 100644 index 00000000000..6d765a4af85 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.cs @@ -0,0 +1,65 @@ +using System.Reflection; +using System.Security; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; + +[assembly: SecurityTransparent] + +namespace Libplanet.SDK.Action.ActionBase +{ + public abstract partial class ActionBase : IAction + { + public IValue PlainValue => Dictionary.Empty + .Add("call", _call ?? "Execute") + .Add("args", _args ?? Null.Value); + + public void LoadPlainValue(IValue plainValue) + { + var dict = (Dictionary)plainValue; + try + { + _call = (Text)dict["call"]; + } + catch + { + _call = "Execute"; + } + + try + { + _args = dict["args"]; + } + catch + { + _args = Null.Value; + } + + IsLoaded = true; + } + + public IWorld Execute(IActionContext context) + { + if (!IsLoaded) + { + throw new InvalidOperationException("Action is not loaded."); + } + + _actionContext = context; + _world = !ValidateStorage(StorageAddress, context.PreviousState) + ? RegisterStorage(StorageAddress, context.PreviousState) + : context.PreviousState; + + MethodInfo? method = CallableMethods.FirstOrDefault(m => m.Name == _call); + if (method is null) + { + throw new InvalidOperationException($"Method {_call} is not found."); + } + + object?[]? args = _args is Null ? null : new object?[] { _args }; + method.Invoke(this, args); + + return World; + } + } +} diff --git a/Libplanet.SDK.Action/Action/ActionSchema.json b/Libplanet.SDK.Action/Action/ActionSchema.json new file mode 100644 index 00000000000..5d53608150f --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionSchema.json @@ -0,0 +1,7 @@ +{ + "type_id": "Bar", + "value": { + "call": "HelloWorld", + "args": ["Alice", "1000"] + } +} diff --git a/Libplanet.SDK.Action/Action/ActionSerializer.cs b/Libplanet.SDK.Action/Action/ActionSerializer.cs new file mode 100644 index 00000000000..3033c35a918 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionSerializer.cs @@ -0,0 +1,6 @@ +namespace Libplanet.SDK.Action; + +public class ActionSerializer +{ + +} diff --git a/Libplanet.SDK.Action/Action/FieldAttribute.cs b/Libplanet.SDK.Action/Action/FieldAttribute.cs new file mode 100644 index 00000000000..3e3b51b1a1f --- /dev/null +++ b/Libplanet.SDK.Action/Action/FieldAttribute.cs @@ -0,0 +1,12 @@ +namespace Libplanet.SDK.Action; + +[AttributeUsage(AttributeTargets.Field)] +public class FieldAttribute : Attribute +{ + public FieldAttribute(byte[] key) + { + Key = key; + } + + public byte[] Key { get; } +} diff --git a/Libplanet.SDK.Action/Action/SchemaAttribute.cs b/Libplanet.SDK.Action/Action/SchemaAttribute.cs new file mode 100644 index 00000000000..4f17032049b --- /dev/null +++ b/Libplanet.SDK.Action/Action/SchemaAttribute.cs @@ -0,0 +1,13 @@ +namespace Libplanet.SDK.Action +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class SchemaAttribute : Attribute + { + protected SchemaAttribute(string name) + { + Name = name; + } + + public string Name { get; } + } +} diff --git a/Libplanet.SDK.Action/Interfaces/ICallable.cs b/Libplanet.SDK.Action/Interfaces/ICallable.cs new file mode 100644 index 00000000000..21776fa7c31 --- /dev/null +++ b/Libplanet.SDK.Action/Interfaces/ICallable.cs @@ -0,0 +1,5 @@ +namespace Libplanet.SDK.Interfaces; + +public interface ICallable +{ +} diff --git a/Libplanet.SDK.Action/Libplanet.SDK.Action.csproj b/Libplanet.SDK.Action/Libplanet.SDK.Action.csproj new file mode 100644 index 00000000000..58fc7c5edc7 --- /dev/null +++ b/Libplanet.SDK.Action/Libplanet.SDK.Action.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + Libplanet.SDK + + + + + false + runtime + + + + diff --git a/Libplanet.sln b/Libplanet.sln index 889e77da657..d72ff9fc538 100644 --- a/Libplanet.sln +++ b/Libplanet.sln @@ -69,6 +69,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B9C00FAF-3 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{88E7FAF4-CEEC-48B6-9114-71CFE3FC0F50}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sdk", "sdk", "{AA37E05B-D531-4546-A259-CDB5AC188E8A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.SDK.Action", "Libplanet.SDK.Action\Libplanet.SDK.Action.csproj", "{0697DCE3-6225-421B-8593-9199C3C15A35}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{33FAC033-5754-46C4-ADD5-C35EF8C786E4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.SDK.Action.Tests", "Libplanet.SDK.Action.Tests\Libplanet.SDK.Action.Tests.csproj", "{60147F82-4879-4C65-AC05-9CA80333F7E3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{231873FC-1BBB-4E9A-BF14-9E0E885DB554}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -613,6 +623,42 @@ Global {46C1A70D-D1DE-4173-A8C0-00F680F026E3}.ReleaseMono|x64.Build.0 = Debug|Any CPU {46C1A70D-D1DE-4173-A8C0-00F680F026E3}.ReleaseMono|x86.ActiveCfg = Debug|Any CPU {46C1A70D-D1DE-4173-A8C0-00F680F026E3}.ReleaseMono|x86.Build.0 = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Debug|x64.ActiveCfg = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Debug|x64.Build.0 = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Debug|x86.ActiveCfg = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Debug|x86.Build.0 = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Release|Any CPU.Build.0 = Release|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Release|x64.ActiveCfg = Release|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Release|x64.Build.0 = Release|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Release|x86.ActiveCfg = Release|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.Release|x86.Build.0 = Release|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.ReleaseMono|Any CPU.ActiveCfg = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.ReleaseMono|Any CPU.Build.0 = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.ReleaseMono|x64.ActiveCfg = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.ReleaseMono|x64.Build.0 = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.ReleaseMono|x86.ActiveCfg = Debug|Any CPU + {0697DCE3-6225-421B-8593-9199C3C15A35}.ReleaseMono|x86.Build.0 = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Debug|x64.ActiveCfg = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Debug|x64.Build.0 = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Debug|x86.ActiveCfg = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Debug|x86.Build.0 = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Release|Any CPU.Build.0 = Release|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Release|x64.ActiveCfg = Release|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Release|x64.Build.0 = Release|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Release|x86.ActiveCfg = Release|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.Release|x86.Build.0 = Release|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|Any CPU.ActiveCfg = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|Any CPU.Build.0 = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|x64.ActiveCfg = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|x64.Build.0 = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|x86.ActiveCfg = Debug|Any CPU + {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -648,6 +694,10 @@ Global {CF31204A-12CF-43C0-9054-B9AF98EC83BD} = {AC908E33-B856-4E23-9F81-B7F7C97A07F9} {97F29346-636E-4BCA-B33D-6D0DB26A5AA6} = {B9C00FAF-36CF-463A-83FA-43E6B974AE2E} {46C1A70D-D1DE-4173-A8C0-00F680F026E3} = {B9C00FAF-36CF-463A-83FA-43E6B974AE2E} + {33FAC033-5754-46C4-ADD5-C35EF8C786E4} = {AA37E05B-D531-4546-A259-CDB5AC188E8A} + {60147F82-4879-4C65-AC05-9CA80333F7E3} = {33FAC033-5754-46C4-ADD5-C35EF8C786E4} + {231873FC-1BBB-4E9A-BF14-9E0E885DB554} = {AA37E05B-D531-4546-A259-CDB5AC188E8A} + {0697DCE3-6225-421B-8593-9199C3C15A35} = {231873FC-1BBB-4E9A-BF14-9E0E885DB554} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DB552D2A-94E1-4A1C-9F3E-E0097C6158CD} diff --git a/test/Libplanet.Net.Tests/Consensus/ContextTest.cs b/test/Libplanet.Net.Tests/Consensus/ContextTest.cs index f21cc5615fd..9e92286f8f6 100644 --- a/test/Libplanet.Net.Tests/Consensus/ContextTest.cs +++ b/test/Libplanet.Net.Tests/Consensus/ContextTest.cs @@ -475,6 +475,7 @@ public async Task CanPreCommitOnEndCommit() /// receiving message from peer C or D. /// /// + /// A representing the asynchronous unit test. [Fact(Timeout = Timeout)] public async Task CanReplaceProposal() { From bfb25a364cbcf9fa6b8252af01cb03fac7ddd9a7 Mon Sep 17 00:00:00 2001 From: Suho Lee Date: Mon, 29 Jul 2024 21:55:51 +0900 Subject: [PATCH 02/15] exp: dynamic action loader --- Libplanet.SDK.Action/Action/ActionSchema.json | 3 +- .../Loader/ContractActionLoader.cs | 80 +++++++++++++++++++ Libplanet.sln | 24 ++++++ src/Libplanet.Action/Loader/IActionLoader.cs | 4 + 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 Libplanet.SDK.Action/Loader/ContractActionLoader.cs diff --git a/Libplanet.SDK.Action/Action/ActionSchema.json b/Libplanet.SDK.Action/Action/ActionSchema.json index 5d53608150f..7a23c6b2570 100644 --- a/Libplanet.SDK.Action/Action/ActionSchema.json +++ b/Libplanet.SDK.Action/Action/ActionSchema.json @@ -1,6 +1,7 @@ { "type_id": "Bar", - "value": { + "contract_address": "", + "values": { "call": "HelloWorld", "args": ["Alice", "1000"] } diff --git a/Libplanet.SDK.Action/Loader/ContractActionLoader.cs b/Libplanet.SDK.Action/Loader/ContractActionLoader.cs new file mode 100644 index 00000000000..70b738f57da --- /dev/null +++ b/Libplanet.SDK.Action/Loader/ContractActionLoader.cs @@ -0,0 +1,80 @@ +using System.Reflection; +using System.Security.Cryptography; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Action.State; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.SDK.Action.ActionBase; +using Libplanet.Store; +using Libplanet.Store.Trie; + +namespace Libplanet.SDK.Loader; + +public class ContractActionLoader : IActionLoader +{ + public static readonly Address ContractResolveAddress + = new Address("0x5657bCEa2BEcF39af57c93d06B60FC2bf589be42"); + + private readonly IStateStore _stateStore; + + public ContractActionLoader(IStateStore stateStore) + { + _stateStore = stateStore; + } + + public IAction LoadAction(long index, IValue value) => + throw new NotSupportedException(); + + public IAction LoadAction(HashDigest rootHash, IValue value) + { + if (value is not Dictionary dict) + { + throw new InvalidOperationException(); + } + + if (!dict.TryGetValue((Text)"contract_address", out var contractValue) + || contractValue is not Text contract) + { + throw new InvalidOperationException(); + } + + if (!dict.TryGetValue((Text)"type_id", out var typeValue) + || typeValue is not Text type) + { + throw new InvalidOperationException(); + } + + if (!dict.TryGetValue((Text)"values", out var plainValue) + || plainValue is not Dictionary) + { + throw new InvalidOperationException(); + } + + Address contractAddress = new Address(contract.Value); + + ITrie worldTrie = _stateStore.GetStateRoot(rootHash); + IWorld world = new World(new WorldBaseState(worldTrie, _stateStore)); + IAccount account = world.GetAccount(contractAddress); + IValue? dllBinary = account.GetState(ContractResolveAddress); + + if (dllBinary is not Binary binary) + { + throw new InvalidOperationException(); + } + + Assembly asm = Assembly.Load(binary.ByteArray.ToArray()); + var obj = asm.CreateInstance(type.Value); + + if (obj is not IAction originAction) + { + throw new InvalidOperationException(); + } + + var action = (ActionBase)originAction; + action.LoadPlainValue(plainValue); + + return action; + } +} diff --git a/Libplanet.sln b/Libplanet.sln index d72ff9fc538..6d7fa6e897b 100644 --- a/Libplanet.sln +++ b/Libplanet.sln @@ -79,6 +79,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.SDK.Action.Tests" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{231873FC-1BBB-4E9A-BF14-9E0E885DB554}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{96149248-DA70-495F-BD37-2EF0DAB77322}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectSeven", "sdk\exmaples\ProjectSeven\ProjectSeven.csproj", "{72731933-F935-400B-8714-64B64C719C09}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -659,6 +663,24 @@ Global {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|x64.Build.0 = Debug|Any CPU {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|x86.ActiveCfg = Debug|Any CPU {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|x86.Build.0 = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Debug|x64.ActiveCfg = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Debug|x64.Build.0 = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Debug|x86.ActiveCfg = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Debug|x86.Build.0 = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Release|Any CPU.Build.0 = Release|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Release|x64.ActiveCfg = Release|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Release|x64.Build.0 = Release|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Release|x86.ActiveCfg = Release|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.Release|x86.Build.0 = Release|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|Any CPU.ActiveCfg = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|Any CPU.Build.0 = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|x64.ActiveCfg = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|x64.Build.0 = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|x86.ActiveCfg = Debug|Any CPU + {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -698,6 +720,8 @@ Global {60147F82-4879-4C65-AC05-9CA80333F7E3} = {33FAC033-5754-46C4-ADD5-C35EF8C786E4} {231873FC-1BBB-4E9A-BF14-9E0E885DB554} = {AA37E05B-D531-4546-A259-CDB5AC188E8A} {0697DCE3-6225-421B-8593-9199C3C15A35} = {231873FC-1BBB-4E9A-BF14-9E0E885DB554} + {96149248-DA70-495F-BD37-2EF0DAB77322} = {AA37E05B-D531-4546-A259-CDB5AC188E8A} + {72731933-F935-400B-8714-64B64C719C09} = {96149248-DA70-495F-BD37-2EF0DAB77322} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DB552D2A-94E1-4A1C-9F3E-E0097C6158CD} diff --git a/src/Libplanet.Action/Loader/IActionLoader.cs b/src/Libplanet.Action/Loader/IActionLoader.cs index f280017439a..768d0ad16d6 100644 --- a/src/Libplanet.Action/Loader/IActionLoader.cs +++ b/src/Libplanet.Action/Loader/IActionLoader.cs @@ -1,4 +1,6 @@ +using System.Security.Cryptography; using Bencodex.Types; +using Libplanet.Common; namespace Libplanet.Action.Loader { @@ -20,5 +22,7 @@ public interface IActionLoader /// /// An instantiated with . public IAction LoadAction(long index, IValue value); + + public IAction LoadAction(HashDigest rootHash, IValue value); } } From 670b1973d86bc5b4ee3e26ddf7f5f4dfaa9c907d Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Wed, 31 Jul 2024 14:56:19 +0900 Subject: [PATCH 03/15] Rebase fix; reorganized files; simplified ActionBase --- .../MockActionContext.cs | 9 ++- .../Action/ActionAttribute.cs | 14 ---- .../Action/{ActionBase => }/ActionBase.API.cs | 21 +---- .../{ActionBase => }/ActionBase.Context.cs | 5 +- .../{ActionBase => }/ActionBase.Fields.cs | 8 +- .../Action/ActionBase.Methods.cs | 14 ++++ .../Action/ActionBase.Params.cs | 25 ++++++ .../Action/{ActionBase => }/ActionBase.cs | 41 +++------- .../Action/ActionBase/ActionBase.Methods.cs | 35 -------- .../Action/ActionBase/ActionBase.Params.cs | 46 ----------- .../Action/ActionBase/ActionBase.Statics.cs | 18 ----- .../Action/ActionSerializer.cs | 6 -- Libplanet.SDK.Action/Action/FieldAttribute.cs | 12 --- .../Action/SchemaAttribute.cs | 13 --- Libplanet.SDK.Action/ActionAttribute.cs | 37 +++++++++ .../Loader/ContractActionLoader.cs | 80 ------------------- Libplanet.sln | 21 ----- src/Libplanet.Action/Loader/IActionLoader.cs | 2 - 18 files changed, 99 insertions(+), 308 deletions(-) delete mode 100644 Libplanet.SDK.Action/Action/ActionAttribute.cs rename Libplanet.SDK.Action/Action/{ActionBase => }/ActionBase.API.cs (63%) rename Libplanet.SDK.Action/Action/{ActionBase => }/ActionBase.Context.cs (66%) rename Libplanet.SDK.Action/Action/{ActionBase => }/ActionBase.Fields.cs (70%) create mode 100644 Libplanet.SDK.Action/Action/ActionBase.Methods.cs create mode 100644 Libplanet.SDK.Action/Action/ActionBase.Params.cs rename Libplanet.SDK.Action/Action/{ActionBase => }/ActionBase.cs (51%) delete mode 100644 Libplanet.SDK.Action/Action/ActionBase/ActionBase.Methods.cs delete mode 100644 Libplanet.SDK.Action/Action/ActionBase/ActionBase.Params.cs delete mode 100644 Libplanet.SDK.Action/Action/ActionBase/ActionBase.Statics.cs delete mode 100644 Libplanet.SDK.Action/Action/ActionSerializer.cs delete mode 100644 Libplanet.SDK.Action/Action/FieldAttribute.cs delete mode 100644 Libplanet.SDK.Action/Action/SchemaAttribute.cs create mode 100644 Libplanet.SDK.Action/ActionAttribute.cs delete mode 100644 Libplanet.SDK.Action/Loader/ContractActionLoader.cs diff --git a/Libplanet.SDK.Action.Tests/MockActionContext.cs b/Libplanet.SDK.Action.Tests/MockActionContext.cs index 0e990d252e7..2a74e6d73f6 100644 --- a/Libplanet.SDK.Action.Tests/MockActionContext.cs +++ b/Libplanet.SDK.Action.Tests/MockActionContext.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; +using Libplanet.Types.Evidence; using Libplanet.Types.Tx; namespace Libplanet.SDK.Tests; @@ -43,7 +45,7 @@ public int RandomSeed get; } - public bool BlockAction + public bool IsPolicyAction { get; } @@ -53,6 +55,11 @@ public IReadOnlyList Txs get; } + public IReadOnlyList Evidence + { + get; + } + public void UseGas(long gas) { throw new NotSupportedException(); diff --git a/Libplanet.SDK.Action/Action/ActionAttribute.cs b/Libplanet.SDK.Action/Action/ActionAttribute.cs deleted file mode 100644 index 8368d02caee..00000000000 --- a/Libplanet.SDK.Action/Action/ActionAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Libplanet.SDK.Action -{ - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public class ActionAttribute : SchemaAttribute - { - public ActionAttribute(string name) - : base(name) - { - Version = 1.0; - } - - public double Version; - } -} diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase.API.cs similarity index 63% rename from Libplanet.SDK.Action/Action/ActionBase/ActionBase.API.cs rename to Libplanet.SDK.Action/Action/ActionBase.API.cs index a0fb75f23bf..f3f53df10aa 100644 --- a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.API.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -3,39 +3,23 @@ using Libplanet.Crypto; using Libplanet.SDK.Interfaces; -namespace Libplanet.SDK.Action.ActionBase +namespace Libplanet.SDK.Action { public partial class ActionBase { protected IValue? GetState(Address address) => World.GetAccount(StorageAddress).GetState(address); - protected IReadOnlyList GetStates(IReadOnlyList
addresses) - => World.GetAccount(StorageAddress).GetStates(addresses); - protected void SetState(Address address, IValue value) { _world = World.SetAccount( StorageAddress, - World.GetAccount(StorageAddress).SetState(address, value) - ); + World.GetAccount(StorageAddress).SetState(address, value)); } protected IValue? Call(Address address, string method, object?[]? args = null) where T : ActionBase, ICallable { - if (World.GetAccount(address).GetState(MetadataAddress) is not Dictionary metadata) - { - throw new Exception("Action cannot be found."); - } - - string? name = typeof(T).GetCustomAttribute()?.Name; - - if (name is null || metadata["name"] is not Text t || t.Value != name) - { - throw new Exception("Action cannot be found."); - } - if ((T?)Activator.CreateInstance(typeof(T), address) is not { } callAction) { throw new Exception("Action cannot be found."); @@ -53,4 +37,3 @@ protected void SetState(Address address, IValue value) } } } - diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Context.cs b/Libplanet.SDK.Action/Action/ActionBase.Context.cs similarity index 66% rename from Libplanet.SDK.Action/Action/ActionBase/ActionBase.Context.cs rename to Libplanet.SDK.Action/Action/ActionBase.Context.cs index d48839600c8..b4593fdcecb 100644 --- a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Context.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.Context.cs @@ -1,15 +1,12 @@ using Libplanet.Action; using Libplanet.Crypto; -namespace Libplanet.SDK.Action.ActionBase +namespace Libplanet.SDK.Action { public partial class ActionBase { protected Address Signer => ActionContext.Signer; protected Address Miner => ActionContext.Miner; - - protected IRandom Random => ActionContext.GetRandom(); } } - diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Fields.cs b/Libplanet.SDK.Action/Action/ActionBase.Fields.cs similarity index 70% rename from Libplanet.SDK.Action/Action/ActionBase/ActionBase.Fields.cs rename to Libplanet.SDK.Action/Action/ActionBase.Fields.cs index e0e7ecbb6ac..389234bc803 100644 --- a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Fields.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.Fields.cs @@ -4,14 +4,11 @@ using Libplanet.Action.State; using Libplanet.Crypto; -namespace Libplanet.SDK.Action.ActionBase +namespace Libplanet.SDK.Action { public partial class ActionBase { - private static readonly Address MetadataAddress - = new Address("999999cf1046e68e36E1aA2E0E07105eDDD1f08E"); - - + private string? _name = null; private IValue? _args = null; private string? _call = null; @@ -22,4 +19,3 @@ private static readonly Address MetadataAddress private IWorld? _world = null; } } - diff --git a/Libplanet.SDK.Action/Action/ActionBase.Methods.cs b/Libplanet.SDK.Action/Action/ActionBase.Methods.cs new file mode 100644 index 00000000000..5a58e3c4ae2 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase.Methods.cs @@ -0,0 +1,14 @@ +using Libplanet.Action; +using Libplanet.Action.State; + +namespace Libplanet.SDK.Action +{ + public partial class ActionBase + { + private void LoadContext(IActionContext context, IWorld world) + { + _actionContext = context; + _world = world; + } + } +} diff --git a/Libplanet.SDK.Action/Action/ActionBase.Params.cs b/Libplanet.SDK.Action/Action/ActionBase.Params.cs new file mode 100644 index 00000000000..0ec9f9c02e9 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase.Params.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Libplanet.SDK.Action +{ + public partial class ActionBase + { + public abstract Address StorageAddress { get; } + + private MethodInfo[] CallableMethods => GetType() + .GetMethods() + .Where(methodInfo => methodInfo.IsPublic) + .ToArray(); + + private IActionContext ActionContext => _actionContext ?? + throw new InvalidOperationException("ActionContext is not set."); + + private IWorld World => _world ?? + throw new InvalidOperationException("State is not set."); + + private bool Loaded => _args is null || _call is null || _name is null; + } +} diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.cs b/Libplanet.SDK.Action/Action/ActionBase.cs similarity index 51% rename from Libplanet.SDK.Action/Action/ActionBase/ActionBase.cs rename to Libplanet.SDK.Action/Action/ActionBase.cs index 6d765a4af85..6a6ad7b05ac 100644 --- a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.cs @@ -6,57 +6,36 @@ [assembly: SecurityTransparent] -namespace Libplanet.SDK.Action.ActionBase +namespace Libplanet.SDK.Action { public abstract partial class ActionBase : IAction { public IValue PlainValue => Dictionary.Empty + .Add("name", _name ?? throw new NullReferenceException()) .Add("call", _call ?? "Execute") .Add("args", _args ?? Null.Value); public void LoadPlainValue(IValue plainValue) { - var dict = (Dictionary)plainValue; - try - { - _call = (Text)dict["call"]; - } - catch - { - _call = "Execute"; - } - - try - { - _args = dict["args"]; - } - catch - { - _args = Null.Value; - } - - IsLoaded = true; + Dictionary dict = (Dictionary)plainValue; + _name = (Text)dict["name"]; + _call = (Text)dict["call"]; + _args = dict["args"]; } public IWorld Execute(IActionContext context) { - if (!IsLoaded) + if (!Loaded) { throw new InvalidOperationException("Action is not loaded."); } _actionContext = context; - _world = !ValidateStorage(StorageAddress, context.PreviousState) - ? RegisterStorage(StorageAddress, context.PreviousState) - : context.PreviousState; + _world = context.PreviousState; - MethodInfo? method = CallableMethods.FirstOrDefault(m => m.Name == _call); - if (method is null) - { + MethodInfo? method = CallableMethods.FirstOrDefault(m => m.Name == _call) ?? throw new InvalidOperationException($"Method {_call} is not found."); - } - - object?[]? args = _args is Null ? null : new object?[] { _args }; + object?[]? args = new object?[] { _args }; method.Invoke(this, args); return World; diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Methods.cs b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Methods.cs deleted file mode 100644 index 467830bd5f4..00000000000 --- a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Methods.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using Bencodex.Types; -using Libplanet.Action; -using Libplanet.Action.State; -using Libplanet.Crypto; - -namespace Libplanet.SDK.Action.ActionBase -{ - public partial class ActionBase - { - private void LoadContext(IActionContext context, IWorld world) - { - _actionContext = context; - _world = world; - } - - private IWorld RegisterStorage(Address storageAddress, IWorld world) - { - string name = GetType().GetCustomAttribute()?.Name - ?? throw new Exception("Name is not set."); - - Dictionary metadata = Dictionary.Empty - .Add("name", name) - .Add("version", 1); - - return world.SetAccount( - storageAddress, - world - .GetAccount(storageAddress) - .SetState(MetadataAddress, metadata) - ); - } - } -} - diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Params.cs b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Params.cs deleted file mode 100644 index 9858546661b..00000000000 --- a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Params.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Reflection; -using Libplanet.Action; -using Libplanet.Action.State; -using Libplanet.Crypto; - -namespace Libplanet.SDK.Action.ActionBase -{ - public partial class ActionBase - { - public bool IsLoaded { get; private set; } = false; - - public abstract Address StorageAddress { get; } - - private MethodInfo[] CallableMethods => - GetType() - .GetMethods() - .Where(IsCallableMethod) - .ToArray(); - - private IActionContext ActionContext - { - get - { - if (_actionContext == null) - { - throw new InvalidOperationException("ActionContext is not set."); - } - - return _actionContext; - } - } - - private IWorld World - { - get - { - if (_world == null) - { - throw new InvalidOperationException("State is not set."); - } - - return _world; - } - } - } -} diff --git a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Statics.cs b/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Statics.cs deleted file mode 100644 index 07a785f5982..00000000000 --- a/Libplanet.SDK.Action/Action/ActionBase/ActionBase.Statics.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using Bencodex.Types; -using Libplanet.Action.State; -using Libplanet.Crypto; - -namespace Libplanet.SDK.Action.ActionBase -{ - public partial class ActionBase - { - private static bool IsCallableMethod(MethodInfo method) => - method.IsPublic; - - private static bool ValidateStorage(Address storageAddress, IWorld world) => - world - .GetAccount(storageAddress) - .GetState(MetadataAddress) is Dictionary; - } -} diff --git a/Libplanet.SDK.Action/Action/ActionSerializer.cs b/Libplanet.SDK.Action/Action/ActionSerializer.cs deleted file mode 100644 index 3033c35a918..00000000000 --- a/Libplanet.SDK.Action/Action/ActionSerializer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Libplanet.SDK.Action; - -public class ActionSerializer -{ - -} diff --git a/Libplanet.SDK.Action/Action/FieldAttribute.cs b/Libplanet.SDK.Action/Action/FieldAttribute.cs deleted file mode 100644 index 3e3b51b1a1f..00000000000 --- a/Libplanet.SDK.Action/Action/FieldAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Libplanet.SDK.Action; - -[AttributeUsage(AttributeTargets.Field)] -public class FieldAttribute : Attribute -{ - public FieldAttribute(byte[] key) - { - Key = key; - } - - public byte[] Key { get; } -} diff --git a/Libplanet.SDK.Action/Action/SchemaAttribute.cs b/Libplanet.SDK.Action/Action/SchemaAttribute.cs deleted file mode 100644 index 4f17032049b..00000000000 --- a/Libplanet.SDK.Action/Action/SchemaAttribute.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Libplanet.SDK.Action -{ - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public class SchemaAttribute : Attribute - { - protected SchemaAttribute(string name) - { - Name = name; - } - - public string Name { get; } - } -} diff --git a/Libplanet.SDK.Action/ActionAttribute.cs b/Libplanet.SDK.Action/ActionAttribute.cs new file mode 100644 index 00000000000..73c5425ad6f --- /dev/null +++ b/Libplanet.SDK.Action/ActionAttribute.cs @@ -0,0 +1,37 @@ +using System; +using Bencodex.Types; + +namespace Libplanet.SDK.Action +{ + /// + /// Indicates that an action class (i.e., a class implementing + /// ) can be held by transactions and blocks. + /// It also gives an action class a for + /// serialization and deserialization. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class ActionTypeAttribute : Attribute + { + /// + /// Creates an with a given + /// . + /// + /// An action class's unique + /// identifier for serialization and deserialization. + public ActionTypeAttribute(string typeIdentifier) + { + TypeIdentifier = new Text(typeIdentifier); + } + + public ActionTypeAttribute(int typeIdentifier) + { + TypeIdentifier = new Integer(typeIdentifier); + } + + /// + /// An action class's unique identifier for serialization and + /// deserialization. + /// + public IValue TypeIdentifier { get; } + } +} diff --git a/Libplanet.SDK.Action/Loader/ContractActionLoader.cs b/Libplanet.SDK.Action/Loader/ContractActionLoader.cs deleted file mode 100644 index 70b738f57da..00000000000 --- a/Libplanet.SDK.Action/Loader/ContractActionLoader.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Reflection; -using System.Security.Cryptography; -using Bencodex.Types; -using Libplanet.Action; -using Libplanet.Action.Loader; -using Libplanet.Action.State; -using Libplanet.Common; -using Libplanet.Crypto; -using Libplanet.SDK.Action.ActionBase; -using Libplanet.Store; -using Libplanet.Store.Trie; - -namespace Libplanet.SDK.Loader; - -public class ContractActionLoader : IActionLoader -{ - public static readonly Address ContractResolveAddress - = new Address("0x5657bCEa2BEcF39af57c93d06B60FC2bf589be42"); - - private readonly IStateStore _stateStore; - - public ContractActionLoader(IStateStore stateStore) - { - _stateStore = stateStore; - } - - public IAction LoadAction(long index, IValue value) => - throw new NotSupportedException(); - - public IAction LoadAction(HashDigest rootHash, IValue value) - { - if (value is not Dictionary dict) - { - throw new InvalidOperationException(); - } - - if (!dict.TryGetValue((Text)"contract_address", out var contractValue) - || contractValue is not Text contract) - { - throw new InvalidOperationException(); - } - - if (!dict.TryGetValue((Text)"type_id", out var typeValue) - || typeValue is not Text type) - { - throw new InvalidOperationException(); - } - - if (!dict.TryGetValue((Text)"values", out var plainValue) - || plainValue is not Dictionary) - { - throw new InvalidOperationException(); - } - - Address contractAddress = new Address(contract.Value); - - ITrie worldTrie = _stateStore.GetStateRoot(rootHash); - IWorld world = new World(new WorldBaseState(worldTrie, _stateStore)); - IAccount account = world.GetAccount(contractAddress); - IValue? dllBinary = account.GetState(ContractResolveAddress); - - if (dllBinary is not Binary binary) - { - throw new InvalidOperationException(); - } - - Assembly asm = Assembly.Load(binary.ByteArray.ToArray()); - var obj = asm.CreateInstance(type.Value); - - if (obj is not IAction originAction) - { - throw new InvalidOperationException(); - } - - var action = (ActionBase)originAction; - action.LoadPlainValue(plainValue); - - return action; - } -} diff --git a/Libplanet.sln b/Libplanet.sln index 6d7fa6e897b..279bc17bd3d 100644 --- a/Libplanet.sln +++ b/Libplanet.sln @@ -81,8 +81,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{231873FC-1BB EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{96149248-DA70-495F-BD37-2EF0DAB77322}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectSeven", "sdk\exmaples\ProjectSeven\ProjectSeven.csproj", "{72731933-F935-400B-8714-64B64C719C09}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -663,24 +661,6 @@ Global {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|x64.Build.0 = Debug|Any CPU {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|x86.ActiveCfg = Debug|Any CPU {60147F82-4879-4C65-AC05-9CA80333F7E3}.ReleaseMono|x86.Build.0 = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Debug|Any CPU.Build.0 = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Debug|x64.ActiveCfg = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Debug|x64.Build.0 = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Debug|x86.ActiveCfg = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Debug|x86.Build.0 = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Release|Any CPU.ActiveCfg = Release|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Release|Any CPU.Build.0 = Release|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Release|x64.ActiveCfg = Release|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Release|x64.Build.0 = Release|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Release|x86.ActiveCfg = Release|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.Release|x86.Build.0 = Release|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|Any CPU.ActiveCfg = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|Any CPU.Build.0 = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|x64.ActiveCfg = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|x64.Build.0 = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|x86.ActiveCfg = Debug|Any CPU - {72731933-F935-400B-8714-64B64C719C09}.ReleaseMono|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -721,7 +701,6 @@ Global {231873FC-1BBB-4E9A-BF14-9E0E885DB554} = {AA37E05B-D531-4546-A259-CDB5AC188E8A} {0697DCE3-6225-421B-8593-9199C3C15A35} = {231873FC-1BBB-4E9A-BF14-9E0E885DB554} {96149248-DA70-495F-BD37-2EF0DAB77322} = {AA37E05B-D531-4546-A259-CDB5AC188E8A} - {72731933-F935-400B-8714-64B64C719C09} = {96149248-DA70-495F-BD37-2EF0DAB77322} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DB552D2A-94E1-4A1C-9F3E-E0097C6158CD} diff --git a/src/Libplanet.Action/Loader/IActionLoader.cs b/src/Libplanet.Action/Loader/IActionLoader.cs index 768d0ad16d6..74d00ebc487 100644 --- a/src/Libplanet.Action/Loader/IActionLoader.cs +++ b/src/Libplanet.Action/Loader/IActionLoader.cs @@ -22,7 +22,5 @@ public interface IActionLoader /// /// An instantiated with . public IAction LoadAction(long index, IValue value); - - public IAction LoadAction(HashDigest rootHash, IValue value); } } From e4171f9aec6db7ce3ed8ac6e8c6bf81b2eff7714 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 9 Aug 2024 06:07:26 +0900 Subject: [PATCH 04/15] Overhauled ActionBase; sample tests --- Libplanet.SDK.Action.Tests/BarAction.cs | 19 -- Libplanet.SDK.Action.Tests/FooAction.cs | 31 ---- .../MockActionContext.cs | 92 ++++----- .../Sample/Actions/NumberAction.cs | 47 +++++ .../Sample/Actions/NumberLogAction.cs | 57 ++++++ .../Sample/Actions/TextAction.cs | 24 +++ .../Sample/SampleActionsTest.cs | 174 ++++++++++++++++++ Libplanet.SDK.Action/Action/ActionBase.API.cs | 45 ++++- .../Action/ActionBase.Context.cs | 1 - .../Action/ActionBase.Fields.cs | 1 - .../Action/ActionBase.Params.cs | 6 +- Libplanet.SDK.Action/Action/ActionBase.cs | 10 +- Libplanet.SDK.Action/ActionAttribute.cs | 37 ---- .../Attributes/CallableAttribute.cs | 7 + .../Attributes/ExecutableAttribute.cs | 7 + Libplanet.SDK.Action/Interfaces/ICallable.cs | 5 - src/Libplanet.Action/AssemblyInfo.cs | 1 + 17 files changed, 401 insertions(+), 163 deletions(-) delete mode 100644 Libplanet.SDK.Action.Tests/BarAction.cs delete mode 100644 Libplanet.SDK.Action.Tests/FooAction.cs create mode 100644 Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs create mode 100644 Libplanet.SDK.Action.Tests/Sample/Actions/NumberLogAction.cs create mode 100644 Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs create mode 100644 Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs delete mode 100644 Libplanet.SDK.Action/ActionAttribute.cs create mode 100644 Libplanet.SDK.Action/Attributes/CallableAttribute.cs create mode 100644 Libplanet.SDK.Action/Attributes/ExecutableAttribute.cs delete mode 100644 Libplanet.SDK.Action/Interfaces/ICallable.cs diff --git a/Libplanet.SDK.Action.Tests/BarAction.cs b/Libplanet.SDK.Action.Tests/BarAction.cs deleted file mode 100644 index ec9f4c4e0cb..00000000000 --- a/Libplanet.SDK.Action.Tests/BarAction.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Libplanet.Crypto; -using Libplanet.SDK.Action; -using Libplanet.SDK.Action.ActionBase; -using Libplanet.SDK.Interfaces; - -namespace Libplanet.SDK -{ - [Action("Bar")] - public class BarAction : ActionBase, ICallable - { - public override Address StorageAddress => - new Address("0xB179B5b2C06C52B6650F25E9ff9A335044Cf590F"); - - public void HelloWorld() - { - Console.WriteLine("Hello, World!"); - } - } -} diff --git a/Libplanet.SDK.Action.Tests/FooAction.cs b/Libplanet.SDK.Action.Tests/FooAction.cs deleted file mode 100644 index 2f8bbe9f650..00000000000 --- a/Libplanet.SDK.Action.Tests/FooAction.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Bencodex.Types; -using Libplanet.Crypto; -using Libplanet.SDK.Action; -using Libplanet.SDK.Action.ActionBase; -using static System.Reflection.BindingFlags; - -namespace Libplanet.SDK; - -[Action("Foo")] -public class FooAction : ActionBase -{ - public override Address StorageAddress - => new Address("0xf54E82560aAE66C7db7eCF6960716B06ae6F8EBc"); - - public void Foo() - { - Console.WriteLine("Foo"); - } - - public void Execute() - { - var key = new Address("0xAFb51D00c4a2C853E1B9e2ab42E299352DF36190"); - SetState(key, (Text)"Hello, World!"); - } - - public void CallBar() - { - var barActionAddress = new Address("0xB179B5b2C06C52B6650F25E9ff9A335044Cf590F"); - var value = Call(barActionAddress, "HelloWorld"); - } -} diff --git a/Libplanet.SDK.Action.Tests/MockActionContext.cs b/Libplanet.SDK.Action.Tests/MockActionContext.cs index 2a74e6d73f6..a45f78098d9 100644 --- a/Libplanet.SDK.Action.Tests/MockActionContext.cs +++ b/Libplanet.SDK.Action.Tests/MockActionContext.cs @@ -1,76 +1,60 @@ -using System.Collections.Generic; using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Types.Evidence; using Libplanet.Types.Tx; -namespace Libplanet.SDK.Tests; - -public class MockActionContext : IActionContext +namespace Libplanet.SDK.Action.Tests { - public Address Signer + public class MockActionContext : IActionContext { - get; - } + public MockActionContext( + Address signer, + Address miner, + IWorld previousState) + { + Signer = signer; + Miner = miner; + PreviousState = previousState; + } - public TxId? TxId - { - get; - } + public Address Signer { get; } - public Address Miner - { - get; - } + public TxId? TxId => + throw new NotSupportedException(); - public long BlockIndex - { - get; - } + public Address Miner { get; } - public int BlockProtocolVersion - { - get; - } + public long BlockIndex => + throw new NotSupportedException(); - public IWorld PreviousState - { - get; - set; - } + public int BlockProtocolVersion => + throw new NotSupportedException(); - public int RandomSeed - { - get; - } + public IWorld PreviousState { get; } - public bool IsPolicyAction - { - get; - } + public int RandomSeed => + throw new NotSupportedException(); - public IReadOnlyList Txs - { - get; - } + public bool IsPolicyAction => + throw new NotSupportedException(); - public IReadOnlyList Evidence - { - get; - } + public IReadOnlyList Txs => + throw new NotSupportedException(); - public void UseGas(long gas) - { - throw new NotSupportedException(); - } + public IReadOnlyList Evidence => + throw new NotSupportedException(); + + public void UseGas(long gas) => + throw new NotSupportedException(); - public IRandom GetRandom() => - throw new NotSupportedException(); + public IRandom GetRandom() => + throw new NotSupportedException(); - public long GasUsed() => - throw new NotSupportedException(); + public long GasUsed() => + throw new NotSupportedException(); - public long GasLimit() => - throw new NotSupportedException(); + public long GasLimit() => + throw new NotSupportedException(); + } } diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs new file mode 100644 index 00000000000..1fba97e2b29 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs @@ -0,0 +1,47 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; + +namespace Libplanet.SDK.Action.Tests.Sample.Actions +{ + [ActionType("Number")] + public class NumberAction : ActionBase + { + public override Address StorageAddress => + new Address("0x1000000000000000000000000000000000000001"); + + [Executable] + public void Add(IValue args) + { + Integer operand = (Integer)args; + Integer stored = GetState(Signer) is IValue value + ? (Integer)value + : new Integer(0); + Call(nameof(NumberLogAction.Add), new object?[] { operand }); + SetState(Signer, new Integer(stored.Value + operand.Value)); + } + + [Executable] + public void Subtract(IValue args) + { + Integer operand = (Integer)args; + Integer stored = GetState(Signer) is IValue value + ? (Integer)value + : new Integer(0); + Call(nameof(NumberLogAction.Subtract), new object?[] { operand }); + SetState(Signer, new Integer(stored.Value - operand.Value)); + } + + [Executable] + public void Multiply(IValue args) + { + Integer operand = (Integer)args; + Integer stored = GetState(Signer) is IValue value + ? (Integer)value + : new Integer(1); + Call(nameof(NumberLogAction.Multiply), new object?[] { operand }); + SetState(Signer, new Integer(stored.Value * operand.Value)); + } + } +} diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberLogAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberLogAction.cs new file mode 100644 index 00000000000..1144d5a6b25 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberLogAction.cs @@ -0,0 +1,57 @@ +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; + +namespace Libplanet.SDK.Action.Tests.Sample.Actions +{ + public class NumberLogAction : ActionBase + { + public override Address StorageAddress => + new Address("0x1000000000000000000000000000000000000002"); + + [Callable] + public void Add(Integer operand) + { + Text stored = GetState(Signer) is IValue value + ? (Text)value + : new Text(string.Empty); + Text formatted = operand.Value >= 0 + ? new Text($"{operand.Value}") + : new Text($"({operand.Value})"); + formatted = stored.Value.Length == 0 + ? new Text(stored.Value + $"{formatted.Value}") + : new Text(stored.Value + $" + {formatted.Value}"); + SetState(Signer, formatted); + } + + [Callable] + public void Subtract(Integer operand) + { + Text stored = GetState(Signer) is IValue value + ? (Text)value + : new Text(string.Empty); + Text formatted = operand.Value >= 0 + ? new Text($"{operand.Value}") + : new Text($"({operand.Value})"); + formatted = stored.Value.Length == 0 + ? new Text(stored.Value + $"{formatted.Value}") + : new Text(stored.Value + $" - {formatted.Value}"); + SetState(Signer, formatted); + } + + // This is without Callable attribute on purpose for testing. + public void Multiply(Integer operand) + { + Text stored = GetState(Signer) is IValue value + ? (Text)value + : new Text(string.Empty); + Text formatted = operand.Value >= 0 + ? new Text($"{operand.Value}") + : new Text($"({operand.Value})"); + formatted = stored.Value.Length == 0 + ? new Text($"{formatted.Value}") + : new Text(stored.Value + $" * {formatted.Value}"); + SetState(Signer, formatted); + } + } +} diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs new file mode 100644 index 00000000000..ef47792d53c --- /dev/null +++ b/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs @@ -0,0 +1,24 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; + +namespace Libplanet.SDK.Action.Tests +{ + [ActionType("Text")] + public class TextAction : ActionBase + { + public override Address StorageAddress => + new Address("0x1000000000000000000000000000000000000003"); + + [Executable] + public void Append(IValue args) + { + Text operand = (Text)args; + Text stored = GetState(Signer) is IValue value + ? (Text)value + : new Text(string.Empty); + SetState(Signer, new Text(stored.Value + operand.Value)); + } + } +} diff --git a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs new file mode 100644 index 00000000000..50d3a9fdb4e --- /dev/null +++ b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs @@ -0,0 +1,174 @@ +using System.Collections.Immutable; +using System.Reflection; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Tests.Sample.Actions; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Libplanet.Types.Blocks; +using Xunit; + +namespace Libplanet.SDK.Action.Tests.Sample +{ + public class SampleActionsTest + { + private TypedActionLoader _loader; + private IStateStore _stateStore; + private IWorld _world; + + public SampleActionsTest() + { + _loader = new TypedActionLoader( + ImmutableDictionary.Empty + .Add(new Text("Number"), typeof(NumberAction)) + .Add(new Text("Text"), typeof(TextAction))); + + _stateStore = new TrieStateStore(new MemoryKeyValueStore()); + + ITrie trie = _stateStore.GetStateRoot(null); + trie = trie.SetMetadata(new TrieMetadata(Block.CurrentProtocolVersion)); + trie = _stateStore.Commit(trie); + _world = new World(new WorldBaseState(trie, _stateStore)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void NumberAddAndSubtract(bool commit) + { + IValue plainValue = Dictionary.Empty + .Add("type_id", "Number") + .Add("call", "Add") + .Add("args", 5); + NumberAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); + Address signer = new PrivateKey().Address; + IWorld world = _world; + + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Integer(5), + world.GetAccountState(action.StorageAddress).GetState(signer)); + Assert.Equal( + new Text("5"), + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + + plainValue = Dictionary.Empty + .Add("type_id", "Number") + .Add("call", "Subtract") + .Add("args", 8); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Integer(-3), + world.GetAccountState(action.StorageAddress).GetState(signer)); + Assert.Equal( + new Text("5 - 8"), + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TextAppend(bool commit) + { + IValue plainValue = Dictionary.Empty + .Add("type_id", "Text") + .Add("call", "Append") + .Add("args", "Hello"); + TextAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); + Address signer = new PrivateKey().Address; + IWorld world = _world; + + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Text("Hello"), + world.GetAccountState(action.StorageAddress).GetState(signer)); + + plainValue = Dictionary.Empty + .Add("type_id", "Text") + .Add("call", "Append") + .Add("args", " world"); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Text("Hello world"), + world.GetAccountState(action.StorageAddress).GetState(signer)); + } + + [Fact] + public void InvalidPlainValueForLoading() + { + IValue plainValue = Dictionary.Empty // Invalid type_id + .Add("type_id", "Run") + .Add("call", "Append") + .Add("args", "Hello"); + Assert.Throws(() => _loader.LoadAction(0, plainValue)); + + plainValue = Dictionary.Empty // Missing type_id + .Add("call", "Append") + .Add("args", "Hello"); + Assert.Throws(() => _loader.LoadAction(0, plainValue)); + + plainValue = Dictionary.Empty // Missing call + .Add("type_id", "Number") + .Add("args", 5); + Assert.Throws(() => _loader.LoadAction(0, plainValue)); + + plainValue = Dictionary.Empty // Missing args + .Add("type_id", "Number") + .Add("call", "Add"); + Assert.Throws(() => _loader.LoadAction(0, plainValue)); + } + + [Fact] + public void InvalidPlainValueForExecution() + { + IValue plainValue = Dictionary.Empty // Invalid call + .Add("type_id", "Number") + .Add("call", "Divide") + .Add("args", 5); + IAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); + Address address = new PrivateKey().Address; + Assert.Throws(() => + action.Execute(new MockActionContext(address, address, _world))); + + plainValue = Dictionary.Empty // Invalid args + .Add("type_id", "Number") + .Add("call", "Add") + .Add("args", "Hello"); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + Assert.IsType( + Assert.Throws(() => + action.Execute(new MockActionContext(address, address, _world))) + .InnerException); + } + + [Fact] + public void CallableAttributeIsRequired() + { + IValue plainValue = Dictionary.Empty + .Add("type_id", "Number") + .Add("call", "Multiply") + .Add("args", 5); + NumberAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); + Address signer = new PrivateKey().Address; + IWorld world = _world; + + Assert.IsType( + Assert.Throws(() => + action.Execute(new MockActionContext(signer, signer, world))) + .InnerException); + } + } +} diff --git a/Libplanet.SDK.Action/Action/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase.API.cs index f3f53df10aa..be06a6b4162 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.API.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -1,7 +1,7 @@ using System.Reflection; using Bencodex.Types; using Libplanet.Crypto; -using Libplanet.SDK.Interfaces; +using Libplanet.SDK.Action.Attributes; namespace Libplanet.SDK.Action { @@ -17,23 +17,52 @@ protected void SetState(Address address, IValue value) World.GetAccount(StorageAddress).SetState(address, value)); } - protected IValue? Call(Address address, string method, object?[]? args = null) - where T : ActionBase, ICallable + protected void Call(string methodName, object?[]? args = null) + where T : ActionBase { - if ((T?)Activator.CreateInstance(typeof(T), address) is not { } callAction) + if (Activator.CreateInstance(typeof(T)) is not T calledAction) { throw new Exception("Action cannot be found."); } - callAction.LoadContext(ActionContext, World); + calledAction.LoadContext(ActionContext, World); - MethodInfo? methodInfo = typeof(T).GetMethod(method); - if (methodInfo is null) + MethodInfo methodInfo = typeof(T).GetMethod(methodName) ?? + throw new Exception("Method cannot be found."); + if (methodInfo.GetCustomAttribute() is null) + { + throw new Exception( + $"Target method is missing a {nameof(CallableAttribute)}"); + } + + methodInfo.Invoke(calledAction, args); + + _world = calledAction._world; + _actionContext = calledAction._actionContext; + } + + protected U Call(string methodName, object?[]? args = null) + where T : ActionBase + { + if (Activator.CreateInstance(typeof(T)) is not T calledAction) { + throw new Exception("Action cannot be found."); + } + + calledAction.LoadContext(ActionContext, World); + + MethodInfo methodInfo = typeof(T).GetMethod(methodName) ?? throw new Exception("Method cannot be found."); + + if (methodInfo.Invoke(calledAction, args) is not U result) + { + throw new Exception("Return type doesn't match."); } - return (IValue?)methodInfo.Invoke(callAction, args); + _world = calledAction._world; + _actionContext = calledAction._actionContext; + + return result; } } } diff --git a/Libplanet.SDK.Action/Action/ActionBase.Context.cs b/Libplanet.SDK.Action/Action/ActionBase.Context.cs index b4593fdcecb..24f6a6d9d26 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.Context.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.Context.cs @@ -1,4 +1,3 @@ -using Libplanet.Action; using Libplanet.Crypto; namespace Libplanet.SDK.Action diff --git a/Libplanet.SDK.Action/Action/ActionBase.Fields.cs b/Libplanet.SDK.Action/Action/ActionBase.Fields.cs index 389234bc803..e0ffd28c28b 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.Fields.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.Fields.cs @@ -2,7 +2,6 @@ using Bencodex.Types; using Libplanet.Action; using Libplanet.Action.State; -using Libplanet.Crypto; namespace Libplanet.SDK.Action { diff --git a/Libplanet.SDK.Action/Action/ActionBase.Params.cs b/Libplanet.SDK.Action/Action/ActionBase.Params.cs index 0ec9f9c02e9..d8b5c29e9c7 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.Params.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.Params.cs @@ -2,6 +2,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; namespace Libplanet.SDK.Action { @@ -9,9 +10,10 @@ public partial class ActionBase { public abstract Address StorageAddress { get; } - private MethodInfo[] CallableMethods => GetType() + private MethodInfo[] ExecutableMethods => GetType() .GetMethods() .Where(methodInfo => methodInfo.IsPublic) + .Where(methodInfo => methodInfo.GetCustomAttribute() is { }) .ToArray(); private IActionContext ActionContext => _actionContext ?? @@ -20,6 +22,6 @@ public partial class ActionBase private IWorld World => _world ?? throw new InvalidOperationException("State is not set."); - private bool Loaded => _args is null || _call is null || _name is null; + private bool Loaded => _args is { } && _call is { } && _name is { }; } } diff --git a/Libplanet.SDK.Action/Action/ActionBase.cs b/Libplanet.SDK.Action/Action/ActionBase.cs index 6a6ad7b05ac..0a13289111a 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.cs @@ -11,14 +11,14 @@ namespace Libplanet.SDK.Action public abstract partial class ActionBase : IAction { public IValue PlainValue => Dictionary.Empty - .Add("name", _name ?? throw new NullReferenceException()) - .Add("call", _call ?? "Execute") - .Add("args", _args ?? Null.Value); + .Add("type_id", _name ?? throw new NullReferenceException()) + .Add("call", _call ?? throw new NullReferenceException()) + .Add("args", _args ?? throw new NullReferenceException()); public void LoadPlainValue(IValue plainValue) { Dictionary dict = (Dictionary)plainValue; - _name = (Text)dict["name"]; + _name = (Text)dict["type_id"]; _call = (Text)dict["call"]; _args = dict["args"]; } @@ -33,7 +33,7 @@ public IWorld Execute(IActionContext context) _actionContext = context; _world = context.PreviousState; - MethodInfo? method = CallableMethods.FirstOrDefault(m => m.Name == _call) ?? + MethodInfo method = ExecutableMethods.FirstOrDefault(m => m.Name == _call) ?? throw new InvalidOperationException($"Method {_call} is not found."); object?[]? args = new object?[] { _args }; method.Invoke(this, args); diff --git a/Libplanet.SDK.Action/ActionAttribute.cs b/Libplanet.SDK.Action/ActionAttribute.cs deleted file mode 100644 index 73c5425ad6f..00000000000 --- a/Libplanet.SDK.Action/ActionAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using Bencodex.Types; - -namespace Libplanet.SDK.Action -{ - /// - /// Indicates that an action class (i.e., a class implementing - /// ) can be held by transactions and blocks. - /// It also gives an action class a for - /// serialization and deserialization. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class ActionTypeAttribute : Attribute - { - /// - /// Creates an with a given - /// . - /// - /// An action class's unique - /// identifier for serialization and deserialization. - public ActionTypeAttribute(string typeIdentifier) - { - TypeIdentifier = new Text(typeIdentifier); - } - - public ActionTypeAttribute(int typeIdentifier) - { - TypeIdentifier = new Integer(typeIdentifier); - } - - /// - /// An action class's unique identifier for serialization and - /// deserialization. - /// - public IValue TypeIdentifier { get; } - } -} diff --git a/Libplanet.SDK.Action/Attributes/CallableAttribute.cs b/Libplanet.SDK.Action/Attributes/CallableAttribute.cs new file mode 100644 index 00000000000..8d0a925defe --- /dev/null +++ b/Libplanet.SDK.Action/Attributes/CallableAttribute.cs @@ -0,0 +1,7 @@ +namespace Libplanet.SDK.Action.Attributes +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class CallableAttribute : Attribute + { + } +} diff --git a/Libplanet.SDK.Action/Attributes/ExecutableAttribute.cs b/Libplanet.SDK.Action/Attributes/ExecutableAttribute.cs new file mode 100644 index 00000000000..bbf1b0a5842 --- /dev/null +++ b/Libplanet.SDK.Action/Attributes/ExecutableAttribute.cs @@ -0,0 +1,7 @@ +namespace Libplanet.SDK.Action.Attributes +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class ExecutableAttribute : Attribute + { + } +} diff --git a/Libplanet.SDK.Action/Interfaces/ICallable.cs b/Libplanet.SDK.Action/Interfaces/ICallable.cs deleted file mode 100644 index 21776fa7c31..00000000000 --- a/Libplanet.SDK.Action/Interfaces/ICallable.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Libplanet.SDK.Interfaces; - -public interface ICallable -{ -} diff --git a/src/Libplanet.Action/AssemblyInfo.cs b/src/Libplanet.Action/AssemblyInfo.cs index 3b64ad1989d..10e9dd66f57 100644 --- a/src/Libplanet.Action/AssemblyInfo.cs +++ b/src/Libplanet.Action/AssemblyInfo.cs @@ -4,3 +4,4 @@ [assembly: InternalsVisibleTo("Libplanet.Action.Tests")] [assembly: InternalsVisibleTo("Libplanet.Explorer.Tests")] [assembly: InternalsVisibleTo("Libplanet.Mocks")] +[assembly: InternalsVisibleTo("Libplanet.SDK.Action.Tests")] From 30d224a2ef86aa61f9f4211867feda815935a29c Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 9 Aug 2024 07:32:22 +0900 Subject: [PATCH 05/15] Added GeneratePlainValue() method --- .../Sample/Actions/InvalidAction.cs | 31 ++++++++++ .../Sample/Actions/NumberAction.cs | 6 ++ .../Sample/Actions/TextAction.cs | 2 +- .../Sample/SampleActionsTest.cs | 46 +++++++++++++-- Libplanet.SDK.Action/Action/ActionBase.API.cs | 56 +++++++++++++++---- 5 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 Libplanet.SDK.Action.Tests/Sample/Actions/InvalidAction.cs diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/InvalidAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/InvalidAction.cs new file mode 100644 index 00000000000..a19714bb3ad --- /dev/null +++ b/Libplanet.SDK.Action.Tests/Sample/Actions/InvalidAction.cs @@ -0,0 +1,31 @@ +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; + +namespace Libplanet.SDK.Action.Tests.Sample.Actions +{ + public class InvalidAction : ActionBase + { + public override Address StorageAddress => + new Address("0x1000000000000000000000000000000000000000"); + + [Executable] + public void Add(IValue args) + { + Integer operand = (Integer)args; + Integer stored = GetState(Signer) is IValue value + ? (Integer)value + : new Integer(0); + SetState(Signer, new Integer(stored.Value + operand.Value)); + } + + public void Subtract(IValue args) + { + Integer operand = (Integer)args; + Integer stored = GetState(Signer) is IValue value + ? (Integer)value + : new Integer(0); + SetState(Signer, new Integer(stored.Value - operand.Value)); + } + } +} diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs index 1fba97e2b29..73560d47792 100644 --- a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs +++ b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs @@ -43,5 +43,11 @@ public void Multiply(IValue args) Call(nameof(NumberLogAction.Multiply), new object?[] { operand }); SetState(Signer, new Integer(stored.Value * operand.Value)); } + + // Just some random public method for testing. + public void DoNothing() + { + return; + } } } diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs index ef47792d53c..a784ac91378 100644 --- a/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs +++ b/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs @@ -3,7 +3,7 @@ using Libplanet.Crypto; using Libplanet.SDK.Action.Attributes; -namespace Libplanet.SDK.Action.Tests +namespace Libplanet.SDK.Action.Tests.Sample.Actions { [ActionType("Text")] public class TextAction : ActionBase diff --git a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs index 50d3a9fdb4e..1349b9900c9 100644 --- a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs +++ b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs @@ -5,6 +5,7 @@ using Libplanet.Action.Loader; using Libplanet.Action.State; using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; using Libplanet.SDK.Action.Tests.Sample.Actions; using Libplanet.Store; using Libplanet.Store.Trie; @@ -109,23 +110,23 @@ public void TextAppend(bool commit) [Fact] public void InvalidPlainValueForLoading() { - IValue plainValue = Dictionary.Empty // Invalid type_id + IValue plainValue = Dictionary.Empty // Invalid type_id .Add("type_id", "Run") .Add("call", "Append") .Add("args", "Hello"); Assert.Throws(() => _loader.LoadAction(0, plainValue)); - plainValue = Dictionary.Empty // Missing type_id + plainValue = Dictionary.Empty // Missing type_id .Add("call", "Append") .Add("args", "Hello"); Assert.Throws(() => _loader.LoadAction(0, plainValue)); - plainValue = Dictionary.Empty // Missing call + plainValue = Dictionary.Empty // Missing call .Add("type_id", "Number") .Add("args", 5); Assert.Throws(() => _loader.LoadAction(0, plainValue)); - plainValue = Dictionary.Empty // Missing args + plainValue = Dictionary.Empty // Missing args .Add("type_id", "Number") .Add("call", "Add"); Assert.Throws(() => _loader.LoadAction(0, plainValue)); @@ -134,7 +135,7 @@ public void InvalidPlainValueForLoading() [Fact] public void InvalidPlainValueForExecution() { - IValue plainValue = Dictionary.Empty // Invalid call + IValue plainValue = Dictionary.Empty // Invalid call .Add("type_id", "Number") .Add("call", "Divide") .Add("args", 5); @@ -143,7 +144,7 @@ public void InvalidPlainValueForExecution() Assert.Throws(() => action.Execute(new MockActionContext(address, address, _world))); - plainValue = Dictionary.Empty // Invalid args + plainValue = Dictionary.Empty // Invalid args .Add("type_id", "Number") .Add("call", "Add") .Add("args", "Hello"); @@ -170,5 +171,38 @@ public void CallableAttributeIsRequired() action.Execute(new MockActionContext(signer, signer, world))) .InnerException); } + + [Fact] + public void GeneratePlainValue() + { + IValue expected = Dictionary.Empty + .Add("type_id", "Number") + .Add("call", "Add") + .Add("args", 5); + IValue generated = ActionBase.GeneratePlainValue( + "Add", new Integer(5)); + Assert.Equal(expected, generated); + + expected = Dictionary.Empty + .Add("type_id", "Text") + .Add("call", "Append") + .Add("args", "Hello"); + generated = ActionBase.GeneratePlainValue( + "Append", new Text("Hello")); + Assert.Equal(expected, generated); + + Assert.Contains( + $"{nameof(ActionTypeAttribute)}", + Assert.Throws(() => + ActionBase.GeneratePlainValue("Add", new Integer(5))).Message); + Assert.Contains( + $"cannot be found", + Assert.Throws(() => + ActionBase.GeneratePlainValue("Divide", new Integer(5))).Message); + Assert.Contains( + $"{nameof(ExecutableAttribute)}", + Assert.Throws(() => + ActionBase.GeneratePlainValue("DoNothing", new Integer(5))).Message); + } } } diff --git a/Libplanet.SDK.Action/Action/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase.API.cs index be06a6b4162..7602ebce771 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.API.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -1,5 +1,6 @@ using System.Reflection; using Bencodex.Types; +using Libplanet.Action; using Libplanet.Crypto; using Libplanet.SDK.Action.Attributes; @@ -7,6 +8,30 @@ namespace Libplanet.SDK.Action { public partial class ActionBase { + public static IValue GeneratePlainValue(string methodName, IValue args) + where T : ActionBase + { + ActionTypeAttribute actionType = typeof(T).GetCustomAttribute() ?? + throw new ArgumentException( + $"Type is missing a {nameof(ActionTypeAttribute)}."); + + MethodInfo methodInfo = typeof(T).GetMethod(methodName) ?? + throw new ArgumentException( + $"Method named {methodName} cannot be found for {typeof(T)}.", + nameof(methodName)); + if (methodInfo.GetCustomAttribute() is null) + { + throw new ArgumentException( + $"Target method is missing a {nameof(ExecutableAttribute)}.", + nameof(methodName)); + } + + return Dictionary.Empty + .Add("type_id", actionType.TypeIdentifier) + .Add("call", methodName) + .Add("args", args); + } + protected IValue? GetState(Address address) => World.GetAccount(StorageAddress).GetState(address); @@ -28,11 +53,12 @@ protected void Call(string methodName, object?[]? args = null) calledAction.LoadContext(ActionContext, World); MethodInfo methodInfo = typeof(T).GetMethod(methodName) ?? - throw new Exception("Method cannot be found."); + throw new ArgumentException( + $"Method named {methodName} cannot be found."); if (methodInfo.GetCustomAttribute() is null) { - throw new Exception( - $"Target method is missing a {nameof(CallableAttribute)}"); + throw new ArgumentException( + $"Target method {methodName} is missing a {nameof(CallableAttribute)}"); } methodInfo.Invoke(calledAction, args); @@ -41,28 +67,36 @@ protected void Call(string methodName, object?[]? args = null) _actionContext = calledAction._actionContext; } - protected U Call(string methodName, object?[]? args = null) - where T : ActionBase + protected TR Call(string methodName, object?[]? args = null) + where TA : ActionBase { - if (Activator.CreateInstance(typeof(T)) is not T calledAction) + if (Activator.CreateInstance(typeof(TA)) is not TA calledAction) { throw new Exception("Action cannot be found."); } calledAction.LoadContext(ActionContext, World); - MethodInfo methodInfo = typeof(T).GetMethod(methodName) ?? - throw new Exception("Method cannot be found."); + MethodInfo methodInfo = typeof(TA).GetMethod(methodName) ?? + throw new ArgumentException( + $"Method named {methodName} cannot be found."); + if (methodInfo.GetCustomAttribute() is null) + { + throw new ArgumentException( + $"Target method {methodName} is missing a {nameof(CallableAttribute)}"); + } - if (methodInfo.Invoke(calledAction, args) is not U result) + var result = methodInfo.Invoke(calledAction, args); + if (result is not TR typedResult) { - throw new Exception("Return type doesn't match."); + throw new Exception( + $"Return type is expected to be {typeof(TR)}: {result?.GetType()}"); } _world = calledAction._world; _actionContext = calledAction._actionContext; - return result; + return typedResult; } } } From 82148088fd93f8a82b01714fbc378796912673fc Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 9 Aug 2024 08:03:38 +0900 Subject: [PATCH 06/15] Added a new set of test actions --- .../SimpleRPG/Actions/AvatarAction.cs | 46 +++++++ .../SimpleRPG/Actions/FarmAction.cs | 30 +++++ .../SimpleRPG/Actions/InfoAction.cs | 33 +++++ .../SimpleRPG/Actions/InventoryAction.cs | 33 +++++ .../SimpleRPG/Models/Avatar.cs | 14 +++ .../SimpleRPG/Models/Info.cs | 38 ++++++ .../SimpleRPG/Models/Inventory.cs | 31 +++++ .../SimpleRPG/SimpleRPGActionsTest.cs | 118 ++++++++++++++++++ 8 files changed, 343 insertions(+) create mode 100644 Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleRPG/Actions/InfoAction.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleRPG/Actions/InventoryAction.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleRPG/Models/Avatar.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleRPG/Models/Info.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleRPG/Models/Inventory.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs new file mode 100644 index 00000000000..736b443928b --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs @@ -0,0 +1,46 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; +using Libplanet.SDK.Action.Tests.SimpleRPG.Models; + +namespace Libplanet.SDK.Action.Tests.SimpleRPG.Actions +{ + [ActionType("Avatar")] + public class AvatarAction : ActionBase + { + // This has no IAccount associated with its domain. + public override Address StorageAddress => default; + + [Executable] + public void Create(IValue args) + { + string name = (Text)args; + Call("Create", new object?[] { name }); + Call("Create"); + } + + [Callable] + public Avatar GetAvatar(Address address) + { + Info info = Call( + "GetInfo", + new object?[] { address }); + Inventory inventory = Call( + "GetInventory", + new object?[] { address }); + return new Avatar(info, inventory); + } + + [Callable] + public void SetAvatar(Address address, Avatar avatar) + { + Call( + "SetInfo", + new object?[] { address, avatar.Info }); + Call( + "SetInventory", + new object?[] { address, avatar.Inventory }); + } + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs new file mode 100644 index 00000000000..b4a07532bc0 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs @@ -0,0 +1,30 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; +using Libplanet.SDK.Action.Tests.SimpleRPG.Models; + +namespace Libplanet.SDK.Action.Tests.SimpleRPG.Actions +{ + [ActionType("Farm")] + public class FarmAction : ActionBase + { + public const int ExpPerFarm = 10; + public const int GoldPerFarm = 20; + + // This has no IAccount associated with its domain. + public override Address StorageAddress => default; + + [Executable] + public void Farm(IValue args) + { + // Simple type checking. + _ = (Null)args; + + Avatar avatar = Call("GetAvatar", new object?[] { Signer }); + avatar.Info.AddExp(ExpPerFarm); + avatar.Inventory.AddGold(GoldPerFarm); + Call("SetAvatar", new object?[] { Signer, avatar }); + } + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/InfoAction.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/InfoAction.cs new file mode 100644 index 00000000000..b308f1373aa --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/InfoAction.cs @@ -0,0 +1,33 @@ +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; +using Libplanet.SDK.Action.Tests.SimpleRPG.Models; + +namespace Libplanet.SDK.Action.Tests.SimpleRPG.Actions +{ + public class InfoAction : ActionBase + { + public override Address StorageAddress => + new Address("0x1000000000000000000000000000000000000001"); + + [Callable] + public Info Create(string name) + { + if (GetState(Signer) is { } value) + { + throw new InvalidOperationException("Info already exists."); + } + + Info info = new Info(name, 0); + SetInfo(Signer, info); + return info; + } + + [Callable] + public Info GetInfo(Address address) => + new Info(GetState(address) ?? throw new NullReferenceException()); + + [Callable] + public void SetInfo(Address address, Info info) => + SetState(address, info.Serialized); + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/InventoryAction.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/InventoryAction.cs new file mode 100644 index 00000000000..788ab3c11a7 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/InventoryAction.cs @@ -0,0 +1,33 @@ +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; +using Libplanet.SDK.Action.Tests.SimpleRPG.Models; + +namespace Libplanet.SDK.Action.Tests.SimpleRPG.Actions +{ + public class InventoryAction : ActionBase + { + public override Address StorageAddress => + new Address("0x1000000000000000000000000000000000000002"); + + [Callable] + public Inventory Create() + { + if (GetState(Signer) is { }) + { + throw new InvalidOperationException("Inventory already exists."); + } + + Inventory inventory = new Inventory(); + SetInventory(Signer, inventory); + return inventory; + } + + [Callable] + public Inventory GetInventory(Address address) => + new Inventory(GetState(address) ?? throw new NullReferenceException()); + + [Callable] + public void SetInventory(Address address, Inventory inventory) => + SetState(address, inventory.Serialized); + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Models/Avatar.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Models/Avatar.cs new file mode 100644 index 00000000000..e43a65ef4d7 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Models/Avatar.cs @@ -0,0 +1,14 @@ +namespace Libplanet.SDK.Action.Tests.SimpleRPG.Models +{ + public class Avatar + { + public Info Info { get; } + public Inventory Inventory { get; } + + public Avatar(Info info, Inventory inventory) + { + Info = info; + Inventory = inventory; + } + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Models/Info.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Models/Info.cs new file mode 100644 index 00000000000..6f880a36609 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Models/Info.cs @@ -0,0 +1,38 @@ +using Bencodex.Types; + +namespace Libplanet.SDK.Action.Tests.SimpleRPG.Models +{ + public class Info + { + public string Name { get; } + + public int Exp { get; private set; } + + public int Level => (Exp / 100); + + public Info(string name) + : this(name, 0) + { + } + + public Info(IValue value) + : this((Text)((List)value)[0], (Integer)((List)value)[1]) + { + } + + public Info(string name, int exp) + { + Name = name; + Exp = exp; + } + + public IValue Serialized => List.Empty + .Add(Name) + .Add(Exp); + + public void AddExp(int exp) + { + Exp = Exp + exp; + } + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Models/Inventory.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Models/Inventory.cs new file mode 100644 index 00000000000..4ad64a15560 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Models/Inventory.cs @@ -0,0 +1,31 @@ +using Bencodex.Types; + +namespace Libplanet.SDK.Action.Tests.SimpleRPG.Models +{ + public class Inventory + { + public int Gold { get; private set; } + + public Inventory() + : this(0) + { + } + + public Inventory(IValue value) + : this((int)(Integer)value) + { + } + + public Inventory(int gold) + { + Gold = gold; + } + + public IValue Serialized => new Integer(Gold); + + public void AddGold(int gold) + { + Gold = Gold + gold; + } + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs new file mode 100644 index 00000000000..98447467214 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs @@ -0,0 +1,118 @@ +using System.Collections.Immutable; +using System.Reflection; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Tests.SimpleRPG.Actions; +using Libplanet.SDK.Action.Tests.SimpleRPG.Models; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Libplanet.Types.Blocks; +using Xunit; + +namespace Libplanet.SDK.Action.Tests.Sample +{ + public class SimpleRPGActionsTest + { + private TypedActionLoader _loader; + private IStateStore _stateStore; + private IWorld _world; + + public SimpleRPGActionsTest() + { + _loader = new TypedActionLoader( + ImmutableDictionary.Empty + .Add(new Text("Avatar"), typeof(AvatarAction)) + .Add(new Text("Farm"), typeof(FarmAction))); + + _stateStore = new TrieStateStore(new MemoryKeyValueStore()); + + ITrie trie = _stateStore.GetStateRoot(null); + trie = trie.SetMetadata(new TrieMetadata(Block.CurrentProtocolVersion)); + trie = _stateStore.Commit(trie); + _world = new World(new WorldBaseState(trie, _stateStore)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Scenario(bool commit) + { + IValue plainValue = Dictionary.Empty + .Add("type_id", "Avatar") + .Add("call", "Create") + .Add("args", "Hero"); + IAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); + Address signer = new PrivateKey().Address; + IWorld world = _world; + + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Info("Hero").Serialized, + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000001")) + .GetState(signer)); + Assert.Equal( + new Inventory().Serialized, + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + + const int repeat = 3; + foreach (var _ in Enumerable.Range(0, repeat)) + { + plainValue = Dictionary.Empty + .Add("type_id", "Farm") + .Add("call", "Farm") + .Add("args", Null.Value); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + } + + Assert.Equal( + new Info("Hero", FarmAction.ExpPerFarm * repeat).Serialized, + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000001")) + .GetState(signer)); + Assert.Equal( + new Inventory(FarmAction.GoldPerFarm * repeat).Serialized, + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + } + + [Fact] + public void CannotCreateTwice() + { + IValue plainValue = Dictionary.Empty + .Add("type_id", "Avatar") + .Add("call", "Create") + .Add("args", "Hero"); + IAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); + Address signer = new PrivateKey().Address; + IWorld world = _world; + + world = action.Execute(new MockActionContext(signer, signer, world)); + world = _stateStore.CommitWorld(world); + + plainValue = Dictionary.Empty + .Add("type_id", "Avatar") + .Add("call", "Create") + .Add("args", "Princess"); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + Assert.Contains( + "Info already exists", + Assert.IsType( + Assert.IsType( + Assert.Throws(() => + action.Execute(new MockActionContext(signer, signer, world))) + .InnerException) + .InnerException) + .Message); + } + } +} From 479e27831eff5e37c16fd7e3539674099f818519 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 9 Aug 2024 10:34:53 +0900 Subject: [PATCH 07/15] Changed naming --- .../Sample/SampleActionsTest.cs | 24 +++++++++---------- .../SimpleRPG/SimpleRPGActionsTest.cs | 8 +++---- Libplanet.SDK.Action/Action/ActionBase.API.cs | 2 +- .../Action/ActionBase.Fields.cs | 2 +- .../Action/ActionBase.Params.cs | 2 +- Libplanet.SDK.Action/Action/ActionBase.cs | 8 +++---- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs index 1349b9900c9..291948bbeab 100644 --- a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs +++ b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs @@ -42,7 +42,7 @@ public void NumberAddAndSubtract(bool commit) { IValue plainValue = Dictionary.Empty .Add("type_id", "Number") - .Add("call", "Add") + .Add("exec", "Add") .Add("args", 5); NumberAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address signer = new PrivateKey().Address; @@ -61,7 +61,7 @@ public void NumberAddAndSubtract(bool commit) plainValue = Dictionary.Empty .Add("type_id", "Number") - .Add("call", "Subtract") + .Add("exec", "Subtract") .Add("args", 8); action = Assert.IsType(_loader.LoadAction(0, plainValue)); world = action.Execute(new MockActionContext(signer, signer, world)); @@ -83,7 +83,7 @@ public void TextAppend(bool commit) { IValue plainValue = Dictionary.Empty .Add("type_id", "Text") - .Add("call", "Append") + .Add("exec", "Append") .Add("args", "Hello"); TextAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address signer = new PrivateKey().Address; @@ -97,7 +97,7 @@ public void TextAppend(bool commit) plainValue = Dictionary.Empty .Add("type_id", "Text") - .Add("call", "Append") + .Add("exec", "Append") .Add("args", " world"); action = Assert.IsType(_loader.LoadAction(0, plainValue)); world = action.Execute(new MockActionContext(signer, signer, world)); @@ -112,12 +112,12 @@ public void InvalidPlainValueForLoading() { IValue plainValue = Dictionary.Empty // Invalid type_id .Add("type_id", "Run") - .Add("call", "Append") + .Add("exec", "Append") .Add("args", "Hello"); Assert.Throws(() => _loader.LoadAction(0, plainValue)); plainValue = Dictionary.Empty // Missing type_id - .Add("call", "Append") + .Add("exec", "Append") .Add("args", "Hello"); Assert.Throws(() => _loader.LoadAction(0, plainValue)); @@ -128,7 +128,7 @@ public void InvalidPlainValueForLoading() plainValue = Dictionary.Empty // Missing args .Add("type_id", "Number") - .Add("call", "Add"); + .Add("exec", "Add"); Assert.Throws(() => _loader.LoadAction(0, plainValue)); } @@ -137,7 +137,7 @@ public void InvalidPlainValueForExecution() { IValue plainValue = Dictionary.Empty // Invalid call .Add("type_id", "Number") - .Add("call", "Divide") + .Add("exec", "Divide") .Add("args", 5); IAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address address = new PrivateKey().Address; @@ -146,7 +146,7 @@ public void InvalidPlainValueForExecution() plainValue = Dictionary.Empty // Invalid args .Add("type_id", "Number") - .Add("call", "Add") + .Add("exec", "Add") .Add("args", "Hello"); action = Assert.IsType(_loader.LoadAction(0, plainValue)); Assert.IsType( @@ -160,7 +160,7 @@ public void CallableAttributeIsRequired() { IValue plainValue = Dictionary.Empty .Add("type_id", "Number") - .Add("call", "Multiply") + .Add("exec", "Multiply") .Add("args", 5); NumberAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address signer = new PrivateKey().Address; @@ -177,7 +177,7 @@ public void GeneratePlainValue() { IValue expected = Dictionary.Empty .Add("type_id", "Number") - .Add("call", "Add") + .Add("exec", "Add") .Add("args", 5); IValue generated = ActionBase.GeneratePlainValue( "Add", new Integer(5)); @@ -185,7 +185,7 @@ public void GeneratePlainValue() expected = Dictionary.Empty .Add("type_id", "Text") - .Add("call", "Append") + .Add("exec", "Append") .Add("args", "Hello"); generated = ActionBase.GeneratePlainValue( "Append", new Text("Hello")); diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs index 98447467214..e2a229ae6e8 100644 --- a/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs @@ -42,7 +42,7 @@ public void Scenario(bool commit) { IValue plainValue = Dictionary.Empty .Add("type_id", "Avatar") - .Add("call", "Create") + .Add("exec", "Create") .Add("args", "Hero"); IAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address signer = new PrivateKey().Address; @@ -66,7 +66,7 @@ public void Scenario(bool commit) { plainValue = Dictionary.Empty .Add("type_id", "Farm") - .Add("call", "Farm") + .Add("exec", "Farm") .Add("args", Null.Value); action = Assert.IsType(_loader.LoadAction(0, plainValue)); world = action.Execute(new MockActionContext(signer, signer, world)); @@ -90,7 +90,7 @@ public void CannotCreateTwice() { IValue plainValue = Dictionary.Empty .Add("type_id", "Avatar") - .Add("call", "Create") + .Add("exec", "Create") .Add("args", "Hero"); IAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address signer = new PrivateKey().Address; @@ -101,7 +101,7 @@ public void CannotCreateTwice() plainValue = Dictionary.Empty .Add("type_id", "Avatar") - .Add("call", "Create") + .Add("exec", "Create") .Add("args", "Princess"); action = Assert.IsType(_loader.LoadAction(0, plainValue)); Assert.Contains( diff --git a/Libplanet.SDK.Action/Action/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase.API.cs index 7602ebce771..6fafe69b4ae 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.API.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -28,7 +28,7 @@ public static IValue GeneratePlainValue(string methodName, IValue args) return Dictionary.Empty .Add("type_id", actionType.TypeIdentifier) - .Add("call", methodName) + .Add("execute", methodName) .Add("args", args); } diff --git a/Libplanet.SDK.Action/Action/ActionBase.Fields.cs b/Libplanet.SDK.Action/Action/ActionBase.Fields.cs index e0ffd28c28b..c3cbcf8a234 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.Fields.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.Fields.cs @@ -9,7 +9,7 @@ public partial class ActionBase { private string? _name = null; private IValue? _args = null; - private string? _call = null; + private string? _exec = null; [SecurityCritical] private IActionContext? _actionContext = null; diff --git a/Libplanet.SDK.Action/Action/ActionBase.Params.cs b/Libplanet.SDK.Action/Action/ActionBase.Params.cs index d8b5c29e9c7..ee57e57aea8 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.Params.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.Params.cs @@ -22,6 +22,6 @@ public partial class ActionBase private IWorld World => _world ?? throw new InvalidOperationException("State is not set."); - private bool Loaded => _args is { } && _call is { } && _name is { }; + private bool Loaded => _args is { } && _exec is { } && _name is { }; } } diff --git a/Libplanet.SDK.Action/Action/ActionBase.cs b/Libplanet.SDK.Action/Action/ActionBase.cs index 0a13289111a..caf32ea1bda 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.cs @@ -12,14 +12,14 @@ public abstract partial class ActionBase : IAction { public IValue PlainValue => Dictionary.Empty .Add("type_id", _name ?? throw new NullReferenceException()) - .Add("call", _call ?? throw new NullReferenceException()) + .Add("exec", _exec ?? throw new NullReferenceException()) .Add("args", _args ?? throw new NullReferenceException()); public void LoadPlainValue(IValue plainValue) { Dictionary dict = (Dictionary)plainValue; _name = (Text)dict["type_id"]; - _call = (Text)dict["call"]; + _exec = (Text)dict["exec"]; _args = dict["args"]; } @@ -33,8 +33,8 @@ public IWorld Execute(IActionContext context) _actionContext = context; _world = context.PreviousState; - MethodInfo method = ExecutableMethods.FirstOrDefault(m => m.Name == _call) ?? - throw new InvalidOperationException($"Method {_call} is not found."); + MethodInfo method = ExecutableMethods.FirstOrDefault(m => m.Name == _exec) ?? + throw new InvalidOperationException($"Method {_exec} is not found."); object?[]? args = new object?[] { _args }; method.Invoke(this, args); From c78e97ecd681d56055715bfecf786a821d4d48ad Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 9 Aug 2024 17:47:18 +0900 Subject: [PATCH 08/15] Removed const strings --- .../SimpleRPG/Actions/AvatarAction.cs | 12 ++++++------ .../SimpleRPG/Actions/FarmAction.cs | 8 ++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs index 736b443928b..300d7417acc 100644 --- a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs @@ -16,18 +16,18 @@ public class AvatarAction : ActionBase public void Create(IValue args) { string name = (Text)args; - Call("Create", new object?[] { name }); - Call("Create"); + Call(nameof(InfoAction.Create), new object?[] { name }); + Call(nameof(InventoryAction.Create)); } [Callable] public Avatar GetAvatar(Address address) { Info info = Call( - "GetInfo", + nameof(InfoAction.GetInfo), new object?[] { address }); Inventory inventory = Call( - "GetInventory", + nameof(InventoryAction.GetInventory), new object?[] { address }); return new Avatar(info, inventory); } @@ -36,10 +36,10 @@ public Avatar GetAvatar(Address address) public void SetAvatar(Address address, Avatar avatar) { Call( - "SetInfo", + nameof(InfoAction.SetInfo), new object?[] { address, avatar.Info }); Call( - "SetInventory", + nameof(InventoryAction.SetInventory), new object?[] { address, avatar.Inventory }); } } diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs index b4a07532bc0..07bbddf29bf 100644 --- a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs @@ -21,10 +21,14 @@ public void Farm(IValue args) // Simple type checking. _ = (Null)args; - Avatar avatar = Call("GetAvatar", new object?[] { Signer }); + Avatar avatar = Call( + nameof(AvatarAction.GetAvatar), + new object?[] { Signer }); avatar.Info.AddExp(ExpPerFarm); avatar.Inventory.AddGold(GoldPerFarm); - Call("SetAvatar", new object?[] { Signer, avatar }); + Call( + nameof(AvatarAction.SetAvatar), + new object?[] { Signer, avatar }); } } } From 225cd81561baefb7d29565c129fab8ba2861f38d Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Mon, 9 Sep 2024 15:19:58 +0900 Subject: [PATCH 09/15] Changed plain value scheme --- .../Sample/Actions/NumberAction.cs | 9 ++--- .../Sample/Actions/TextAction.cs | 3 +- .../Sample/SampleActionsTest.cs | 37 ++++++++++--------- .../SimpleRPG/Actions/AvatarAction.cs | 4 +- .../SimpleRPG/Actions/FarmAction.cs | 5 +-- .../SimpleRPG/SimpleRPGActionsTest.cs | 8 ++-- Libplanet.SDK.Action/Action/ActionBase.API.cs | 29 +++++++++++++-- Libplanet.SDK.Action/Action/ActionBase.cs | 24 +++++++++++- 8 files changed, 80 insertions(+), 39 deletions(-) diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs index 73560d47792..d660d91fa16 100644 --- a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs +++ b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs @@ -12,9 +12,8 @@ public class NumberAction : ActionBase new Address("0x1000000000000000000000000000000000000001"); [Executable] - public void Add(IValue args) + public void Add(Integer operand) { - Integer operand = (Integer)args; Integer stored = GetState(Signer) is IValue value ? (Integer)value : new Integer(0); @@ -23,9 +22,8 @@ public void Add(IValue args) } [Executable] - public void Subtract(IValue args) + public void Subtract(Integer operand) { - Integer operand = (Integer)args; Integer stored = GetState(Signer) is IValue value ? (Integer)value : new Integer(0); @@ -34,9 +32,8 @@ public void Subtract(IValue args) } [Executable] - public void Multiply(IValue args) + public void Multiply(Integer operand) { - Integer operand = (Integer)args; Integer stored = GetState(Signer) is IValue value ? (Integer)value : new Integer(1); diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs index a784ac91378..fa16f3c5a5d 100644 --- a/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs +++ b/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs @@ -12,9 +12,8 @@ public class TextAction : ActionBase new Address("0x1000000000000000000000000000000000000003"); [Executable] - public void Append(IValue args) + public void Append(Text operand) { - Text operand = (Text)args; Text stored = GetState(Signer) is IValue value ? (Text)value : new Text(string.Empty); diff --git a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs index 291948bbeab..fe751348eff 100644 --- a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs +++ b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs @@ -43,7 +43,7 @@ public void NumberAddAndSubtract(bool commit) IValue plainValue = Dictionary.Empty .Add("type_id", "Number") .Add("exec", "Add") - .Add("args", 5); + .Add("args", List.Empty.Add(5)); NumberAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address signer = new PrivateKey().Address; IWorld world = _world; @@ -62,7 +62,7 @@ public void NumberAddAndSubtract(bool commit) plainValue = Dictionary.Empty .Add("type_id", "Number") .Add("exec", "Subtract") - .Add("args", 8); + .Add("args", List.Empty.Add(8)); action = Assert.IsType(_loader.LoadAction(0, plainValue)); world = action.Execute(new MockActionContext(signer, signer, world)); world = commit ? _stateStore.CommitWorld(world) : world; @@ -84,7 +84,7 @@ public void TextAppend(bool commit) IValue plainValue = Dictionary.Empty .Add("type_id", "Text") .Add("exec", "Append") - .Add("args", "Hello"); + .Add("args", List.Empty.Add("Hello")); TextAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address signer = new PrivateKey().Address; IWorld world = _world; @@ -98,7 +98,7 @@ public void TextAppend(bool commit) plainValue = Dictionary.Empty .Add("type_id", "Text") .Add("exec", "Append") - .Add("args", " world"); + .Add("args", List.Empty.Add(" world")); action = Assert.IsType(_loader.LoadAction(0, plainValue)); world = action.Execute(new MockActionContext(signer, signer, world)); world = commit ? _stateStore.CommitWorld(world) : world; @@ -113,17 +113,17 @@ public void InvalidPlainValueForLoading() IValue plainValue = Dictionary.Empty // Invalid type_id .Add("type_id", "Run") .Add("exec", "Append") - .Add("args", "Hello"); + .Add("args", List.Empty.Add("Hello")); Assert.Throws(() => _loader.LoadAction(0, plainValue)); plainValue = Dictionary.Empty // Missing type_id .Add("exec", "Append") - .Add("args", "Hello"); + .Add("args", List.Empty.Add("Hello")); Assert.Throws(() => _loader.LoadAction(0, plainValue)); plainValue = Dictionary.Empty // Missing call .Add("type_id", "Number") - .Add("args", 5); + .Add("args", List.Empty.Add(5)); Assert.Throws(() => _loader.LoadAction(0, plainValue)); plainValue = Dictionary.Empty // Missing args @@ -138,7 +138,7 @@ public void InvalidPlainValueForExecution() IValue plainValue = Dictionary.Empty // Invalid call .Add("type_id", "Number") .Add("exec", "Divide") - .Add("args", 5); + .Add("args", List.Empty.Add(5)); IAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address address = new PrivateKey().Address; Assert.Throws(() => @@ -147,7 +147,7 @@ public void InvalidPlainValueForExecution() plainValue = Dictionary.Empty // Invalid args .Add("type_id", "Number") .Add("exec", "Add") - .Add("args", "Hello"); + .Add("args", List.Empty.Add("Hello")); action = Assert.IsType(_loader.LoadAction(0, plainValue)); Assert.IsType( Assert.Throws(() => @@ -161,7 +161,7 @@ public void CallableAttributeIsRequired() IValue plainValue = Dictionary.Empty .Add("type_id", "Number") .Add("exec", "Multiply") - .Add("args", 5); + .Add("args", List.Empty.Add(5)); NumberAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address signer = new PrivateKey().Address; IWorld world = _world; @@ -178,31 +178,34 @@ public void GeneratePlainValue() IValue expected = Dictionary.Empty .Add("type_id", "Number") .Add("exec", "Add") - .Add("args", 5); + .Add("args", List.Empty.Add(5)); IValue generated = ActionBase.GeneratePlainValue( - "Add", new Integer(5)); + "Add", List.Empty.Add(new Integer(5))); Assert.Equal(expected, generated); expected = Dictionary.Empty .Add("type_id", "Text") .Add("exec", "Append") - .Add("args", "Hello"); + .Add("args", List.Empty.Add("Hello")); generated = ActionBase.GeneratePlainValue( - "Append", new Text("Hello")); + "Append", List.Empty.Add(new Text("Hello"))); Assert.Equal(expected, generated); Assert.Contains( $"{nameof(ActionTypeAttribute)}", Assert.Throws(() => - ActionBase.GeneratePlainValue("Add", new Integer(5))).Message); + ActionBase.GeneratePlainValue( + "Add", List.Empty.Add(new Integer(5)))).Message); Assert.Contains( $"cannot be found", Assert.Throws(() => - ActionBase.GeneratePlainValue("Divide", new Integer(5))).Message); + ActionBase.GeneratePlainValue( + "Divide", List.Empty.Add(new Integer(5)))).Message); Assert.Contains( $"{nameof(ExecutableAttribute)}", Assert.Throws(() => - ActionBase.GeneratePlainValue("DoNothing", new Integer(5))).Message); + ActionBase.GeneratePlainValue( + "DoNothing", List.Empty.Add(new Integer(5)))).Message); } } } diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs index 300d7417acc..c829d5db068 100644 --- a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs @@ -13,9 +13,9 @@ public class AvatarAction : ActionBase public override Address StorageAddress => default; [Executable] - public void Create(IValue args) + public void Create(Text args) { - string name = (Text)args; + string name = args; Call(nameof(InfoAction.Create), new object?[] { name }); Call(nameof(InventoryAction.Create)); } diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs index 07bbddf29bf..a03e90b04b4 100644 --- a/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs @@ -16,11 +16,8 @@ public class FarmAction : ActionBase public override Address StorageAddress => default; [Executable] - public void Farm(IValue args) + public void Farm() { - // Simple type checking. - _ = (Null)args; - Avatar avatar = Call( nameof(AvatarAction.GetAvatar), new object?[] { Signer }); diff --git a/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs index e2a229ae6e8..ed5d91b33bc 100644 --- a/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/SimpleRPGActionsTest.cs @@ -43,7 +43,7 @@ public void Scenario(bool commit) IValue plainValue = Dictionary.Empty .Add("type_id", "Avatar") .Add("exec", "Create") - .Add("args", "Hero"); + .Add("args", List.Empty.Add("Hero")); IAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address signer = new PrivateKey().Address; IWorld world = _world; @@ -67,7 +67,7 @@ public void Scenario(bool commit) plainValue = Dictionary.Empty .Add("type_id", "Farm") .Add("exec", "Farm") - .Add("args", Null.Value); + .Add("args", List.Empty); action = Assert.IsType(_loader.LoadAction(0, plainValue)); world = action.Execute(new MockActionContext(signer, signer, world)); world = commit ? _stateStore.CommitWorld(world) : world; @@ -91,7 +91,7 @@ public void CannotCreateTwice() IValue plainValue = Dictionary.Empty .Add("type_id", "Avatar") .Add("exec", "Create") - .Add("args", "Hero"); + .Add("args", List.Empty.Add("Hero")); IAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); Address signer = new PrivateKey().Address; IWorld world = _world; @@ -102,7 +102,7 @@ public void CannotCreateTwice() plainValue = Dictionary.Empty .Add("type_id", "Avatar") .Add("exec", "Create") - .Add("args", "Princess"); + .Add("args", List.Empty.Add("Princess")); action = Assert.IsType(_loader.LoadAction(0, plainValue)); Assert.Contains( "Info already exists", diff --git a/Libplanet.SDK.Action/Action/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase.API.cs index 6fafe69b4ae..95ee2a6a0db 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.API.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -8,7 +8,7 @@ namespace Libplanet.SDK.Action { public partial class ActionBase { - public static IValue GeneratePlainValue(string methodName, IValue args) + public static IValue GeneratePlainValue(string methodName, List arguments) where T : ActionBase { ActionTypeAttribute actionType = typeof(T).GetCustomAttribute() ?? @@ -26,10 +26,33 @@ public static IValue GeneratePlainValue(string methodName, IValue args) nameof(methodName)); } + ParameterInfo[] paramInfos = methodInfo.GetParameters(); + if (paramInfos.Length != arguments.Count) + { + throw new ArgumentException( + $"The length of {nameof(arguments)} should be {paramInfos.Length}: " + + $"{arguments.Count}", + nameof(arguments)); + } + + foreach (((ParameterInfo paramInfo, IValue argument), int index) in + paramInfos.Zip(arguments).Select((pair, i) => (pair, i))) + { + Type expectedType = paramInfo.ParameterType; + Type actualType = argument.GetType(); + if (!paramInfo.ParameterType.Equals(argument.GetType())) + { + throw new ArgumentException( + $"The argument at {index} for given {nameof(arguments)} should be " + + $"{expectedType}: {actualType}", + nameof(arguments)); + } + } + return Dictionary.Empty .Add("type_id", actionType.TypeIdentifier) - .Add("execute", methodName) - .Add("args", args); + .Add("exec", methodName) + .Add("args", arguments); } protected IValue? GetState(Address address) diff --git a/Libplanet.SDK.Action/Action/ActionBase.cs b/Libplanet.SDK.Action/Action/ActionBase.cs index caf32ea1bda..beee0d36e26 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.cs @@ -35,10 +35,32 @@ public IWorld Execute(IActionContext context) MethodInfo method = ExecutableMethods.FirstOrDefault(m => m.Name == _exec) ?? throw new InvalidOperationException($"Method {_exec} is not found."); - object?[]? args = new object?[] { _args }; + ParameterInfo[] paramInfos = method.GetParameters(); + object[] args = GetArgs(paramInfos, _args); method.Invoke(this, args); return World; } + + private static object[] GetArgs(ParameterInfo[] paramInfos, IValue? args) + { + if (args is List list) + { + if (paramInfos.Length != list.Count) + { + throw new ArgumentException( + $"Given {nameof(args)} must be of " + + $"length {paramInfos.Length}: {list.Count}"); + } + + return list.ToArray(); + } + else + { + throw new ArgumentException( + $"Given {nameof(args)} must be of type {nameof(List)}: {args.GetType()}", + nameof(args)); + } + } } } From 05524166a69c3c23c950e7aa8e5d0f7719758d2b Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Tue, 10 Sep 2024 14:16:34 +0900 Subject: [PATCH 10/15] Fixed test --- Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs index fe751348eff..db7c9697c80 100644 --- a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs +++ b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs @@ -149,10 +149,8 @@ public void InvalidPlainValueForExecution() .Add("exec", "Add") .Add("args", List.Empty.Add("Hello")); action = Assert.IsType(_loader.LoadAction(0, plainValue)); - Assert.IsType( - Assert.Throws(() => - action.Execute(new MockActionContext(address, address, _world))) - .InnerException); + Assert.Throws(() => + action.Execute(new MockActionContext(address, address, _world))); } [Fact] @@ -166,7 +164,7 @@ public void CallableAttributeIsRequired() Address signer = new PrivateKey().Address; IWorld world = _world; - Assert.IsType( + Assert.IsType( Assert.Throws(() => action.Execute(new MockActionContext(signer, signer, world))) .InnerException); From 3ec8e059d0b12d91ee0faec0a139f79e73761f46 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Tue, 10 Sep 2024 15:58:23 +0900 Subject: [PATCH 11/15] Added plain value validation --- .../Sample/SampleActionsTest.cs | 65 ++++++++++++----- Libplanet.SDK.Action/Action/ActionBase.API.cs | 73 +++++++++++++++---- 2 files changed, 108 insertions(+), 30 deletions(-) diff --git a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs index db7c9697c80..0cb79a7fc4c 100644 --- a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs +++ b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs @@ -171,39 +171,70 @@ public void CallableAttributeIsRequired() } [Fact] - public void GeneratePlainValue() + public void ValidatePlainValue() { - IValue expected = Dictionary.Empty + IValue plainValue; + plainValue = Dictionary.Empty .Add("type_id", "Number") - .Add("exec", "Add") + .Add("exec", nameof(NumberAction.Add)) .Add("args", List.Empty.Add(5)); - IValue generated = ActionBase.GeneratePlainValue( - "Add", List.Empty.Add(new Integer(5))); - Assert.Equal(expected, generated); + ActionBase.ValidatePlainValue(plainValue); - expected = Dictionary.Empty + plainValue = Dictionary.Empty .Add("type_id", "Text") - .Add("exec", "Append") + .Add("exec", nameof(TextAction.Append)) .Add("args", List.Empty.Add("Hello")); - generated = ActionBase.GeneratePlainValue( - "Append", List.Empty.Add(new Text("Hello"))); - Assert.Equal(expected, generated); + ActionBase.ValidatePlainValue(plainValue); + // Missing type_id. + plainValue = Dictionary.Empty + .Add("type_id", "Invalid") + .Add("exec", nameof(TextAction.Append)) + .Add("args", List.Empty.Add("Hello")); Assert.Contains( $"{nameof(ActionTypeAttribute)}", Assert.Throws(() => - ActionBase.GeneratePlainValue( - "Add", List.Empty.Add(new Integer(5)))).Message); + ActionBase.ValidatePlainValue(plainValue)).Message); + + // Unknown exec. + plainValue = Dictionary.Empty + .Add("type_id", "Number") + .Add("exec", "Divide") + .Add("args", List.Empty.Add(5)); Assert.Contains( $"cannot be found", Assert.Throws(() => - ActionBase.GeneratePlainValue( - "Divide", List.Empty.Add(new Integer(5)))).Message); + ActionBase.ValidatePlainValue(plainValue)).Message); + + // Missing attribute. + plainValue = Dictionary.Empty + .Add("type_id", "Number") + .Add("exec", nameof(NumberAction.DoNothing)) + .Add("args", List.Empty); Assert.Contains( $"{nameof(ExecutableAttribute)}", Assert.Throws(() => - ActionBase.GeneratePlainValue( - "DoNothing", List.Empty.Add(new Integer(5)))).Message); + ActionBase.ValidatePlainValue(plainValue)).Message); + + // Invalid argument type. + plainValue = Dictionary.Empty + .Add("type_id", "Number") + .Add("exec", nameof(NumberAction.Add)) + .Add("args", List.Empty.Add("Hello")); + Assert.Contains( + $"argument at", + Assert.Throws(() => + ActionBase.ValidatePlainValue(plainValue)).Message); + + // Invalid arguments length. + plainValue = Dictionary.Empty + .Add("type_id", "Number") + .Add("exec", nameof(NumberAction.Add)) + .Add("args", List.Empty.Add(5).Add(6)); + Assert.Contains( + $"The length", + Assert.Throws(() => + ActionBase.ValidatePlainValue(plainValue)).Message); } } } diff --git a/Libplanet.SDK.Action/Action/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase.API.cs index 95ee2a6a0db..6c28991a1f6 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.API.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -8,17 +8,55 @@ namespace Libplanet.SDK.Action { public partial class ActionBase { - public static IValue GeneratePlainValue(string methodName, List arguments) + public static void ValidatePlainValue(IValue plainValue) where T : ActionBase { ActionTypeAttribute actionType = typeof(T).GetCustomAttribute() ?? throw new ArgumentException( $"Type is missing a {nameof(ActionTypeAttribute)}."); + // NOTE: Check type. + if (!(plainValue is Dictionary dict)) + { + throw new ArgumentException( + $"Given {nameof(plainValue)} must be of " + + $"type {nameof(Dictionary)}: {plainValue.GetType()}", + nameof(plainValue)); + } + + // NOTE: Check type id. + if (!dict.ContainsKey("type_id")) + { + throw new ArgumentException( + $"Given dictionary {nameof(plainValue)} must contain \"type_id\" key", + nameof(plainValue)); + } + + if (!actionType.TypeIdentifier.Equals(dict["type_id"])) + { + throw new ArgumentException( + $"The value of \"type_id\" for {nameof(plainValue)} does not match " + + $"the expected value {actionType.TypeIdentifier} specified " + + $"by {nameof(ActionTypeAttribute)}: {dict["type_id"]}"); + } + + // NODE: Check exec. + if (!dict.ContainsKey("exec")) + { + throw new ArgumentException( + $"Given dictionary {nameof(plainValue)} must contain \"exec\" key", + nameof(plainValue)); + } + + string methodName = dict["exec"] is Text methodNameText + ? methodNameText.Value + : throw new ArgumentException( + $"The value of \"exec\" for {nameof(plainValue)} is expected to be " + + $"of type {nameof(Text)}: {dict["exec"].GetType()}"); MethodInfo methodInfo = typeof(T).GetMethod(methodName) ?? throw new ArgumentException( $"Method named {methodName} cannot be found for {typeof(T)}.", - nameof(methodName)); + nameof(plainValue)); if (methodInfo.GetCustomAttribute() is null) { throw new ArgumentException( @@ -26,13 +64,27 @@ public static IValue GeneratePlainValue(string methodName, List arguments) nameof(methodName)); } + // NOTE: Check arguments. + if (!dict.ContainsKey("args")) + { + throw new ArgumentException( + $"Given dictionary {nameof(plainValue)} must contain \"args\" key", + nameof(plainValue)); + } + + List arguments = dict["args"] is List list + ? list + : throw new ArgumentException( + $"The value of \"args\" for {nameof(plainValue)} is expected to be " + + $"of type {nameof(Text)}: {dict["args"].GetType()}"); + ParameterInfo[] paramInfos = methodInfo.GetParameters(); if (paramInfos.Length != arguments.Count) { throw new ArgumentException( - $"The length of {nameof(arguments)} should be {paramInfos.Length}: " + - $"{arguments.Count}", - nameof(arguments)); + $"The length of \"args\" for {nameof(plainValue)} should be " + + $"{paramInfos.Length}: {arguments.Count}", + nameof(plainValue)); } foreach (((ParameterInfo paramInfo, IValue argument), int index) in @@ -43,16 +95,11 @@ public static IValue GeneratePlainValue(string methodName, List arguments) if (!paramInfo.ParameterType.Equals(argument.GetType())) { throw new ArgumentException( - $"The argument at {index} for given {nameof(arguments)} should be " + - $"{expectedType}: {actualType}", - nameof(arguments)); + $"The argument at {index} of \"args\" for {nameof(plainValue)} " + + $"should be {expectedType}: {actualType}", + nameof(plainValue)); } } - - return Dictionary.Empty - .Add("type_id", actionType.TypeIdentifier) - .Add("exec", methodName) - .Add("args", arguments); } protected IValue? GetState(Address address) From 427471b6ae30d0d53467831fb348c825e1c41523 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Tue, 10 Sep 2024 17:54:30 +0900 Subject: [PATCH 12/15] Added an initial implementation for schema generation --- .../Sample/Actions/NumberAction.cs | 12 +- .../Sample/SampleActionsTest.cs | 78 +++++++- Libplanet.SDK.Action/Action/ActionBase.API.cs | 186 ++++++++++++++++++ 3 files changed, 274 insertions(+), 2 deletions(-) diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs index d660d91fa16..08946cc1866 100644 --- a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs +++ b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs @@ -41,8 +41,18 @@ public void Multiply(Integer operand) SetState(Signer, new Integer(stored.Value * operand.Value)); } + [Executable] + public void Bump() + { + Integer stored = GetState(Signer) is IValue value + ? (Integer)value + : 0; + Call(nameof(NumberLogAction.Add), new object?[] { new Integer(1) }); + SetState(Signer, new Integer(stored.Value + 1)); + } + // Just some random public method for testing. - public void DoNothing() + public void NoAttribute() { return; } diff --git a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs index 0cb79a7fc4c..0c537797c0b 100644 --- a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs +++ b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs @@ -74,6 +74,22 @@ public void NumberAddAndSubtract(bool commit) world .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) .GetState(signer)); + + plainValue = Dictionary.Empty + .Add("type_id", "Number") + .Add("exec", "Bump") + .Add("args", List.Empty); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Integer(-2), + world.GetAccountState(action.StorageAddress).GetState(signer)); + Assert.Equal( + new Text("5 - 8 + 1"), + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); } [Theory] @@ -209,7 +225,7 @@ public void ValidatePlainValue() // Missing attribute. plainValue = Dictionary.Empty .Add("type_id", "Number") - .Add("exec", nameof(NumberAction.DoNothing)) + .Add("exec", nameof(NumberAction.NoAttribute)) .Add("args", List.Empty); Assert.Contains( $"{nameof(ExecutableAttribute)}", @@ -236,5 +252,65 @@ public void ValidatePlainValue() Assert.Throws(() => ActionBase.ValidatePlainValue(plainValue)).Message); } + + [Fact] + public void GenerateMethodConstraintsSchema() + { + var expected = List.Empty.Add( + Dictionary.Empty + .Add( + "if", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add("type_id", Dictionary.Empty.Add("const", "Number")) + .Add("exec", Dictionary.Empty.Add("const", "Add")))) + .Add( + "then", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "args", + Dictionary.Empty + .Add("type", "list") + .Add( + "prefixItems", + List.Empty.Add(Dictionary.Empty.Add("type", "integer"))) + .Add("minItems", 1) + .Add("maxItems", 1))))); + var generated = ActionBase.GenerateMethodConstraintsSchema( + typeof(NumberAction), nameof(NumberAction.Add)); + Assert.Equal(expected, generated); + + expected = List.Empty.Add( + Dictionary.Empty + .Add( + "if", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add("type_id", Dictionary.Empty.Add("const", "Number")) + .Add("exec", Dictionary.Empty.Add("const", "Bump")))) + .Add( + "then", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "args", + Dictionary.Empty + .Add("type", "list") + .Add("prefixItems", List.Empty) + .Add("minItems", 0) + .Add("maxItems", 0))))); + generated = ActionBase.GenerateMethodConstraintsSchema( + typeof(NumberAction), nameof(NumberAction.Bump)); + Assert.Equal(expected, generated); + } } } diff --git a/Libplanet.SDK.Action/Action/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase.API.cs index 6c28991a1f6..55bb4bccf12 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.API.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -102,6 +102,192 @@ public static void ValidatePlainValue(IValue plainValue) } } + public static List GenerateMethodConstraintsSchema(Type actionType, string methodName) + { + if (!typeof(ActionBase).IsAssignableFrom(actionType)) + { + throw new ArgumentException( + $"Given {nameof(actionType)} is not assignable to " + + $"{nameof(ActionBase)}: {actionType}", + nameof(actionType)); + } + + ActionTypeAttribute actionTypeAttribute = + actionType.GetCustomAttribute() ?? + throw new ArgumentException( + $"Type is missing a {nameof(ActionTypeAttribute)}.", + nameof(actionType)); + Text typeId = actionTypeAttribute.TypeIdentifier is Text text + ? text + : throw new ArgumentException( + $"Type of {nameof(ActionTypeAttribute.TypeIdentifier)} is expected " + + $"to be {nameof(Text)}: {actionTypeAttribute.TypeIdentifier.GetType()}"); + MethodInfo methodInfo = actionType.GetMethod(methodName) ?? + throw new ArgumentException( + $"Method named {methodName} cannot be found for {actionType}.", + nameof(methodName)); + ParameterInfo[] paramInfos = methodInfo.GetParameters(); + + int minItems = paramInfos.Length, maxItems = paramInfos.Length; + var prefixItems = List.Empty; + foreach (var paramInfo in paramInfos) + { + if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Boolean))) + { + prefixItems = prefixItems.Add(Dictionary.Empty.Add("type", "boolean")); + } + else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Integer))) + { + prefixItems = prefixItems.Add(Dictionary.Empty.Add("type", "integer")); + } + else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Text))) + { + prefixItems = prefixItems.Add(Dictionary.Empty.Add("type", "text")); + } + else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.List))) + { + prefixItems = prefixItems.Add(Dictionary.Empty.Add("type", "list")); + } + else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Dictionary))) + { + prefixItems = prefixItems.Add(Dictionary.Empty.Add("type", "dictionary")); + } + else + { + throw new ArgumentException( + $"Method named {methodName} has a parameter named {paramInfo.Name}" + + $"that has an invalid type: {paramInfo.ParameterType}"); + } + } + + Dictionary argsConstraints = Dictionary.Empty + .Add( + "if", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "type_id", + Dictionary.Empty.Add("const", typeId)) + .Add( + "exec", + Dictionary.Empty.Add("const", methodName)))) + .Add( + "then", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "args", + Dictionary.Empty + .Add("type", "list") + .Add("prefixItems", prefixItems) + .Add("minItems", minItems) + .Add("maxItems", maxItems)))); + return List.Empty.Add(argsConstraints); + } + + public static List GenerateClassConstraintsSchema(Type actionType) + { + if (!typeof(ActionBase).IsAssignableFrom(actionType)) + { + throw new ArgumentException( + $"Given {nameof(actionType)} is not assignable to " + + $"{nameof(ActionBase)}: {actionType}", + nameof(actionType)); + } + + ActionTypeAttribute actionTypeAttribute = + actionType.GetCustomAttribute() ?? + throw new ArgumentException( + $"Type is missing a {nameof(ActionTypeAttribute)}.", + nameof(actionType)); + Text typeId = actionTypeAttribute.TypeIdentifier is Text text + ? text + : throw new ArgumentException( + $"Type of {nameof(ActionTypeAttribute.TypeIdentifier)} is expected " + + $"to be {nameof(Text)}: {actionTypeAttribute.TypeIdentifier.GetType()}"); + + MethodInfo[] methodInfos = actionType + .GetMethods() + .Where(methodInfo => methodInfo.IsPublic) + .Where(methodInfo => methodInfo.GetCustomAttribute() is { }) + .ToArray(); + + Dictionary classConstraints = Dictionary.Empty + .Add( + "if", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "type_id", + Dictionary.Empty.Add("const", typeId)))) + .Add( + "then", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "exec", + Dictionary.Empty.Add( + "enum", + new List(methodInfos.Select(methodInfo => methodInfo.Name) + .ToList()))))); + List result = List.Empty; + result = result.Add(classConstraints); + foreach (var methodInfo in methodInfos) + { + var methodConstraints = + GenerateMethodConstraintsSchema(actionType, methodInfo.Name); + foreach (var methodConstraint in methodConstraints) + { + result = result.Add(methodConstraint); + } + } + + return result; + } + + public static List GenerateConstraintsSchema(Assembly assembly) + { + var baseType = typeof(ActionBase); + List targetTypes = assembly + .GetTypes() + .Where(type => baseType.IsAssignableFrom(type)) + .ToList(); + List result = List.Empty; + foreach (var targetType in targetTypes) + { + var constraints = GenerateClassConstraintsSchema(targetType); + foreach (var constraint in constraints) + { + result.Add(constraint); + } + } + + return result; + } + + public static Dictionary GenerateSchema(Assembly assembly) + { + Dictionary result = Dictionary.Empty + .Add("type", "dictionary") + .Add( + "properties", + Dictionary.Empty + .Add("type_id", Dictionary.Empty.Add("type", "text")) + .Add("exec", Dictionary.Empty.Add("type", "text")) + .Add("args", Dictionary.Empty.Add("type", "list"))) + .Add("required", List.Empty.Add("type_id").Add("exec").Add("args")); + List constraints = GenerateConstraintsSchema(assembly); + return result.Add("allOf", constraints); + } + protected IValue? GetState(Address address) => World.GetAccount(StorageAddress).GetState(address); From 8a1cd64716ac965a1170b3260962b618c548070f Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Thu, 12 Sep 2024 17:21:27 +0900 Subject: [PATCH 13/15] Cleanup --- .../Sample/Actions/InvalidAction.cs | 31 -- .../Sample/Actions/NumberAction.cs | 60 --- .../Sample/Actions/NumberLogAction.cs | 57 --- .../Sample/Actions/TextAction.cs | 23 -- .../Sample/SampleActionsTest.cs | 316 ---------------- .../SimpleTools/Actions/CalculatorAction.cs | 54 +++ .../SimpleTools/Actions/HistoryAction.cs | 45 +++ .../Actions/SimpleToolsActionBase.cs | 6 + .../SimpleTools/Actions/TextAction.cs | 30 ++ .../SimpleTools/SimpleTools.cs | 344 ++++++++++++++++++ Libplanet.SDK.Action/Action/ActionBase.API.cs | 39 +- 11 files changed, 511 insertions(+), 494 deletions(-) delete mode 100644 Libplanet.SDK.Action.Tests/Sample/Actions/InvalidAction.cs delete mode 100644 Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs delete mode 100644 Libplanet.SDK.Action.Tests/Sample/Actions/NumberLogAction.cs delete mode 100644 Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs delete mode 100644 Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleTools/Actions/HistoryAction.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleTools/Actions/SimpleToolsActionBase.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleTools/Actions/TextAction.cs create mode 100644 Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/InvalidAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/InvalidAction.cs deleted file mode 100644 index a19714bb3ad..00000000000 --- a/Libplanet.SDK.Action.Tests/Sample/Actions/InvalidAction.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Bencodex.Types; -using Libplanet.Crypto; -using Libplanet.SDK.Action.Attributes; - -namespace Libplanet.SDK.Action.Tests.Sample.Actions -{ - public class InvalidAction : ActionBase - { - public override Address StorageAddress => - new Address("0x1000000000000000000000000000000000000000"); - - [Executable] - public void Add(IValue args) - { - Integer operand = (Integer)args; - Integer stored = GetState(Signer) is IValue value - ? (Integer)value - : new Integer(0); - SetState(Signer, new Integer(stored.Value + operand.Value)); - } - - public void Subtract(IValue args) - { - Integer operand = (Integer)args; - Integer stored = GetState(Signer) is IValue value - ? (Integer)value - : new Integer(0); - SetState(Signer, new Integer(stored.Value - operand.Value)); - } - } -} diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs deleted file mode 100644 index 08946cc1866..00000000000 --- a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberAction.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Bencodex.Types; -using Libplanet.Action; -using Libplanet.Crypto; -using Libplanet.SDK.Action.Attributes; - -namespace Libplanet.SDK.Action.Tests.Sample.Actions -{ - [ActionType("Number")] - public class NumberAction : ActionBase - { - public override Address StorageAddress => - new Address("0x1000000000000000000000000000000000000001"); - - [Executable] - public void Add(Integer operand) - { - Integer stored = GetState(Signer) is IValue value - ? (Integer)value - : new Integer(0); - Call(nameof(NumberLogAction.Add), new object?[] { operand }); - SetState(Signer, new Integer(stored.Value + operand.Value)); - } - - [Executable] - public void Subtract(Integer operand) - { - Integer stored = GetState(Signer) is IValue value - ? (Integer)value - : new Integer(0); - Call(nameof(NumberLogAction.Subtract), new object?[] { operand }); - SetState(Signer, new Integer(stored.Value - operand.Value)); - } - - [Executable] - public void Multiply(Integer operand) - { - Integer stored = GetState(Signer) is IValue value - ? (Integer)value - : new Integer(1); - Call(nameof(NumberLogAction.Multiply), new object?[] { operand }); - SetState(Signer, new Integer(stored.Value * operand.Value)); - } - - [Executable] - public void Bump() - { - Integer stored = GetState(Signer) is IValue value - ? (Integer)value - : 0; - Call(nameof(NumberLogAction.Add), new object?[] { new Integer(1) }); - SetState(Signer, new Integer(stored.Value + 1)); - } - - // Just some random public method for testing. - public void NoAttribute() - { - return; - } - } -} diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberLogAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/NumberLogAction.cs deleted file mode 100644 index 1144d5a6b25..00000000000 --- a/Libplanet.SDK.Action.Tests/Sample/Actions/NumberLogAction.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Bencodex.Types; -using Libplanet.Crypto; -using Libplanet.SDK.Action.Attributes; - -namespace Libplanet.SDK.Action.Tests.Sample.Actions -{ - public class NumberLogAction : ActionBase - { - public override Address StorageAddress => - new Address("0x1000000000000000000000000000000000000002"); - - [Callable] - public void Add(Integer operand) - { - Text stored = GetState(Signer) is IValue value - ? (Text)value - : new Text(string.Empty); - Text formatted = operand.Value >= 0 - ? new Text($"{operand.Value}") - : new Text($"({operand.Value})"); - formatted = stored.Value.Length == 0 - ? new Text(stored.Value + $"{formatted.Value}") - : new Text(stored.Value + $" + {formatted.Value}"); - SetState(Signer, formatted); - } - - [Callable] - public void Subtract(Integer operand) - { - Text stored = GetState(Signer) is IValue value - ? (Text)value - : new Text(string.Empty); - Text formatted = operand.Value >= 0 - ? new Text($"{operand.Value}") - : new Text($"({operand.Value})"); - formatted = stored.Value.Length == 0 - ? new Text(stored.Value + $"{formatted.Value}") - : new Text(stored.Value + $" - {formatted.Value}"); - SetState(Signer, formatted); - } - - // This is without Callable attribute on purpose for testing. - public void Multiply(Integer operand) - { - Text stored = GetState(Signer) is IValue value - ? (Text)value - : new Text(string.Empty); - Text formatted = operand.Value >= 0 - ? new Text($"{operand.Value}") - : new Text($"({operand.Value})"); - formatted = stored.Value.Length == 0 - ? new Text($"{formatted.Value}") - : new Text(stored.Value + $" * {formatted.Value}"); - SetState(Signer, formatted); - } - } -} diff --git a/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs b/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs deleted file mode 100644 index fa16f3c5a5d..00000000000 --- a/Libplanet.SDK.Action.Tests/Sample/Actions/TextAction.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Bencodex.Types; -using Libplanet.Action; -using Libplanet.Crypto; -using Libplanet.SDK.Action.Attributes; - -namespace Libplanet.SDK.Action.Tests.Sample.Actions -{ - [ActionType("Text")] - public class TextAction : ActionBase - { - public override Address StorageAddress => - new Address("0x1000000000000000000000000000000000000003"); - - [Executable] - public void Append(Text operand) - { - Text stored = GetState(Signer) is IValue value - ? (Text)value - : new Text(string.Empty); - SetState(Signer, new Text(stored.Value + operand.Value)); - } - } -} diff --git a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs b/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs deleted file mode 100644 index 0c537797c0b..00000000000 --- a/Libplanet.SDK.Action.Tests/Sample/SampleActionsTest.cs +++ /dev/null @@ -1,316 +0,0 @@ -using System.Collections.Immutable; -using System.Reflection; -using Bencodex.Types; -using Libplanet.Action; -using Libplanet.Action.Loader; -using Libplanet.Action.State; -using Libplanet.Crypto; -using Libplanet.SDK.Action.Attributes; -using Libplanet.SDK.Action.Tests.Sample.Actions; -using Libplanet.Store; -using Libplanet.Store.Trie; -using Libplanet.Types.Blocks; -using Xunit; - -namespace Libplanet.SDK.Action.Tests.Sample -{ - public class SampleActionsTest - { - private TypedActionLoader _loader; - private IStateStore _stateStore; - private IWorld _world; - - public SampleActionsTest() - { - _loader = new TypedActionLoader( - ImmutableDictionary.Empty - .Add(new Text("Number"), typeof(NumberAction)) - .Add(new Text("Text"), typeof(TextAction))); - - _stateStore = new TrieStateStore(new MemoryKeyValueStore()); - - ITrie trie = _stateStore.GetStateRoot(null); - trie = trie.SetMetadata(new TrieMetadata(Block.CurrentProtocolVersion)); - trie = _stateStore.Commit(trie); - _world = new World(new WorldBaseState(trie, _stateStore)); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void NumberAddAndSubtract(bool commit) - { - IValue plainValue = Dictionary.Empty - .Add("type_id", "Number") - .Add("exec", "Add") - .Add("args", List.Empty.Add(5)); - NumberAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); - Address signer = new PrivateKey().Address; - IWorld world = _world; - - world = action.Execute(new MockActionContext(signer, signer, world)); - world = commit ? _stateStore.CommitWorld(world) : world; - Assert.Equal( - new Integer(5), - world.GetAccountState(action.StorageAddress).GetState(signer)); - Assert.Equal( - new Text("5"), - world - .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) - .GetState(signer)); - - plainValue = Dictionary.Empty - .Add("type_id", "Number") - .Add("exec", "Subtract") - .Add("args", List.Empty.Add(8)); - action = Assert.IsType(_loader.LoadAction(0, plainValue)); - world = action.Execute(new MockActionContext(signer, signer, world)); - world = commit ? _stateStore.CommitWorld(world) : world; - Assert.Equal( - new Integer(-3), - world.GetAccountState(action.StorageAddress).GetState(signer)); - Assert.Equal( - new Text("5 - 8"), - world - .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) - .GetState(signer)); - - plainValue = Dictionary.Empty - .Add("type_id", "Number") - .Add("exec", "Bump") - .Add("args", List.Empty); - action = Assert.IsType(_loader.LoadAction(0, plainValue)); - world = action.Execute(new MockActionContext(signer, signer, world)); - world = commit ? _stateStore.CommitWorld(world) : world; - Assert.Equal( - new Integer(-2), - world.GetAccountState(action.StorageAddress).GetState(signer)); - Assert.Equal( - new Text("5 - 8 + 1"), - world - .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) - .GetState(signer)); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void TextAppend(bool commit) - { - IValue plainValue = Dictionary.Empty - .Add("type_id", "Text") - .Add("exec", "Append") - .Add("args", List.Empty.Add("Hello")); - TextAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); - Address signer = new PrivateKey().Address; - IWorld world = _world; - - world = action.Execute(new MockActionContext(signer, signer, world)); - world = commit ? _stateStore.CommitWorld(world) : world; - Assert.Equal( - new Text("Hello"), - world.GetAccountState(action.StorageAddress).GetState(signer)); - - plainValue = Dictionary.Empty - .Add("type_id", "Text") - .Add("exec", "Append") - .Add("args", List.Empty.Add(" world")); - action = Assert.IsType(_loader.LoadAction(0, plainValue)); - world = action.Execute(new MockActionContext(signer, signer, world)); - world = commit ? _stateStore.CommitWorld(world) : world; - Assert.Equal( - new Text("Hello world"), - world.GetAccountState(action.StorageAddress).GetState(signer)); - } - - [Fact] - public void InvalidPlainValueForLoading() - { - IValue plainValue = Dictionary.Empty // Invalid type_id - .Add("type_id", "Run") - .Add("exec", "Append") - .Add("args", List.Empty.Add("Hello")); - Assert.Throws(() => _loader.LoadAction(0, plainValue)); - - plainValue = Dictionary.Empty // Missing type_id - .Add("exec", "Append") - .Add("args", List.Empty.Add("Hello")); - Assert.Throws(() => _loader.LoadAction(0, plainValue)); - - plainValue = Dictionary.Empty // Missing call - .Add("type_id", "Number") - .Add("args", List.Empty.Add(5)); - Assert.Throws(() => _loader.LoadAction(0, plainValue)); - - plainValue = Dictionary.Empty // Missing args - .Add("type_id", "Number") - .Add("exec", "Add"); - Assert.Throws(() => _loader.LoadAction(0, plainValue)); - } - - [Fact] - public void InvalidPlainValueForExecution() - { - IValue plainValue = Dictionary.Empty // Invalid call - .Add("type_id", "Number") - .Add("exec", "Divide") - .Add("args", List.Empty.Add(5)); - IAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); - Address address = new PrivateKey().Address; - Assert.Throws(() => - action.Execute(new MockActionContext(address, address, _world))); - - plainValue = Dictionary.Empty // Invalid args - .Add("type_id", "Number") - .Add("exec", "Add") - .Add("args", List.Empty.Add("Hello")); - action = Assert.IsType(_loader.LoadAction(0, plainValue)); - Assert.Throws(() => - action.Execute(new MockActionContext(address, address, _world))); - } - - [Fact] - public void CallableAttributeIsRequired() - { - IValue plainValue = Dictionary.Empty - .Add("type_id", "Number") - .Add("exec", "Multiply") - .Add("args", List.Empty.Add(5)); - NumberAction action = Assert.IsType(_loader.LoadAction(0, plainValue)); - Address signer = new PrivateKey().Address; - IWorld world = _world; - - Assert.IsType( - Assert.Throws(() => - action.Execute(new MockActionContext(signer, signer, world))) - .InnerException); - } - - [Fact] - public void ValidatePlainValue() - { - IValue plainValue; - plainValue = Dictionary.Empty - .Add("type_id", "Number") - .Add("exec", nameof(NumberAction.Add)) - .Add("args", List.Empty.Add(5)); - ActionBase.ValidatePlainValue(plainValue); - - plainValue = Dictionary.Empty - .Add("type_id", "Text") - .Add("exec", nameof(TextAction.Append)) - .Add("args", List.Empty.Add("Hello")); - ActionBase.ValidatePlainValue(plainValue); - - // Missing type_id. - plainValue = Dictionary.Empty - .Add("type_id", "Invalid") - .Add("exec", nameof(TextAction.Append)) - .Add("args", List.Empty.Add("Hello")); - Assert.Contains( - $"{nameof(ActionTypeAttribute)}", - Assert.Throws(() => - ActionBase.ValidatePlainValue(plainValue)).Message); - - // Unknown exec. - plainValue = Dictionary.Empty - .Add("type_id", "Number") - .Add("exec", "Divide") - .Add("args", List.Empty.Add(5)); - Assert.Contains( - $"cannot be found", - Assert.Throws(() => - ActionBase.ValidatePlainValue(plainValue)).Message); - - // Missing attribute. - plainValue = Dictionary.Empty - .Add("type_id", "Number") - .Add("exec", nameof(NumberAction.NoAttribute)) - .Add("args", List.Empty); - Assert.Contains( - $"{nameof(ExecutableAttribute)}", - Assert.Throws(() => - ActionBase.ValidatePlainValue(plainValue)).Message); - - // Invalid argument type. - plainValue = Dictionary.Empty - .Add("type_id", "Number") - .Add("exec", nameof(NumberAction.Add)) - .Add("args", List.Empty.Add("Hello")); - Assert.Contains( - $"argument at", - Assert.Throws(() => - ActionBase.ValidatePlainValue(plainValue)).Message); - - // Invalid arguments length. - plainValue = Dictionary.Empty - .Add("type_id", "Number") - .Add("exec", nameof(NumberAction.Add)) - .Add("args", List.Empty.Add(5).Add(6)); - Assert.Contains( - $"The length", - Assert.Throws(() => - ActionBase.ValidatePlainValue(plainValue)).Message); - } - - [Fact] - public void GenerateMethodConstraintsSchema() - { - var expected = List.Empty.Add( - Dictionary.Empty - .Add( - "if", - Dictionary.Empty - .Add( - "properties", - Dictionary.Empty - .Add("type_id", Dictionary.Empty.Add("const", "Number")) - .Add("exec", Dictionary.Empty.Add("const", "Add")))) - .Add( - "then", - Dictionary.Empty - .Add( - "properties", - Dictionary.Empty - .Add( - "args", - Dictionary.Empty - .Add("type", "list") - .Add( - "prefixItems", - List.Empty.Add(Dictionary.Empty.Add("type", "integer"))) - .Add("minItems", 1) - .Add("maxItems", 1))))); - var generated = ActionBase.GenerateMethodConstraintsSchema( - typeof(NumberAction), nameof(NumberAction.Add)); - Assert.Equal(expected, generated); - - expected = List.Empty.Add( - Dictionary.Empty - .Add( - "if", - Dictionary.Empty - .Add( - "properties", - Dictionary.Empty - .Add("type_id", Dictionary.Empty.Add("const", "Number")) - .Add("exec", Dictionary.Empty.Add("const", "Bump")))) - .Add( - "then", - Dictionary.Empty - .Add( - "properties", - Dictionary.Empty - .Add( - "args", - Dictionary.Empty - .Add("type", "list") - .Add("prefixItems", List.Empty) - .Add("minItems", 0) - .Add("maxItems", 0))))); - generated = ActionBase.GenerateMethodConstraintsSchema( - typeof(NumberAction), nameof(NumberAction.Bump)); - Assert.Equal(expected, generated); - } - } -} diff --git a/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs new file mode 100644 index 00000000000..3b84cdc4324 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs @@ -0,0 +1,54 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; + +namespace Libplanet.SDK.Action.Tests.SimpleTools.Actions +{ + [ActionType("Calc")] + public class CalculatorAction : SimpleToolsActionBase + { + public override Address StorageAddress => + new Address("0x1000000000000000000000000000000000000001"); + + [Executable] + public void Add(Integer x, Integer y) + { + Integer result = x + y; + Call(nameof(HistoryAction.LogInteger), new object[] { result }); + SetState(Signer, result); + } + + [Executable] + public void Subtract(Integer x, Integer y) + { + Integer result = x - y; + Call(nameof(HistoryAction.LogInteger), new object[] { result }); + SetState(Signer, result); + } + + [Executable] + public void Multiply(Integer x, Integer y) + { + Integer result = x * y; + Call(nameof(HistoryAction.LogInteger), new object[] { result }); + SetState(Signer, result); + } + + [Executable] + public void Square(Integer x) + { + Integer result = x * x; + Call(nameof(HistoryAction.LogInteger), new object[] { result }); + SetState(Signer, result); + } + + [Executable] + public void LogLength() + { + Integer result = Call(nameof(HistoryAction.LogLength)); + Call(nameof(HistoryAction.LogInteger), new object[] { result }); + SetState(Signer, result); + } + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleTools/Actions/HistoryAction.cs b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/HistoryAction.cs new file mode 100644 index 00000000000..6f71c5ef178 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/HistoryAction.cs @@ -0,0 +1,45 @@ +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; + +namespace Libplanet.SDK.Action.Tests.SimpleTools.Actions +{ + public class HistoryAction : SimpleToolsActionBase + { + public override Address StorageAddress => + new Address("0x1000000000000000000000000000000000000002"); + + [Callable] + public void LogInteger(Integer result) + { + Text stored = GetState(Signer) is IValue value + ? (Text)value + : new Text(string.Empty); + Text formatted = stored.Value.Length == 0 + ? new Text($"{result.Value}") + : new Text(stored.Value + $", {result.Value}"); + SetState(Signer, formatted); + } + + [Callable] + public void LogText(Text result) + { + Text stored = GetState(Signer) is IValue value + ? (Text)value + : new Text(string.Empty); + Text formatted = stored.Value.Length == 0 + ? new Text($"{result.Value}") + : new Text(stored.Value + $", {result.Value}"); + SetState(Signer, formatted); + } + + [Callable] + public Integer LogLength() + { + Text stored = GetState(Signer) is IValue value + ? (Text)value + : new Text(string.Empty); + return new Integer(stored.Value.Length); + } + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleTools/Actions/SimpleToolsActionBase.cs b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/SimpleToolsActionBase.cs new file mode 100644 index 00000000000..a2adbc0a840 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/SimpleToolsActionBase.cs @@ -0,0 +1,6 @@ +namespace Libplanet.SDK.Action.Tests.SimpleTools.Actions +{ + public abstract class SimpleToolsActionBase : ActionBase + { + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleTools/Actions/TextAction.cs b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/TextAction.cs new file mode 100644 index 00000000000..7d73aea85aa --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/TextAction.cs @@ -0,0 +1,30 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; + +namespace Libplanet.SDK.Action.Tests.SimpleTools.Actions +{ + [ActionType("Text")] + public class TextAction : SimpleToolsActionBase + { + public override Address StorageAddress => + new Address("0x1000000000000000000000000000000000000003"); + + [Executable] + public void ToUpper(Text text) + { + Text result = new Text(text.Value.ToUpper()); + Call(nameof(HistoryAction.LogText), new object[] { result }); + SetState(Signer, new Text(result)); + } + + [Executable] + public void ToLower(Text text) + { + Text result = new Text(text.Value.ToLower()); + Call(nameof(HistoryAction.LogText), new object[] { result }); + SetState(Signer, new Text(result)); + } + } +} diff --git a/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs b/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs new file mode 100644 index 00000000000..e100beefca9 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs @@ -0,0 +1,344 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Tests.SimpleTools.Actions; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Libplanet.Types.Blocks; +using Xunit; + +namespace Libplanet.SDK.Action.Tests.Calculator +{ + public class SimpleToolsTest + { + private TypedActionLoader _loader; + private IStateStore _stateStore; + private IWorld _world; + + public SimpleToolsTest() + { + _loader = TypedActionLoader.Create(baseType: typeof(SimpleToolsActionBase)); + _stateStore = new TrieStateStore(new MemoryKeyValueStore()); + + ITrie trie = _stateStore.GetStateRoot(null); + trie = trie.SetMetadata(new TrieMetadata(Block.CurrentProtocolVersion)); + trie = _stateStore.Commit(trie); + _world = new World(new WorldBaseState(trie, _stateStore)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Execute(bool commit) + { + Address signer = new PrivateKey().Address; + IWorld world = _world; + IValue plainValue; + IAction action; + + plainValue = Dictionary.Empty + .Add("type_id", "Calc") + .Add("exec", "Add") + .Add("args", List.Empty.Add(5).Add(3)); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Integer(8), + world.GetAccountState(((CalculatorAction)action).StorageAddress).GetState(signer)); + Assert.Equal( + new Text("8"), + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + + plainValue = Dictionary.Empty + .Add("type_id", "Calc") + .Add("exec", "Subtract") + .Add("args", List.Empty.Add(17).Add(13)); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Integer(4), + world.GetAccountState(((CalculatorAction)action).StorageAddress).GetState(signer)); + Assert.Equal( + new Text("8, 4"), + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + + plainValue = Dictionary.Empty + .Add("type_id", "Calc") + .Add("exec", "Multiply") + .Add("args", List.Empty.Add(2).Add(-3)); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Integer(-6), + world.GetAccountState(((CalculatorAction)action).StorageAddress).GetState(signer)); + Assert.Equal( + new Text("8, 4, -6"), + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + + plainValue = Dictionary.Empty + .Add("type_id", "Calc") + .Add("exec", "Square") + .Add("args", List.Empty.Add(7)); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Integer(49), + world.GetAccountState(((CalculatorAction)action).StorageAddress).GetState(signer)); + Assert.Equal( + new Text("8, 4, -6, 49"), + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + + plainValue = Dictionary.Empty + .Add("type_id", "Text") + .Add("exec", "ToUpper") + .Add("args", List.Empty.Add(new Text("Hello"))); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Text("HELLO"), + world.GetAccountState(((TextAction)action).StorageAddress).GetState(signer)); + Assert.Equal( + new Text("8, 4, -6, 49, HELLO"), + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + + plainValue = Dictionary.Empty + .Add("type_id", "Text") + .Add("exec", "ToLower") + .Add("args", List.Empty.Add(new Text("World"))); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Text("world"), + world.GetAccountState(((TextAction)action).StorageAddress).GetState(signer)); + Assert.Equal( + new Text("8, 4, -6, 49, HELLO, world"), + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + + plainValue = Dictionary.Empty + .Add("type_id", "Calc") + .Add("exec", "LogLength") + .Add("args", List.Empty); + action = Assert.IsType(_loader.LoadAction(0, plainValue)); + world = action.Execute(new MockActionContext(signer, signer, world)); + world = commit ? _stateStore.CommitWorld(world) : world; + Assert.Equal( + new Integer(26), + world.GetAccountState(((CalculatorAction)action).StorageAddress).GetState(signer)); + Assert.Equal( + new Text("8, 4, -6, 49, HELLO, world, 26"), + world + .GetAccountState(new Address("0x1000000000000000000000000000000000000002")) + .GetState(signer)); + } + + [Fact] + public void ValidatePlainValue() + { + IValue plainValue; + plainValue = Dictionary.Empty + .Add("type_id", "Calc") + .Add("exec", nameof(CalculatorAction.Add)) + .Add("args", List.Empty.Add(5).Add(3)); + ActionBase.ValidatePlainValue(plainValue); + + // Wrong type_id. + plainValue = Dictionary.Empty + .Add("type_id", "Invalid") + .Add("exec", nameof(CalculatorAction.Add)) + .Add("args", List.Empty.Add(5).Add(3)); + Assert.Contains( + "\"type_id\" for plainValue does not match the expected", + Assert.Throws(() => + ActionBase.ValidatePlainValue(plainValue)).Message); + + // Unknown exec. + plainValue = Dictionary.Empty + .Add("type_id", "Calc") + .Add("exec", "Divide") + .Add("args", List.Empty.Add(5).Add(3)); + Assert.Contains( + "Method named Divide cannot be found", + Assert.Throws(() => + ActionBase.ValidatePlainValue(plainValue)).Message); + + // Invalid argument type. + plainValue = Dictionary.Empty + .Add("type_id", "Calc") + .Add("exec", nameof(CalculatorAction.Add)) + .Add("args", List.Empty.Add("Hello").Add("World")); + Assert.Contains( + $"The argument at ", + Assert.Throws(() => + ActionBase.ValidatePlainValue(plainValue)).Message); + + // Invalid arguments length. + plainValue = Dictionary.Empty + .Add("type_id", "Calc") + .Add("exec", nameof(CalculatorAction.Add)) + .Add("args", List.Empty.Add(5)); + Assert.Contains( + $"The length of \"args\" for plainValue should be", + Assert.Throws(() => + ActionBase.ValidatePlainValue(plainValue)).Message); + } + + [Fact] + public void GenerateMethodConstraintsSchema() + { + var expected = List.Empty.Add( + Dictionary.Empty + .Add( + "if", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add("type_id", Dictionary.Empty.Add("const", "Calc")) + .Add("exec", Dictionary.Empty.Add("const", "Add")))) + .Add( + "then", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "args", + Dictionary.Empty + .Add("type", "list") + .Add( + "prefixItems", + List.Empty + .Add(Dictionary.Empty.Add("type", "integer")) + .Add(Dictionary.Empty.Add("type", "integer"))) + .Add("minItems", 2) + .Add("maxItems", 2))))); + var generated = ActionBase.GenerateMethodConstraintsSchema( + typeof(CalculatorAction), nameof(CalculatorAction.Add)); + Assert.Equal(expected, generated); + + expected = List.Empty.Add( + Dictionary.Empty + .Add( + "if", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add("type_id", Dictionary.Empty.Add("const", "Calc")) + .Add("exec", Dictionary.Empty.Add("const", "LogLength")))) + .Add( + "then", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "args", + Dictionary.Empty + .Add("type", "list") + .Add("prefixItems", List.Empty) + .Add("minItems", 0) + .Add("maxItems", 0))))); + generated = ActionBase.GenerateMethodConstraintsSchema( + typeof(CalculatorAction), nameof(CalculatorAction.LogLength)); + Assert.Equal(expected, generated); + } + + [Fact] + public void GenerateClassConstraintsSchema() + { + var expected = List.Empty.Add( + Dictionary.Empty + .Add( + "if", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add("type_id", Dictionary.Empty.Add("const", "Calc")))) + .Add( + "then", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "exec", + Dictionary.Empty + .Add( + "enum", + List.Empty + .Add("Add") + .Add("Subtract") + .Add("Multiply") + .Add("Square") + .Add("LogLength")))))); + List methodNames = new List() + { + "Add", "Subtract", "Multiply", "Square", "LogLength" + }; + foreach (var constraints in methodNames.Select( + methodName => ActionBase.GenerateMethodConstraintsSchema( + typeof(CalculatorAction), methodName))) + { + foreach (var constraint in constraints) + { + expected = expected.Add(constraint); + } + }; + + var generated = ActionBase.GenerateClassConstraintsSchema(typeof(CalculatorAction)); + Assert.Equal(expected, generated); + } + + [Fact] + public void GenerateConstraintsSchema() + { + var expected = List.Empty.Add( + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "type_id", + Dictionary.Empty + .Add("enum", List.Empty.Add("Calc").Add("Text"))))); + List actionTypes = new List() + { + typeof(CalculatorAction), typeof(TextAction) + }; + foreach (var constraints in actionTypes.Select( + actionType => ActionBase.GenerateClassConstraintsSchema( + actionType))) + { + foreach (var constraint in constraints) + { + expected = expected.Add(constraint); + } + }; + + var generated = ActionBase.GenerateConstraintsSchema(typeof(SimpleToolsActionBase)); + Assert.Equal(expected, generated); + } + } +} diff --git a/Libplanet.SDK.Action/Action/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase.API.cs index 55bb4bccf12..b2d56044404 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.API.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -202,7 +202,7 @@ public static List GenerateClassConstraintsSchema(Type actionType) ActionTypeAttribute actionTypeAttribute = actionType.GetCustomAttribute() ?? throw new ArgumentException( - $"Type is missing a {nameof(ActionTypeAttribute)}.", + $"Type is missing a {nameof(ActionTypeAttribute)}: {actionType}", nameof(actionType)); Text typeId = actionTypeAttribute.TypeIdentifier is Text text ? text @@ -253,27 +253,46 @@ public static List GenerateClassConstraintsSchema(Type actionType) return result; } - public static List GenerateConstraintsSchema(Assembly assembly) + public static List GenerateConstraintsSchema(Assembly assembly, Type baseType) { - var baseType = typeof(ActionBase); List targetTypes = assembly .GetTypes() .Where(type => baseType.IsAssignableFrom(type)) + .Where(type => type.GetCustomAttribute() is { }) .ToList(); - List result = List.Empty; +#pragma warning disable MEN002 + List result = List.Empty + .Add( + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add( + "type_id", + Dictionary.Empty + .Add( + "enum", + new List(targetTypes.Select(targetType => targetType.GetCustomAttribute()!.TypeIdentifier)))))); +#pragma warning restore MEN002 foreach (var targetType in targetTypes) { var constraints = GenerateClassConstraintsSchema(targetType); foreach (var constraint in constraints) { - result.Add(constraint); + result = result.Add(constraint); } } return result; } - public static Dictionary GenerateSchema(Assembly assembly) + public static List GenerateConstraintsSchema(Assembly assembly) => + GenerateConstraintsSchema(assembly, typeof(ActionBase)); + + public static List GenerateConstraintsSchema(Type baseType) => + GenerateConstraintsSchema(baseType.Assembly, baseType); + + public static Dictionary GenerateSchema(Assembly assembly, Type baseType) { Dictionary result = Dictionary.Empty .Add("type", "dictionary") @@ -284,10 +303,16 @@ public static Dictionary GenerateSchema(Assembly assembly) .Add("exec", Dictionary.Empty.Add("type", "text")) .Add("args", Dictionary.Empty.Add("type", "list"))) .Add("required", List.Empty.Add("type_id").Add("exec").Add("args")); - List constraints = GenerateConstraintsSchema(assembly); + List constraints = GenerateConstraintsSchema(assembly, baseType); return result.Add("allOf", constraints); } + public static Dictionary GenerateSchema(Assembly assembly) => + GenerateSchema(assembly, typeof(ActionBase)); + + public static Dictionary GenerateSchema(Type baseType) => + GenerateSchema(baseType.Assembly, baseType); + protected IValue? GetState(Address address) => World.GetAccount(StorageAddress).GetState(address); From 083f3b82cf3c52269a2f82a448e31e06eb4a27be Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 13 Sep 2024 14:07:55 +0900 Subject: [PATCH 14/15] Added optional description feature to executable attribute --- .../SimpleTools/Actions/CalculatorAction.cs | 4 ++-- .../SimpleTools/Actions/TextAction.cs | 2 +- .../SimpleTools/SimpleTools.cs | 6 +++++- Libplanet.SDK.Action/Action/ActionBase.API.cs | 20 +++++++++++++------ .../Attributes/ExecutableAttribute.cs | 6 ++++++ 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs index 3b84cdc4324..afdc45b29c0 100644 --- a/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs +++ b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs @@ -11,7 +11,7 @@ public class CalculatorAction : SimpleToolsActionBase public override Address StorageAddress => new Address("0x1000000000000000000000000000000000000001"); - [Executable] + [Executable("Adds two numbers.")] public void Add(Integer x, Integer y) { Integer result = x + y; @@ -35,7 +35,7 @@ public void Multiply(Integer x, Integer y) SetState(Signer, result); } - [Executable] + [Executable("Squares the number.")] public void Square(Integer x) { Integer result = x * x; diff --git a/Libplanet.SDK.Action.Tests/SimpleTools/Actions/TextAction.cs b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/TextAction.cs index 7d73aea85aa..acdf4aaf06b 100644 --- a/Libplanet.SDK.Action.Tests/SimpleTools/Actions/TextAction.cs +++ b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/TextAction.cs @@ -11,7 +11,7 @@ public class TextAction : SimpleToolsActionBase public override Address StorageAddress => new Address("0x1000000000000000000000000000000000000003"); - [Executable] + [Executable("Converts a string to uppercase.")] public void ToUpper(Text text) { Text result = new Text(text.Value.ToUpper()); diff --git a/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs b/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs index e100beefca9..31add373712 100644 --- a/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs +++ b/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs @@ -214,7 +214,11 @@ public void GenerateMethodConstraintsSchema() "properties", Dictionary.Empty .Add("type_id", Dictionary.Empty.Add("const", "Calc")) - .Add("exec", Dictionary.Empty.Add("const", "Add")))) + .Add( + "exec", + Dictionary.Empty + .Add("const", "Add") + .Add("description", "Adds two numbers.")))) .Add( "then", Dictionary.Empty diff --git a/Libplanet.SDK.Action/Action/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase.API.cs index b2d56044404..313fe4c0219 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.API.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -126,6 +126,11 @@ public static List GenerateMethodConstraintsSchema(Type actionType, string metho throw new ArgumentException( $"Method named {methodName} cannot be found for {actionType}.", nameof(methodName)); + ExecutableAttribute executableAttribute = + methodInfo.GetCustomAttribute() ?? + throw new ArgumentException( + $"Method named {methodName} is missing a {nameof(ExecutableAttribute)}.", + nameof(methodName)); ParameterInfo[] paramInfos = methodInfo.GetParameters(); int minItems = paramInfos.Length, maxItems = paramInfos.Length; @@ -160,6 +165,13 @@ public static List GenerateMethodConstraintsSchema(Type actionType, string metho } } + Dictionary typeIdValue = Dictionary.Empty; + Dictionary execValue = Dictionary.Empty; + typeIdValue = typeIdValue.Add("const", typeId); + execValue = execValue.Add("const", methodInfo.Name); + execValue = executableAttribute.Description is { } executalbeDescription + ? execValue.Add("description", new Text(executalbeDescription)) + : execValue; Dictionary argsConstraints = Dictionary.Empty .Add( "if", @@ -167,12 +179,8 @@ public static List GenerateMethodConstraintsSchema(Type actionType, string metho .Add( "properties", Dictionary.Empty - .Add( - "type_id", - Dictionary.Empty.Add("const", typeId)) - .Add( - "exec", - Dictionary.Empty.Add("const", methodName)))) + .Add("type_id", typeIdValue) + .Add("exec", execValue))) .Add( "then", Dictionary.Empty diff --git a/Libplanet.SDK.Action/Attributes/ExecutableAttribute.cs b/Libplanet.SDK.Action/Attributes/ExecutableAttribute.cs index bbf1b0a5842..f0dda2ff052 100644 --- a/Libplanet.SDK.Action/Attributes/ExecutableAttribute.cs +++ b/Libplanet.SDK.Action/Attributes/ExecutableAttribute.cs @@ -3,5 +3,11 @@ namespace Libplanet.SDK.Action.Attributes [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public class ExecutableAttribute : Attribute { + public ExecutableAttribute(string? description = null) + { + Description = description; + } + + public string? Description { get; set; } } } From 815b5eacde1c582d67342ddff1624b81526bddc9 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 13 Sep 2024 14:20:47 +0900 Subject: [PATCH 15/15] Added optional parameter description to schema --- .../SimpleTools/Actions/CalculatorAction.cs | 7 +++++-- .../SimpleTools/SimpleTools.cs | 16 ++++++++++++++-- Libplanet.SDK.Action/Action/ActionBase.API.cs | 19 ++++++++++++++----- .../Attributes/ParameterAttribute.cs | 13 +++++++++++++ 4 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 Libplanet.SDK.Action/Attributes/ParameterAttribute.cs diff --git a/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs index afdc45b29c0..e6588a07cc5 100644 --- a/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs +++ b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs @@ -12,7 +12,9 @@ public class CalculatorAction : SimpleToolsActionBase new Address("0x1000000000000000000000000000000000000001"); [Executable("Adds two numbers.")] - public void Add(Integer x, Integer y) + public void Add( + [Parameter("The first operand.")]Integer x, + [Parameter("The second operand.")]Integer y) { Integer result = x + y; Call(nameof(HistoryAction.LogInteger), new object[] { result }); @@ -36,7 +38,8 @@ public void Multiply(Integer x, Integer y) } [Executable("Squares the number.")] - public void Square(Integer x) + public void Square( + [Parameter("The number to sqaure.")]Integer x) { Integer result = x * x; Call(nameof(HistoryAction.LogInteger), new object[] { result }); diff --git a/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs b/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs index 31add373712..281562ad9c8 100644 --- a/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs +++ b/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs @@ -232,8 +232,20 @@ public void GenerateMethodConstraintsSchema() .Add( "prefixItems", List.Empty - .Add(Dictionary.Empty.Add("type", "integer")) - .Add(Dictionary.Empty.Add("type", "integer"))) + .Add( + Dictionary.Empty + .Add("type", "integer") + .Add("title", "x") + .Add( + "description", + "The first operand.")) + .Add( + Dictionary.Empty + .Add("type", "integer") + .Add("title", "y") + .Add( + "description", + "The second operand."))) .Add("minItems", 2) .Add("maxItems", 2))))); var generated = ActionBase.GenerateMethodConstraintsSchema( diff --git a/Libplanet.SDK.Action/Action/ActionBase.API.cs b/Libplanet.SDK.Action/Action/ActionBase.API.cs index 313fe4c0219..4caf41a56c3 100644 --- a/Libplanet.SDK.Action/Action/ActionBase.API.cs +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -137,25 +137,26 @@ public static List GenerateMethodConstraintsSchema(Type actionType, string metho var prefixItems = List.Empty; foreach (var paramInfo in paramInfos) { + Dictionary prefixItem = Dictionary.Empty; if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Boolean))) { - prefixItems = prefixItems.Add(Dictionary.Empty.Add("type", "boolean")); + prefixItem = prefixItem.Add("type", "boolean"); } else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Integer))) { - prefixItems = prefixItems.Add(Dictionary.Empty.Add("type", "integer")); + prefixItem = prefixItem.Add("type", "integer"); } else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Text))) { - prefixItems = prefixItems.Add(Dictionary.Empty.Add("type", "text")); + prefixItem = prefixItem.Add("type", "text"); } else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.List))) { - prefixItems = prefixItems.Add(Dictionary.Empty.Add("type", "list")); + prefixItem = prefixItem.Add("type", "list"); } else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Dictionary))) { - prefixItems = prefixItems.Add(Dictionary.Empty.Add("type", "dictionary")); + prefixItem = prefixItem.Add("type", "dictionary"); } else { @@ -163,6 +164,14 @@ public static List GenerateMethodConstraintsSchema(Type actionType, string metho $"Method named {methodName} has a parameter named {paramInfo.Name}" + $"that has an invalid type: {paramInfo.ParameterType}"); } + + prefixItem = prefixItem.Add("title", paramInfo.Name!); + if (paramInfo.GetCustomAttribute() is { } parameterAttribute) + { + prefixItem = prefixItem.Add("description", parameterAttribute.Description); + } + + prefixItems = prefixItems.Add(prefixItem); } Dictionary typeIdValue = Dictionary.Empty; diff --git a/Libplanet.SDK.Action/Attributes/ParameterAttribute.cs b/Libplanet.SDK.Action/Attributes/ParameterAttribute.cs new file mode 100644 index 00000000000..fa77d37564c --- /dev/null +++ b/Libplanet.SDK.Action/Attributes/ParameterAttribute.cs @@ -0,0 +1,13 @@ +namespace Libplanet.SDK.Action.Attributes +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + public class ParameterAttribute : Attribute + { + public ParameterAttribute(string description) + { + Description = description; + } + + public string Description { get; set; } + } +}