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..a45f78098d9 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/MockActionContext.cs @@ -0,0 +1,60 @@ +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Evidence; +using Libplanet.Types.Tx; + +namespace Libplanet.SDK.Action.Tests +{ + public class MockActionContext : IActionContext + { + public MockActionContext( + Address signer, + Address miner, + IWorld previousState) + { + Signer = signer; + Miner = miner; + PreviousState = previousState; + } + + public Address Signer { get; } + + public TxId? TxId => + throw new NotSupportedException(); + + public Address Miner { get; } + + public long BlockIndex => + throw new NotSupportedException(); + + public int BlockProtocolVersion => + throw new NotSupportedException(); + + public IWorld PreviousState { get; } + + public int RandomSeed => + throw new NotSupportedException(); + + public bool IsPolicyAction => + throw new NotSupportedException(); + + public IReadOnlyList Txs => + throw new NotSupportedException(); + + public IReadOnlyList Evidence => + throw new NotSupportedException(); + + 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.Tests/SimpleRPG/Actions/AvatarAction.cs b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/AvatarAction.cs new file mode 100644 index 00000000000..c829d5db068 --- /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(Text args) + { + string name = args; + Call(nameof(InfoAction.Create), new object?[] { name }); + Call(nameof(InventoryAction.Create)); + } + + [Callable] + public Avatar GetAvatar(Address address) + { + Info info = Call( + nameof(InfoAction.GetInfo), + new object?[] { address }); + Inventory inventory = Call( + nameof(InventoryAction.GetInventory), + new object?[] { address }); + return new Avatar(info, inventory); + } + + [Callable] + public void SetAvatar(Address address, Avatar avatar) + { + Call( + nameof(InfoAction.SetInfo), + new object?[] { address, avatar.Info }); + Call( + 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 new file mode 100644 index 00000000000..a03e90b04b4 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleRPG/Actions/FarmAction.cs @@ -0,0 +1,31 @@ +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() + { + Avatar avatar = Call( + nameof(AvatarAction.GetAvatar), + new object?[] { Signer }); + avatar.Info.AddExp(ExpPerFarm); + avatar.Inventory.AddGold(GoldPerFarm); + Call( + nameof(AvatarAction.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..ed5d91b33bc --- /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("exec", "Create") + .Add("args", List.Empty.Add("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("exec", "Farm") + .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 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("exec", "Create") + .Add("args", List.Empty.Add("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("exec", "Create") + .Add("args", List.Empty.Add("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); + } + } +} 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..e6588a07cc5 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleTools/Actions/CalculatorAction.cs @@ -0,0 +1,57 @@ +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("Adds two numbers.")] + 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 }); + 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("Squares the number.")] + public void Square( + [Parameter("The number to sqaure.")]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..acdf4aaf06b --- /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("Converts a string to uppercase.")] + 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..281562ad9c8 --- /dev/null +++ b/Libplanet.SDK.Action.Tests/SimpleTools/SimpleTools.cs @@ -0,0 +1,360 @@ +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("description", "Adds two numbers.")))) + .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("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( + 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 new file mode 100644 index 00000000000..4caf41a56c3 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase.API.cs @@ -0,0 +1,400 @@ +using System.Reflection; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; + +namespace Libplanet.SDK.Action +{ + public partial class ActionBase + { + 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(plainValue)); + if (methodInfo.GetCustomAttribute() is null) + { + throw new ArgumentException( + $"Target method is missing a {nameof(ExecutableAttribute)}.", + 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 \"args\" for {nameof(plainValue)} should be " + + $"{paramInfos.Length}: {arguments.Count}", + nameof(plainValue)); + } + + 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} of \"args\" for {nameof(plainValue)} " + + $"should be {expectedType}: {actualType}", + nameof(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)); + 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; + var prefixItems = List.Empty; + foreach (var paramInfo in paramInfos) + { + Dictionary prefixItem = Dictionary.Empty; + if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Boolean))) + { + prefixItem = prefixItem.Add("type", "boolean"); + } + else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Integer))) + { + prefixItem = prefixItem.Add("type", "integer"); + } + else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Text))) + { + prefixItem = prefixItem.Add("type", "text"); + } + else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.List))) + { + prefixItem = prefixItem.Add("type", "list"); + } + else if (paramInfo.ParameterType.Equals(typeof(Bencodex.Types.Dictionary))) + { + prefixItem = prefixItem.Add("type", "dictionary"); + } + else + { + throw new ArgumentException( + $"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; + 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", + Dictionary.Empty + .Add( + "properties", + Dictionary.Empty + .Add("type_id", typeIdValue) + .Add("exec", execValue))) + .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)}: {actionType}", + 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, Type baseType) + { + List targetTypes = assembly + .GetTypes() + .Where(type => baseType.IsAssignableFrom(type)) + .Where(type => type.GetCustomAttribute() is { }) + .ToList(); +#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 = result.Add(constraint); + } + } + + return result; + } + + 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") + .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, 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); + + protected void SetState(Address address, IValue value) + { + _world = World.SetAccount( + StorageAddress, + World.GetAccount(StorageAddress).SetState(address, value)); + } + + protected void 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 ArgumentException( + $"Method named {methodName} cannot be found."); + if (methodInfo.GetCustomAttribute() is null) + { + throw new ArgumentException( + $"Target method {methodName} is missing a {nameof(CallableAttribute)}"); + } + + methodInfo.Invoke(calledAction, args); + + _world = calledAction._world; + _actionContext = calledAction._actionContext; + } + + protected TR Call(string methodName, object?[]? args = null) + where TA : ActionBase + { + if (Activator.CreateInstance(typeof(TA)) is not TA calledAction) + { + throw new Exception("Action cannot be found."); + } + + calledAction.LoadContext(ActionContext, World); + + 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)}"); + } + + var result = methodInfo.Invoke(calledAction, args); + if (result is not TR typedResult) + { + throw new Exception( + $"Return type is expected to be {typeof(TR)}: {result?.GetType()}"); + } + + _world = calledAction._world; + _actionContext = calledAction._actionContext; + + return typedResult; + } + } +} diff --git a/Libplanet.SDK.Action/Action/ActionBase.Context.cs b/Libplanet.SDK.Action/Action/ActionBase.Context.cs new file mode 100644 index 00000000000..24f6a6d9d26 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase.Context.cs @@ -0,0 +1,11 @@ +using Libplanet.Crypto; + +namespace Libplanet.SDK.Action +{ + public partial class ActionBase + { + protected Address Signer => ActionContext.Signer; + + protected Address Miner => ActionContext.Miner; + } +} diff --git a/Libplanet.SDK.Action/Action/ActionBase.Fields.cs b/Libplanet.SDK.Action/Action/ActionBase.Fields.cs new file mode 100644 index 00000000000..c3cbcf8a234 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase.Fields.cs @@ -0,0 +1,20 @@ +using System.Security; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; + +namespace Libplanet.SDK.Action +{ + public partial class ActionBase + { + private string? _name = null; + private IValue? _args = null; + private string? _exec = null; + + [SecurityCritical] + private IActionContext? _actionContext = null; + + [SecurityCritical] + 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..ee57e57aea8 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase.Params.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.SDK.Action.Attributes; + +namespace Libplanet.SDK.Action +{ + public partial class ActionBase + { + public abstract Address StorageAddress { get; } + + private MethodInfo[] ExecutableMethods => GetType() + .GetMethods() + .Where(methodInfo => methodInfo.IsPublic) + .Where(methodInfo => methodInfo.GetCustomAttribute() is { }) + .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 { } && _exec is { } && _name is { }; + } +} diff --git a/Libplanet.SDK.Action/Action/ActionBase.cs b/Libplanet.SDK.Action/Action/ActionBase.cs new file mode 100644 index 00000000000..beee0d36e26 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionBase.cs @@ -0,0 +1,66 @@ +using System.Reflection; +using System.Security; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; + +[assembly: SecurityTransparent] + +namespace Libplanet.SDK.Action +{ + public abstract partial class ActionBase : IAction + { + public IValue PlainValue => Dictionary.Empty + .Add("type_id", _name ?? 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"]; + _exec = (Text)dict["exec"]; + _args = dict["args"]; + } + + public IWorld Execute(IActionContext context) + { + if (!Loaded) + { + throw new InvalidOperationException("Action is not loaded."); + } + + _actionContext = context; + _world = context.PreviousState; + + MethodInfo method = ExecutableMethods.FirstOrDefault(m => m.Name == _exec) ?? + throw new InvalidOperationException($"Method {_exec} is not found."); + 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)); + } + } + } +} diff --git a/Libplanet.SDK.Action/Action/ActionSchema.json b/Libplanet.SDK.Action/Action/ActionSchema.json new file mode 100644 index 00000000000..7a23c6b2570 --- /dev/null +++ b/Libplanet.SDK.Action/Action/ActionSchema.json @@ -0,0 +1,8 @@ +{ + "type_id": "Bar", + "contract_address": "", + "values": { + "call": "HelloWorld", + "args": ["Alice", "1000"] + } +} 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..f0dda2ff052 --- /dev/null +++ b/Libplanet.SDK.Action/Attributes/ExecutableAttribute.cs @@ -0,0 +1,13 @@ +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; } + } +} 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; } + } +} 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..279bc17bd3d 100644 --- a/Libplanet.sln +++ b/Libplanet.sln @@ -69,6 +69,18 @@ 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 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{96149248-DA70-495F-BD37-2EF0DAB77322}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -613,6 +625,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 +696,11 @@ 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} + {96149248-DA70-495F-BD37-2EF0DAB77322} = {AA37E05B-D531-4546-A259-CDB5AC188E8A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DB552D2A-94E1-4A1C-9F3E-E0097C6158CD} 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")] diff --git a/src/Libplanet.Action/Loader/IActionLoader.cs b/src/Libplanet.Action/Loader/IActionLoader.cs index f280017439a..74d00ebc487 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 { 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() {