diff --git a/.Lib9c.Tests/Action/ClaimItemsTest.cs b/.Lib9c.Tests/Action/ClaimItemsTest.cs new file mode 100644 index 0000000000..b29660ea64 --- /dev/null +++ b/.Lib9c.Tests/Action/ClaimItemsTest.cs @@ -0,0 +1,218 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Model; + using Nekoyume.Model.State; + using Serilog; + using Xunit; + using Xunit.Abstractions; + + public class ClaimItemsTest + { + private readonly IAccount _initialState; + private readonly Address _signerAddress; + + private readonly TableSheets _tableSheets; + private readonly List _currencies; + private readonly List _itemIds; + + public ClaimItemsTest(ITestOutputHelper outputHelper) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(outputHelper) + .CreateLogger(); + + _initialState = new MockStateDelta(); + + var sheets = TableSheetsImporter.ImportSheets(); + foreach (var (key, value) in sheets) + { + _initialState = _initialState + .SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _tableSheets = new TableSheets(sheets); + _itemIds = _tableSheets.CostumeItemSheet.Values.Take(3).Select(x => x.Id).ToList(); + _currencies = _itemIds.Select(id => Currency.Legacy($"Item_T_{id}", 0, minters: null)).ToList(); + + _signerAddress = new PrivateKey().ToAddress(); + + var context = new ActionContext(); + _initialState = _initialState + .MintAsset(context, _signerAddress, _currencies[0] * 5) + .MintAsset(context, _signerAddress, _currencies[1] * 5) + .MintAsset(context, _signerAddress, _currencies[2] * 5); + } + + [Fact] + public void Serialize() + { + var states = GenerateAvatar(_initialState, out var avatarAddress1); + GenerateAvatar(states, out var avatarAddress2); + + var action = new ClaimItems(new List<(Address, IReadOnlyList)> + { + (avatarAddress1, new List { _currencies[0] * 1, _currencies[1] * 1 }), + (avatarAddress2, new List { _currencies[0] * 1 }), + }); + var deserialized = new ClaimItems(); + deserialized.LoadPlainValue(action.PlainValue); + + var orderedClaimData = action.ClaimData.OrderBy(x => x.address).ToList(); + + foreach (var i in Enumerable.Range(0, 2)) + { + Assert.Equal(orderedClaimData[i].address, deserialized.ClaimData[i].address); + Assert.True(orderedClaimData[i].fungibleAssetValues + .SequenceEqual(deserialized.ClaimData[i].fungibleAssetValues)); + } + } + + [Fact] + public void Execute_Throws_ArgumentException_TickerInvalid() + { + var state = GenerateAvatar(_initialState, out var recipientAvatarAddress); + + var currency = Currencies.Crystal; + var action = new ClaimItems(new List<(Address, IReadOnlyList)> + { + (recipientAvatarAddress, new List { currency * 1 }), + }); + Assert.Throws(() => + action.Execute(new ActionContext + { + PreviousState = state, + Signer = _signerAddress, + BlockIndex = 100, + Random = new TestRandom(), + })); + } + + [Fact] + public void Execute_Throws_WhenNotEnoughBalance() + { + var state = GenerateAvatar(_initialState, out var recipientAvatarAddress); + + var currency = _currencies.First(); + var action = new ClaimItems(new List<(Address, IReadOnlyList)> + { + (recipientAvatarAddress, new List { currency * 6 }), + }); + Assert.Throws(() => + action.Execute(new ActionContext + { + PreviousState = state, + Signer = _signerAddress, + BlockIndex = 100, + Random = new TestRandom(), + })); + } + + [Fact] + public void Execute() + { + var state = GenerateAvatar(_initialState, out var recipientAvatarAddress); + + var fungibleAssetValues = _currencies.Select(currency => currency * 1).ToList(); + var action = new ClaimItems(new List<(Address, IReadOnlyList)> + { + (recipientAvatarAddress, fungibleAssetValues), + }); + var states = action.Execute(new ActionContext + { + PreviousState = state, + Signer = _signerAddress, + BlockIndex = 0, + Random = new TestRandom(), + }); + + var inventory = states.GetInventory(recipientAvatarAddress.Derive(SerializeKeys.LegacyInventoryKey)); + foreach (var i in Enumerable.Range(0, 3)) + { + Assert.Equal(_currencies[i] * 4, states.GetBalance(_signerAddress, _currencies[i])); + Assert.Equal( + 1, + inventory.Items.First(x => x.item.Id == _itemIds[i]).count); + } + } + + [Fact] + public void Execute_WithMultipleRecipients() + { + var state = GenerateAvatar(_initialState, out var recipientAvatarAddress1); + state = GenerateAvatar(state, out var recipientAvatarAddress2); + + var recipientAvatarAddresses = new List
+ { + recipientAvatarAddress1, recipientAvatarAddress2, + }; + var fungibleAssetValues = _currencies.Select(currency => currency * 1).ToList(); + + var action = new ClaimItems(new List<(Address, IReadOnlyList)> + { + (recipientAvatarAddress1, fungibleAssetValues.Take(2).ToList()), + (recipientAvatarAddress2, fungibleAssetValues), + }); + + var states = action.Execute(new ActionContext + { + PreviousState = state, + Signer = _signerAddress, + BlockIndex = 0, + Random = new TestRandom(), + }); + + Assert.Equal(states.GetBalance(_signerAddress, _currencies[0]), _currencies[0] * 3); + Assert.Equal(states.GetBalance(_signerAddress, _currencies[1]), _currencies[1] * 3); + Assert.Equal(states.GetBalance(_signerAddress, _currencies[2]), _currencies[2] * 4); + + var inventory1 = states.GetInventory(recipientAvatarAddress1.Derive(SerializeKeys.LegacyInventoryKey)); + Assert.Equal(1, inventory1.Items.First(x => x.item.Id == _itemIds[0]).count); + Assert.Equal(1, inventory1.Items.First(x => x.item.Id == _itemIds[1]).count); + + var inventory2 = states.GetInventory(recipientAvatarAddress2.Derive(SerializeKeys.LegacyInventoryKey)); + Assert.Equal(1, inventory2.Items.First(x => x.item.Id == _itemIds[0]).count); + Assert.Equal(1, inventory2.Items.First(x => x.item.Id == _itemIds[1]).count); + Assert.Equal(1, inventory2.Items.First(x => x.item.Id == _itemIds[2]).count); + } + + private IAccount GenerateAvatar(IAccount state, out Address avatarAddress) + { + var address = new PrivateKey().ToAddress(); + var agentState = new AgentState(address); + avatarAddress = address.Derive("avatar"); + var rankingMapAddress = new PrivateKey().ToAddress(); + var avatarState = new AvatarState( + avatarAddress, + address, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + rankingMapAddress) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.ActionsInShop), + }; + agentState.avatarAddresses[0] = avatarAddress; + + state = state + .SetState(address, agentState.Serialize()) + .SetState(avatarAddress, avatarState.Serialize()) + .SetState( + avatarAddress.Derive(SerializeKeys.LegacyInventoryKey), + avatarState.inventory.Serialize()); + + return state; + } + } +} diff --git a/Lib9c/Action/ClaimItems.cs b/Lib9c/Action/ClaimItems.cs new file mode 100644 index 0000000000..0a162c0140 --- /dev/null +++ b/Lib9c/Action/ClaimItems.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Extensions; +using Nekoyume.Model.Item; +using Nekoyume.Model.State; +using Nekoyume.TableData; +using static Lib9c.SerializeKeys; + +namespace Nekoyume.Action +{ + [ActionType(ActionTypeText)] + public class ClaimItems : GameAction, IClaimItems + { + private const string ActionTypeText = "claim_items"; + + public IReadOnlyList<(Address address, IReadOnlyList fungibleAssetValues)> ClaimData { get; private set; } + + public ClaimItems() + { + } + + public ClaimItems(IReadOnlyList<(Address, IReadOnlyList)> claimData) + { + ClaimData = claimData; + } + + protected override IImmutableDictionary PlainValueInternal => + ImmutableDictionary.Empty + .Add(ClaimDataKey, ClaimData.Select(tuple => + { + var serializedFungibleAssetValues = tuple.fungibleAssetValues.Select(x => x.Serialize()).Serialize(); + + return (tuple.address, serialized: serializedFungibleAssetValues); + }).Serialize()); + + protected override void LoadPlainValueInternal( + IImmutableDictionary plainValue) + { + ClaimData = plainValue[ClaimDataKey].ToStateList() + .Select((tuple => + { + return ( + tuple.Item1, + tuple.Item2.ToList((x => x.ToFungibleAssetValue())) as IReadOnlyList); + })).ToList(); + } + + public override IAccount Execute(IActionContext context) + { + context.UseGas(1); + + var states = context.PreviousState; + var itemSheet = states.GetSheets(containItemSheet: true).GetItemSheet(); + + foreach (var (avatarAddress, fungibleAssetValues) in ClaimData) + { + var inventoryAddress = avatarAddress.Derive(LegacyInventoryKey); + var inventory = states.GetInventory(inventoryAddress) + ?? throw new FailedLoadStateException( + ActionTypeText, + GetSignerAndOtherAddressesHex(context, inventoryAddress), + typeof(Inventory), + inventoryAddress); + + foreach (var fungibleAssetValue in fungibleAssetValues) + { + if (fungibleAssetValue.Currency.DecimalPlaces != 0) + { + throw new ArgumentException( + $"DecimalPlaces of fungibleAssetValue for claimItems are not 0: {fungibleAssetValue.Currency.Ticker}"); + } + + var parsedTicker = fungibleAssetValue.Currency.Ticker.Split("_"); + if (parsedTicker.Length != 3 + || parsedTicker[0] != "Item" + || (parsedTicker[1] != "NT" && parsedTicker[1] != "T") + || !int.TryParse(parsedTicker[2], out var itemId)) + { + throw new ArgumentException( + $"Format of Amount currency's ticker is invalid"); + } + + states = states.BurnAsset(context, context.Signer, fungibleAssetValue); + + var item = itemSheet[itemId] switch + { + MaterialItemSheet.Row materialRow => parsedTicker[1] == "T" + ? ItemFactory.CreateTradableMaterial(materialRow) + : ItemFactory.CreateMaterial(materialRow), + var itemRow => ItemFactory.CreateItem(itemRow, context.Random) + }; + + inventory.AddItem(item, (int)fungibleAssetValue.RawValue); + } + + states = states.SetState(inventoryAddress, inventory.Serialize()); + } + + return states; + } + } +} diff --git a/Lib9c/Action/IClaimItems.cs b/Lib9c/Action/IClaimItems.cs new file mode 100644 index 0000000000..47cdc86745 --- /dev/null +++ b/Lib9c/Action/IClaimItems.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Action +{ + public interface IClaimItems + { + public IReadOnlyList<(Address address, IReadOnlyList fungibleAssetValues)> ClaimData { get; } + } +} diff --git a/Lib9c/SerializeKeys.cs b/Lib9c/SerializeKeys.cs index f1e5daebab..b7cd9dda83 100644 --- a/Lib9c/SerializeKeys.cs +++ b/Lib9c/SerializeKeys.cs @@ -174,5 +174,8 @@ public static class SerializeKeys // Grand Finale public const string GrandFinaleIdKey = "gfi"; + + // ClaimItems + public const string ClaimDataKey = "cd"; } }