From 7298f4b8b739e3dbe1da2f094c2f4f1d69eaf681 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Thu, 14 Mar 2024 12:29:17 +0900 Subject: [PATCH 01/16] Restore mistakenly removed tests since release 1.6.0 --- .../Action/CombinationConsumable8Test.cs | 147 +++ .Lib9c.Tests/Action/CreateAvatar10Test.cs | 307 ++++++ .../Action/EventDungeonBattleV5Test.cs | 499 +++++++++ .Lib9c.Tests/Action/HackAndSlashSweep9Test.cs | 985 ++++++++++++++++++ .Lib9c.Tests/Action/ItemEnhancement13Test.cs | 381 +++++++ .Lib9c.Tests/Action/Raid6Test.cs | 636 +++++++++++ .Lib9c.Tests/Action/RapidCombination9Test.cs | 684 ++++++++++++ 7 files changed, 3639 insertions(+) create mode 100644 .Lib9c.Tests/Action/CombinationConsumable8Test.cs create mode 100644 .Lib9c.Tests/Action/CreateAvatar10Test.cs create mode 100644 .Lib9c.Tests/Action/EventDungeonBattleV5Test.cs create mode 100644 .Lib9c.Tests/Action/HackAndSlashSweep9Test.cs create mode 100644 .Lib9c.Tests/Action/ItemEnhancement13Test.cs create mode 100644 .Lib9c.Tests/Action/Raid6Test.cs create mode 100644 .Lib9c.Tests/Action/RapidCombination9Test.cs diff --git a/.Lib9c.Tests/Action/CombinationConsumable8Test.cs b/.Lib9c.Tests/Action/CombinationConsumable8Test.cs new file mode 100644 index 0000000000..90fc3bb85d --- /dev/null +++ b/.Lib9c.Tests/Action/CombinationConsumable8Test.cs @@ -0,0 +1,147 @@ +namespace Lib9c.Tests.Action +{ + using System.Globalization; + using System.Linq; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Xunit; + using static Lib9c.SerializeKeys; + + public class CombinationConsumable8Test + { + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly IRandom _random; + private readonly TableSheets _tableSheets; + private IAccount _initialState; + + public CombinationConsumable8Test() + { + _agentAddress = new PrivateKey().Address; + _avatarAddress = _agentAddress.Derive("avatar"); + var slotAddress = _avatarAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0 + ) + ); + var sheets = TableSheetsImporter.ImportSheets(); + _random = new TestRandom(); + _tableSheets = new TableSheets(sheets); + + var agentState = new AgentState(_agentAddress); + agentState.avatarAddresses[0] = _avatarAddress; + + var gameConfigState = new GameConfigState(); + + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 1, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var gold = new GoldCurrencyState(Currency.Legacy("NCG", 2, null)); +#pragma warning restore CS0618 + + _initialState = new Account(MockState.Empty) + .SetState(_agentAddress, agentState.Serialize()) + .SetState(_avatarAddress, avatarState.Serialize()) + .SetState( + slotAddress, + new CombinationSlotState( + slotAddress, + GameConfig.RequireClearedStageLevel.CombinationConsumableAction).Serialize()) + .SetState(GameConfigState.Address, gold.Serialize()); + + foreach (var (key, value) in sheets) + { + _initialState = + _initialState.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Execute(bool backward) + { + var avatarState = _initialState.GetAvatarState(_avatarAddress); + var row = _tableSheets.ConsumableItemRecipeSheet.Values.First(); + var costActionPoint = row.RequiredActionPoint; + foreach (var materialInfo in row.Materials) + { + var materialRow = _tableSheets.MaterialItemSheet[materialInfo.Id]; + var material = ItemFactory.CreateItem(materialRow, _random); + avatarState.inventory.AddItem(material, materialInfo.Count); + } + + var previousActionPoint = avatarState.actionPoint; + var previousResultConsumableCount = + avatarState.inventory.Equipments.Count(e => e.Id == row.ResultConsumableItemId); + var previousMailCount = avatarState.mailBox.Count; + + avatarState.worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.CombinationConsumableAction); + + IAccount previousState; + if (backward) + { + previousState = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + previousState = _initialState + .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) + .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) + .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) + .SetState(_avatarAddress, avatarState.SerializeV2()); + } + + var action = new CombinationConsumable8 + { + avatarAddress = _avatarAddress, + recipeId = row.Id, + slotIndex = 0, + }; + + var nextState = action.Execute(new ActionContext + { + PreviousState = previousState, + Signer = _agentAddress, + BlockIndex = 1, + RandomSeed = _random.Seed, + }); + + var slotState = nextState.GetCombinationSlotState(_avatarAddress, 0); + Assert.NotNull(slotState.Result); + Assert.NotNull(slotState.Result.itemUsable); + + var consumable = (Consumable)slotState.Result.itemUsable; + Assert.NotNull(consumable); + + var nextAvatarState = nextState.GetAvatarStateV2(_avatarAddress); + Assert.Equal(previousActionPoint - costActionPoint, nextAvatarState.actionPoint); + Assert.Equal(previousMailCount + 1, nextAvatarState.mailBox.Count); + Assert.IsType(nextAvatarState.mailBox.First()); + Assert.Equal( + previousResultConsumableCount + 1, + nextAvatarState.inventory.Consumables.Count(e => e.Id == row.ResultConsumableItemId)); + } + } +} diff --git a/.Lib9c.Tests/Action/CreateAvatar10Test.cs b/.Lib9c.Tests/Action/CreateAvatar10Test.cs new file mode 100644 index 0000000000..71a257306d --- /dev/null +++ b/.Lib9c.Tests/Action/CreateAvatar10Test.cs @@ -0,0 +1,307 @@ +namespace Lib9c.Tests.Action +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Runtime.Serialization.Formatters.Binary; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model.State; + using Nekoyume.TableData; + using Xunit; + using static Lib9c.SerializeKeys; + + public class CreateAvatar10Test + { + private readonly Address _agentAddress; + private readonly TableSheets _tableSheets; + + public CreateAvatar10Test() + { + _agentAddress = default; + _tableSheets = new TableSheets(TableSheetsImporter.ImportSheets()); + } + + [Theory] + [InlineData(0L)] + [InlineData(7_210_000L)] + [InlineData(7_210_001L)] + public void Execute(long blockIndex) + { + var action = new CreateAvatar10() + { + index = 0, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + var sheets = TableSheetsImporter.ImportSheets(); + var state = new Account(MockState.Empty) + .SetState( + Addresses.GameConfig, + new GameConfigState(sheets[nameof(GameConfigSheet)]).Serialize() + ); + + foreach (var (key, value) in sheets) + { + state = state.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + Assert.Equal(0 * CrystalCalculator.CRYSTAL, state.GetBalance(_agentAddress, CrystalCalculator.CRYSTAL)); + + var nextState = action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = blockIndex, + RandomSeed = 0, + }); + + var avatarAddress = _agentAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CreateAvatar2.DeriveFormat, + 0 + ) + ); + Assert.True(nextState.TryGetAgentAvatarStatesV2( + default, + avatarAddress, + out var agentState, + out var nextAvatarState, + out _) + ); + Assert.True(agentState.avatarAddresses.Any()); + Assert.Equal("test", nextAvatarState.name); + Assert.Equal(200_000 * CrystalCalculator.CRYSTAL, nextState.GetBalance(_agentAddress, CrystalCalculator.CRYSTAL)); + var avatarItemSheet = nextState.GetSheet(); + foreach (var row in avatarItemSheet.Values) + { + Assert.True(nextAvatarState.inventory.HasItem(row.ItemId, row.Count)); + } + + var avatarFavSheet = nextState.GetSheet(); + foreach (var row in avatarFavSheet.Values) + { + var targetAddress = row.Target == CreateAvatarFavSheet.Target.Agent + ? _agentAddress + : avatarAddress; + Assert.Equal(row.Currency * row.Quantity, nextState.GetBalance(targetAddress, row.Currency)); + } + } + + [Theory] + [InlineData("홍길동")] + [InlineData("山田太郎")] + public void ExecuteThrowInvalidNamePatterException(string nickName) + { + var agentAddress = default(Address); + + var action = new CreateAvatar10() + { + index = 0, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = nickName, + }; + + var state = new Account(MockState.Empty); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = agentAddress, + BlockIndex = 0, + }) + ); + } + + [Fact] + public void ExecuteThrowInvalidAddressException() + { + var avatarAddress = _agentAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CreateAvatar2.DeriveFormat, + 0 + ) + ); + + var avatarState = new AvatarState( + avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + default + ); + + var action = new CreateAvatar10() + { + index = 0, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + var state = new Account(MockState.Empty).SetState(avatarAddress, avatarState.Serialize()); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = 0, + }) + ); + } + + [Theory] + [InlineData(-1)] + [InlineData(3)] + public void ExecuteThrowAvatarIndexOutOfRangeException(int index) + { + var agentState = new AgentState(_agentAddress); + var state = new Account(MockState.Empty).SetState(_agentAddress, agentState.Serialize()); + var action = new CreateAvatar10() + { + index = index, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = 0, + }) + ); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void ExecuteThrowAvatarIndexAlreadyUsedException(int index) + { + var agentState = new AgentState(_agentAddress); + var avatarAddress = _agentAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CreateAvatar2.DeriveFormat, + 0 + ) + ); + agentState.avatarAddresses[index] = avatarAddress; + var state = new Account(MockState.Empty).SetState(_agentAddress, agentState.Serialize()); + + var action = new CreateAvatar10() + { + index = index, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = 0, + }) + ); + } + + [Fact] + public void Serialize_With_DotnetAPI() + { + var formatter = new BinaryFormatter(); + var action = new CreateAvatar10() + { + index = 2, + hair = 1, + ear = 4, + lens = 5, + tail = 7, + name = "test", + }; + + using var ms = new MemoryStream(); + formatter.Serialize(ms, action); + + ms.Seek(0, SeekOrigin.Begin); + var deserialized = (CreateAvatar10)formatter.Deserialize(ms); + + Assert.Equal(2, deserialized.index); + Assert.Equal(1, deserialized.hair); + Assert.Equal(4, deserialized.ear); + Assert.Equal(5, deserialized.lens); + Assert.Equal(7, deserialized.tail); + Assert.Equal("test", deserialized.name); + } + + [Fact] + public void AddItem() + { + var itemSheet = _tableSheets.ItemSheet; + var createAvatarItemSheet = new CreateAvatarItemSheet(); + createAvatarItemSheet.Set(@"item_id,count +10112000,2 +10512000,2 +600201,2 +"); + var avatarState = new AvatarState(default, default, 0L, _tableSheets.GetAvatarSheets(), new GameConfigState(), default, "test"); + CreateAvatar10.AddItem(itemSheet, createAvatarItemSheet, avatarState, new TestRandom()); + foreach (var row in createAvatarItemSheet.Values) + { + Assert.True(avatarState.inventory.HasItem(row.ItemId, row.Count)); + } + + Assert.Equal(4, avatarState.inventory.Equipments.Count()); + foreach (var equipment in avatarState.inventory.Equipments) + { + var equipmentRow = _tableSheets.EquipmentItemSheet[equipment.Id]; + Assert.Equal(equipmentRow.Stat, equipment.Stat); + } + } + + [Fact] + public void MintAsset() + { + var createAvatarFavSheet = new CreateAvatarFavSheet(); + createAvatarFavSheet.Set(@"currency,quantity,target +CRYSTAL,200000,Agent +RUNE_GOLDENLEAF,200000,Avatar +"); + var avatarAddress = new PrivateKey().Address; + var agentAddress = new PrivateKey().Address; + var avatarState = new AvatarState(avatarAddress, agentAddress, 0L, _tableSheets.GetAvatarSheets(), new GameConfigState(), default, "test"); + var nextState = CreateAvatar10.MintAsset(createAvatarFavSheet, avatarState, new Account(MockState.Empty), new ActionContext()); + foreach (var row in createAvatarFavSheet.Values) + { + var targetAddress = row.Target == CreateAvatarFavSheet.Target.Agent + ? agentAddress + : avatarAddress; + Assert.Equal(row.Currency * row.Quantity, nextState.GetBalance(targetAddress, row.Currency)); + } + } + } +} diff --git a/.Lib9c.Tests/Action/EventDungeonBattleV5Test.cs b/.Lib9c.Tests/Action/EventDungeonBattleV5Test.cs new file mode 100644 index 0000000000..07197699ad --- /dev/null +++ b/.Lib9c.Tests/Action/EventDungeonBattleV5Test.cs @@ -0,0 +1,499 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Blockchain.Policy; + using Nekoyume.Exceptions; + using Nekoyume.Extensions; + using Nekoyume.Model.Event; + using Nekoyume.Model.Rune; + using Nekoyume.Model.State; + using Nekoyume.TableData; + using Nekoyume.TableData.Event; + using Xunit; + using static Lib9c.SerializeKeys; + + public class EventDungeonBattleV5Test + { + private readonly Currency _ncgCurrency; + private readonly TableSheets _tableSheets; + + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private IAccount _initialStates; + + public EventDungeonBattleV5Test() + { + _initialStates = new Account(MockState.Empty); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _ncgCurrency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + _initialStates = _initialStates.SetState( + GoldCurrencyState.Address, + new GoldCurrencyState(_ncgCurrency).Serialize()); + var sheets = TableSheetsImporter.ImportSheets(); + foreach (var (key, value) in sheets) + { + _initialStates = _initialStates + .SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _tableSheets = new TableSheets(sheets); + + _agentAddress = new PrivateKey().Address; + _avatarAddress = _agentAddress.Derive("avatar"); + var inventoryAddr = _avatarAddress.Derive(LegacyInventoryKey); + var worldInformationAddr = _avatarAddress.Derive(LegacyWorldInformationKey); + var questListAddr = _avatarAddress.Derive(LegacyQuestListKey); + + var agentState = new AgentState(_agentAddress); + agentState.avatarAddresses.Add(0, _avatarAddress); + + var gameConfigState = new GameConfigState(sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + new PrivateKey().Address + ) + { + level = 100, + }; + + _initialStates = _initialStates + .SetState(_agentAddress, agentState.Serialize()) + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState(inventoryAddr, avatarState.inventory.Serialize()) + .SetState(worldInformationAddr, avatarState.worldInformation.Serialize()) + .SetState(questListAddr, avatarState.questList.Serialize()) + .SetState(gameConfigState.address, gameConfigState.Serialize()); + } + + [Theory] + [InlineData(1001, 10010001, 10010001)] + public void Execute_Success_Within_Event_Period( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + var contextBlockIndex = scheduleRow.StartBlockIndex; + var nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = + new EventDungeonInfo(nextStates.GetState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + + contextBlockIndex = scheduleRow.DungeonEndBlockIndex; + nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + eventDungeonInfo = + new EventDungeonInfo(nextStates.GetState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + } + + [Theory] + [InlineData(1001, 10010001, 10010001, 0, 0, 0)] + [InlineData(1001, 10010001, 10010001, 1, 1, 1)] + [InlineData(1001, 10010001, 10010001, int.MaxValue, int.MaxValue, int.MaxValue - 1)] + public void Execute_Success_With_Ticket_Purchase( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId, + int dungeonTicketPrice, + int dungeonTicketAdditionalPrice, + int numberOfTicketPurchases) + { + var context = new ActionContext(); + var previousStates = _initialStates; + var scheduleSheet = _tableSheets.EventScheduleSheet; + Assert.True(scheduleSheet.TryGetValue(eventScheduleId, out var scheduleRow)); + var sb = new StringBuilder(); + sb.AppendLine( + "id,_name,start_block_index,dungeon_end_block_index,dungeon_tickets_max,dungeon_tickets_reset_interval_block_range,dungeon_exp_seed_value,recipe_end_block_index,dungeon_ticket_price,dungeon_ticket_additional_price"); + sb.AppendLine( + $"{eventScheduleId}" + + $",\"2022 Summer Event\"" + + $",{scheduleRow.StartBlockIndex}" + + $",{scheduleRow.DungeonEndBlockIndex}" + + $",{scheduleRow.DungeonTicketsMax}" + + $",{scheduleRow.DungeonTicketsResetIntervalBlockRange}" + + $",{dungeonTicketPrice}" + + $",{dungeonTicketAdditionalPrice}" + + $",{scheduleRow.DungeonExpSeedValue}" + + $",{scheduleRow.RecipeEndBlockIndex}"); + previousStates = previousStates.SetState( + Addresses.GetSheetAddress(), + sb.ToString().Serialize()); + + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = new EventDungeonInfo( + remainingTickets: 0, + numberOfTicketPurchases: numberOfTicketPurchases); + previousStates = previousStates.SetState( + eventDungeonInfoAddr, + eventDungeonInfo.Serialize()); + + Assert.True(previousStates.GetSheet() + .TryGetValue(eventScheduleId, out var newScheduleRow)); + var ncgHas = newScheduleRow.GetDungeonTicketCost( + numberOfTicketPurchases, + _ncgCurrency); + if (ncgHas.Sign > 0) + { + previousStates = previousStates.MintAsset(context, _agentAddress, ncgHas); + } + + var nextStates = Execute( + previousStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + buyTicketIfNeeded: true, + blockIndex: scheduleRow.StartBlockIndex); + var nextEventDungeonInfoList = + (Bencodex.Types.List)nextStates.GetState(eventDungeonInfoAddr)!; + Assert.Equal( + numberOfTicketPurchases + 1, + nextEventDungeonInfoList[2].ToInteger()); + Assert.True( + nextStates.TryGetGoldBalance( + _agentAddress, + _ncgCurrency, + out FungibleAssetValue balance + ) + ); + Assert.Equal(0 * _ncgCurrency, balance); + } + + [Theory] + [InlineData(10000001, 10010001, 10010001)] + [InlineData(10010001, 10010001, 10010001)] + public void Execute_Throw_InvalidActionFieldException_By_EventScheduleId( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) => + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId)); + + [Theory] + [InlineData(1001, 10010001, 10010001)] + public void Execute_Throw_InvalidActionFieldException_By_ContextBlockIndex( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + var contextBlockIndex = scheduleRow.StartBlockIndex - 1; + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex)); + contextBlockIndex = scheduleRow.DungeonEndBlockIndex + 1; + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex)); + } + + [Theory] + [InlineData(1001, 10020001, 10010001)] + [InlineData(1001, 1001, 10010001)] + public void Execute_Throw_InvalidActionFieldException_By_EventDungeonId( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10020001)] + [InlineData(1001, 10010001, 1001)] + public void Execute_Throw_InvalidActionFieldException_By_EventDungeonStageId( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10010001)] + public void Execute_Throw_NotEnoughEventDungeonTicketsException( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + var previousStates = _initialStates; + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = new EventDungeonInfo(); + previousStates = previousStates + .SetState(eventDungeonInfoAddr, eventDungeonInfo.Serialize()); + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + previousStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10010001, 0)] + [InlineData(1001, 10010001, 10010001, int.MaxValue - 1)] + public void Execute_Throw_InsufficientBalanceException( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId, + int numberOfTicketPurchases) + { + var context = new ActionContext(); + var previousStates = _initialStates; + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = new EventDungeonInfo( + remainingTickets: 0, + numberOfTicketPurchases: numberOfTicketPurchases); + previousStates = previousStates + .SetState(eventDungeonInfoAddr, eventDungeonInfo.Serialize()); + + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + var ncgHas = scheduleRow.GetDungeonTicketCost( + numberOfTicketPurchases, + _ncgCurrency) - 1 * _ncgCurrency; + if (ncgHas.Sign > 0) + { + previousStates = previousStates.MintAsset(context, _agentAddress, ncgHas); + } + + Assert.Throws(() => + Execute( + previousStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + buyTicketIfNeeded: true, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10010002)] + public void Execute_Throw_StageNotClearedException( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(0, 30001, 1, 30001, typeof(DuplicatedRuneIdException))] + [InlineData(1, 10002, 1, 30001, typeof(DuplicatedRuneSlotIndexException))] + public void Execute_DuplicatedException(int slotIndex, int runeId, int slotIndex2, int runeId2, Type exception) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(1001, out var scheduleRow)); + + var context = new ActionContext(); + _initialStates = _initialStates.MintAsset(context, _agentAddress, 99999 * _ncgCurrency); + + var unlockRuneSlot = new UnlockRuneSlot() + { + AvatarAddress = _avatarAddress, + SlotIndex = 1, + }; + + _initialStates = unlockRuneSlot.Execute(new ActionContext + { + BlockIndex = 1, + PreviousState = _initialStates, + Signer = _agentAddress, + RandomSeed = 0, + }); + + Assert.Throws(exception, () => + Execute( + _initialStates, + 1001, + 10010001, + 10010001, + false, + scheduleRow.StartBlockIndex, + slotIndex, + runeId, + slotIndex2, + runeId2)); + } + + [Fact] + public void Execute_V100301() + { + int eventScheduleId = 1001; + int eventDungeonId = 10010001; + int eventDungeonStageId = 10010001; + var csv = $@"id,_name,start_block_index,dungeon_end_block_index,dungeon_tickets_max,dungeon_tickets_reset_interval_block_range,dungeon_ticket_price,dungeon_ticket_additional_price,dungeon_exp_seed_value,recipe_end_block_index + 1001,2022 Summer Event,{ActionObsoleteConfig.V100301ExecutedBlockIndex},{ActionObsoleteConfig.V100301ExecutedBlockIndex + 100},5,7200,5,2,1,5018000"; + _initialStates = + _initialStates.SetState( + Addresses.GetSheetAddress(), + csv.Serialize()); + var sheet = new EventScheduleSheet(); + sheet.Set(csv); + Assert.True(sheet.TryGetValue(eventScheduleId, out var scheduleRow)); + var contextBlockIndex = scheduleRow.StartBlockIndex; + var nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = + new EventDungeonInfo(nextStates.GetState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + + contextBlockIndex = scheduleRow.DungeonEndBlockIndex; + nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + eventDungeonInfo = + new EventDungeonInfo(nextStates.GetState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + } + + private IAccount Execute( + IAccount previousStates, + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId, + bool buyTicketIfNeeded = false, + long blockIndex = 0, + int slotIndex = 0, + int runeId = 10002, + int slotIndex2 = 1, + int runeId2 = 30001) + { + var previousAvatarState = previousStates.GetAvatarStateV2(_avatarAddress); + var equipments = + Doomfist.GetAllParts(_tableSheets, previousAvatarState.level); + foreach (var equipment in equipments) + { + previousAvatarState.inventory.AddItem(equipment, iLock: null); + } + + var action = new EventDungeonBattleV5 + { + AvatarAddress = _avatarAddress, + EventScheduleId = eventScheduleId, + EventDungeonId = eventDungeonId, + EventDungeonStageId = eventDungeonStageId, + Equipments = equipments + .Select(e => e.NonFungibleId) + .ToList(), + Costumes = new List(), + Foods = new List(), + RuneInfos = new List() + { + new RuneSlotInfo(slotIndex, runeId), + new RuneSlotInfo(slotIndex2, runeId2), + }, + BuyTicketIfNeeded = buyTicketIfNeeded, + }; + + var nextStates = action.Execute(new ActionContext + { + PreviousState = previousStates, + Signer = _agentAddress, + RandomSeed = 0, + BlockIndex = blockIndex, + }); + + Assert.True(nextStates.GetSheet().TryGetValue( + eventScheduleId, + out var scheduleRow)); + var nextAvatarState = nextStates.GetAvatarStateV2(_avatarAddress); + var expectExp = scheduleRow.GetStageExp( + eventDungeonStageId.ToEventDungeonStageNumber()); + Assert.Equal( + previousAvatarState.exp + expectExp, + nextAvatarState.exp); + + return nextStates; + } + } +} diff --git a/.Lib9c.Tests/Action/HackAndSlashSweep9Test.cs b/.Lib9c.Tests/Action/HackAndSlashSweep9Test.cs new file mode 100644 index 0000000000..53a7dafbed --- /dev/null +++ b/.Lib9c.Tests/Action/HackAndSlashSweep9Test.cs @@ -0,0 +1,985 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Bencodex.Types; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Extensions; + using Nekoyume.Helper; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.Rune; + using Nekoyume.Model.State; + using Nekoyume.TableData; + using Xunit; + using static Lib9c.SerializeKeys; + + public class HackAndSlashSweep9Test + { + private readonly Dictionary _sheets; + private readonly TableSheets _tableSheets; + + private readonly Address _agentAddress; + + private readonly Address _avatarAddress; + private readonly AvatarState _avatarState; + + private readonly Address _inventoryAddress; + private readonly Address _worldInformationAddress; + private readonly Address _questListAddress; + + private readonly Address _rankingMapAddress; + + private readonly WeeklyArenaState _weeklyArenaState; + private readonly IAccount _initialState; + private readonly IRandom _random; + + public HackAndSlashSweep9Test() + { + _random = new TestRandom(); + _sheets = TableSheetsImporter.ImportSheets(); + _tableSheets = new TableSheets(_sheets); + + var privateKey = new PrivateKey(); + _agentAddress = privateKey.PublicKey.Address; + var agentState = new AgentState(_agentAddress); + + _avatarAddress = _agentAddress.Derive("avatar"); + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + _rankingMapAddress = _avatarAddress.Derive("ranking_map"); + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress + ) + { + level = 100, + }; + _inventoryAddress = _avatarAddress.Derive(LegacyInventoryKey); + _worldInformationAddress = _avatarAddress.Derive(LegacyWorldInformationKey); + _questListAddress = _avatarAddress.Derive(LegacyQuestListKey); + agentState.avatarAddresses.Add(0, _avatarAddress); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + var goldCurrencyState = new GoldCurrencyState(currency); + _weeklyArenaState = new WeeklyArenaState(0); + _initialState = new Account(MockState.Empty) + .SetState(_weeklyArenaState.address, _weeklyArenaState.Serialize()) + .SetState(_agentAddress, agentState.SerializeV2()) + .SetState(_avatarAddress, _avatarState.SerializeV2()) + .SetState(_inventoryAddress, _avatarState.inventory.Serialize()) + .SetState(_worldInformationAddress, _avatarState.worldInformation.Serialize()) + .SetState(_questListAddress, _avatarState.questList.Serialize()) + .SetState(gameConfigState.address, gameConfigState.Serialize()) + .SetState(Addresses.GoldCurrency, goldCurrencyState.Serialize()); + + foreach (var (key, value) in _sheets) + { + _initialState = _initialState + .SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + foreach (var address in _avatarState.combinationSlotAddresses) + { + var slotState = new CombinationSlotState( + address, + GameConfig.RequireClearedStageLevel.CombinationEquipmentAction); + _initialState = _initialState.SetState(address, slotState.Serialize()); + } + } + + public (List Equipments, List Costumes) GetDummyItems(AvatarState avatarState) + { + var equipments = Doomfist.GetAllParts(_tableSheets, avatarState.level) + .Select(e => e.NonFungibleId).ToList(); + var random = new TestRandom(); + var costumes = new List(); + if (avatarState.level >= GameConfig.RequireCharacterLevel.CharacterFullCostumeSlot) + { + var costumeId = _tableSheets + .CostumeItemSheet + .Values + .First(r => r.ItemSubType == ItemSubType.FullCostume) + .Id; + + var costume = (Costume)ItemFactory.CreateItem( + _tableSheets.ItemSheet[costumeId], random); + avatarState.inventory.AddItem(costume); + costumes.Add(costume.ItemId); + } + + return (equipments, costumes); + } + + [Theory] + [InlineData(1, 1, 1, false, true)] + [InlineData(1, 1, 1, false, false)] + [InlineData(2, 1, 2, false, true)] + [InlineData(2, 1, 2, false, false)] + [InlineData(2, 2, 51, false, true)] + [InlineData(2, 2, 51, false, false)] + [InlineData(2, 2, 52, false, true)] + [InlineData(2, 2, 52, false, false)] + [InlineData(2, 1, 1, true, true)] + [InlineData(2, 1, 1, true, false)] + [InlineData(2, 1, 2, true, true)] + [InlineData(2, 1, 2, true, false)] + [InlineData(2, 2, 51, true, true)] + [InlineData(2, 2, 51, true, false)] + [InlineData(2, 2, 52, true, true)] + [InlineData(2, 2, 52, true, false)] + public void Execute(int apStoneCount, int worldId, int stageId, bool challenge, bool backward) + { + var gameConfigState = _initialState.GetGameConfigState(); + var prevStageId = stageId - 1; + var worldInformation = new WorldInformation( + 0, _initialState.GetSheet(), challenge ? prevStageId : stageId); + + if (challenge) + { + worldInformation.UnlockWorld(worldId, 0, _tableSheets.WorldSheet); + } + + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = worldInformation, + level = 400, + }; + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.ApStone); + var apStone = ItemFactory.CreateTradableMaterial(row); + avatarState.inventory.AddItem(apStone, apStoneCount); + + IAccount state; + if (backward) + { + state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + state = _initialState + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState( + _avatarAddress.Derive(LegacyInventoryKey), + avatarState.inventory.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyWorldInformationKey), + avatarState.worldInformation.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyQuestListKey), + avatarState.questList.Serialize()); + } + + state = state.SetState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(stageId, out var stageRow)) + { + var itemPlayCount = gameConfigState.ActionPointMax / stageRow.CostAP * apStoneCount; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + stageId, + playCount); + + var random = new TestRandom(_random.Seed); + var expectedRewardItems = HackAndSlashSweep6.GetRewardItems( + random, + playCount, + stageRow, + _tableSheets.MaterialItemSheet); + + var (equipments, costumes) = GetDummyItems(avatarState); + var action = new HackAndSlashSweep9 + { + actionPoint = avatarState.actionPoint, + costumes = costumes, + equipments = equipments, + runeInfos = new List(), + avatarAddress = _avatarAddress, + apStoneCount = apStoneCount, + worldId = worldId, + stageId = stageId, + }; + + state = action.Execute(new ActionContext + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = _random.Seed, + }); + + var nextAvatarState = state.GetAvatarStateV2(_avatarAddress); + + Assert.Equal(expectedLevel, nextAvatarState.level); + Assert.Equal(expectedExp, nextAvatarState.exp); + Assert.Equal( + expectedRewardItems.Count(), + nextAvatarState.inventory.Items.Sum(x => x.count)); + foreach (var i in nextAvatarState.inventory.Items) + { + nextAvatarState.inventory.TryGetItem(i.item.Id, out var item); + Assert.Equal(expectedRewardItems.Count(x => x.Id == i.item.Id), item.count); + } + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Execute_FailedLoadStateException(bool backward) + { + var action = new HackAndSlashSweep9 + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = 1, + stageId = 1, + }; + + var state = backward ? new Account(MockState.Empty) : _initialState; + if (!backward) + { + state = _initialState + .SetState(_avatarAddress, _avatarState.SerializeV2()) + .SetNull(_avatarAddress.Derive(LegacyInventoryKey)) + .SetNull(_avatarAddress.Derive(LegacyWorldInformationKey)) + .SetNull(_avatarAddress.Derive(LegacyQuestListKey)); + } + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(100, 1)] + public void Execute_SheetRowNotFoundException(int worldId, int stageId) + { + var action = new HackAndSlashSweep9 + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + + var state = _initialState.SetState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(1, 999)] + [InlineData(2, 50)] + public void Execute_SheetRowColumnException(int worldId, int stageId) + { + var action = new HackAndSlashSweep9 + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + + var state = _initialState.SetState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(1, 48, 1, 50, true)] + [InlineData(1, 48, 1, 50, false)] + [InlineData(1, 49, 2, 51, true)] + [InlineData(1, 49, 2, 51, false)] + public void Execute_InvalidStageException(int clearedWorldId, int clearedStageId, int worldId, int stageId, bool backward) + { + var action = new HackAndSlashSweep9 + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + var worldSheet = _initialState.GetSheet(); + var worldUnlockSheet = _initialState.GetSheet(); + + _avatarState.worldInformation.ClearStage(clearedWorldId, clearedStageId, 1, worldSheet, worldUnlockSheet); + + var state = _initialState.SetState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + + if (backward) + { + state = state.SetState(_avatarAddress, _avatarState.Serialize()); + } + else + { + state = state + .SetState( + _avatarAddress.Derive(LegacyWorldInformationKey), + _avatarState.worldInformation.Serialize()); + } + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(GameConfig.MimisbrunnrWorldId, true, 10000001, false)] + [InlineData(GameConfig.MimisbrunnrWorldId, false, 10000001, true)] + // Unlock CRYSTAL first. + [InlineData(2, false, 51, false)] + [InlineData(2, true, 51, false)] + public void Execute_InvalidWorldException(int worldId, bool backward, int stageId, bool unlockedIdsExist) + { + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 10000001), + }; + + IAccount state; + if (backward) + { + state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + state = _initialState + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState( + _avatarAddress.Derive(LegacyInventoryKey), + avatarState.inventory.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyWorldInformationKey), + avatarState.worldInformation.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyQuestListKey), + avatarState.questList.Serialize()); + } + + if (unlockedIdsExist) + { + state = state.SetState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + } + + var action = new HackAndSlashSweep9 + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(99, true)] + [InlineData(99, false)] + public void Execute_UsageLimitExceedException(int apStoneCount, bool backward) + { + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + }; + + IAccount state; + if (backward) + { + state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + state = _initialState + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState( + _avatarAddress.Derive(LegacyInventoryKey), + avatarState.inventory.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyWorldInformationKey), + avatarState.worldInformation.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyQuestListKey), + avatarState.questList.Serialize()); + } + + var action = new HackAndSlashSweep9 + { + runeInfos = new List(), + apStoneCount = apStoneCount, + avatarAddress = _avatarAddress, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(3, 2, true)] + [InlineData(7, 5, false)] + public void Execute_NotEnoughMaterialException(int useApStoneCount, int holdingApStoneCount, bool backward) + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + level = 400, + }; + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.ApStone); + var apStone = ItemFactory.CreateTradableMaterial(row); + avatarState.inventory.AddItem(apStone, holdingApStoneCount); + + IAccount state; + if (backward) + { + state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + state = _initialState + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState( + _avatarAddress.Derive(LegacyInventoryKey), + avatarState.inventory.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyWorldInformationKey), + avatarState.worldInformation.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyQuestListKey), + avatarState.questList.Serialize()); + } + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(2, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * useApStoneCount; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + 2, + playCount); + + var (equipments, costumes) = GetDummyItems(avatarState); + + var action = new HackAndSlashSweep9 + { + equipments = equipments, + costumes = costumes, + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = useApStoneCount, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Execute_NotEnoughActionPointException(bool backward) + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + level = 400, + actionPoint = 0, + }; + + IAccount state; + if (backward) + { + state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + state = _initialState + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState( + _avatarAddress.Derive(LegacyInventoryKey), + avatarState.inventory.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyWorldInformationKey), + avatarState.worldInformation.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyQuestListKey), + avatarState.questList.Serialize()); + } + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(2, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * 1; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + 2, + playCount); + + var (equipments, costumes) = GetDummyItems(avatarState); + var action = new HackAndSlashSweep9 + { + runeInfos = new List(), + costumes = costumes, + equipments = equipments, + avatarAddress = _avatarAddress, + actionPoint = 999999, + apStoneCount = 0, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => + action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Execute_PlayCountIsZeroException(bool backward) + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + level = 400, + actionPoint = 0, + }; + + IAccount state; + if (backward) + { + state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + state = _initialState + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState( + _avatarAddress.Derive(LegacyInventoryKey), + avatarState.inventory.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyWorldInformationKey), + avatarState.worldInformation.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyQuestListKey), + avatarState.questList.Serialize()); + } + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(2, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * 1; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + 2, + playCount); + + var (equipments, costumes) = GetDummyItems(avatarState); + var action = new HackAndSlashSweep9 + { + costumes = costumes, + equipments = equipments, + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = 0, + apStoneCount = 0, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => + action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Theory] + [InlineData(1, 24, true)] + [InlineData(1, 24, false)] + public void Execute_NotEnoughCombatPointException(int worldId, int stageId, bool backward) + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + actionPoint = 0, + level = 1, + }; + + IAccount state; + if (backward) + { + state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + state = _initialState + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState( + _avatarAddress.Derive(LegacyInventoryKey), + avatarState.inventory.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyWorldInformationKey), + avatarState.worldInformation.Serialize()) + .SetState( + _avatarAddress.Derive(LegacyQuestListKey), + avatarState.questList.Serialize()); + } + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(stageId, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * 1; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + stageId, + playCount); + + var action = new HackAndSlashSweep9 + { + costumes = new List(), + equipments = new List(), + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = 1, + worldId = worldId, + stageId = stageId, + }; + + Assert.Throws(() => + action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + public void ExecuteWithStake(int stakingLevel) + { + const int worldId = 1; + const int stageId = 1; + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + actionPoint = 120, + level = 3, + }; + var itemRow = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.ApStone); + var apStone = ItemFactory.CreateTradableMaterial(itemRow); + avatarState.inventory.AddItem(apStone); + + var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); + var stakeState = new StakeState(stakeStateAddress, 1); + var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows + .FirstOrDefault(r => r.Level == stakingLevel)?.RequiredGold ?? 0; + var context = new ActionContext(); + var state = _initialState + .SetState(_avatarAddress, avatarState.Serialize()) + .SetState(stakeStateAddress, stakeState.Serialize()) + .MintAsset(context, stakeStateAddress, requiredGold * _initialState.GetGoldCurrency()); + var stageSheet = _initialState.GetSheet(); + if (stageSheet.TryGetValue(stageId, out var stageRow)) + { + var apSheet = _initialState.GetSheet(); + var costAp = apSheet.GetActionPointByStaking(stageRow.CostAP, 1, stakingLevel); + var itemPlayCount = + gameConfigState.ActionPointMax / costAp * 1; + var apPlayCount = avatarState.actionPoint / costAp; + var playCount = apPlayCount + itemPlayCount; + var (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _initialState.GetSheet(), + stageId, + playCount); + + var action = new HackAndSlashSweep9 + { + costumes = new List(), + equipments = new List(), + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = 1, + worldId = worldId, + stageId = stageId, + }; + + var nextState = action.Execute(new ActionContext + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + }); + var nextAvatar = nextState.GetAvatarStateV2(_avatarAddress); + Assert.Equal(expectedLevel, nextAvatar.level); + Assert.Equal(expectedExp, nextAvatar.exp); + } + else + { + throw new SheetRowNotFoundException(nameof(StageSheet), stageId); + } + } + + [Theory] + [InlineData(0, 30001, 1, 30001, typeof(DuplicatedRuneIdException))] + [InlineData(1, 10002, 1, 30001, typeof(DuplicatedRuneSlotIndexException))] + public void ExecuteDuplicatedException(int slotIndex, int runeId, int slotIndex2, int runeId2, Type exception) + { + var stakingLevel = 1; + const int worldId = 1; + const int stageId = 1; + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + actionPoint = 120, + level = 3, + }; + var itemRow = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.ApStone); + var apStone = ItemFactory.CreateTradableMaterial(itemRow); + avatarState.inventory.AddItem(apStone); + + var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); + var stakeState = new StakeState(stakeStateAddress, 1); + var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows + .FirstOrDefault(r => r.Level == stakingLevel)?.RequiredGold ?? 0; + var context = new ActionContext(); + var state = _initialState + .SetState(_avatarAddress, avatarState.Serialize()) + .SetState(stakeStateAddress, stakeState.Serialize()) + .MintAsset(context, stakeStateAddress, requiredGold * _initialState.GetGoldCurrency()); + var stageSheet = _initialState.GetSheet(); + if (stageSheet.TryGetValue(stageId, out var stageRow)) + { + var apSheet = _initialState.GetSheet(); + var costAp = apSheet.GetActionPointByStaking(stageRow.CostAP, 1, stakingLevel); + var itemPlayCount = + gameConfigState.ActionPointMax / costAp * 1; + var apPlayCount = avatarState.actionPoint / costAp; + var playCount = apPlayCount + itemPlayCount; + var (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _initialState.GetSheet(), + stageId, + playCount); + + var ncgCurrency = state.GetGoldCurrency(); + state = state.MintAsset(context, _agentAddress, 99999 * ncgCurrency); + + var unlockRuneSlot = new UnlockRuneSlot() + { + AvatarAddress = _avatarAddress, + SlotIndex = 1, + }; + + state = unlockRuneSlot.Execute(new ActionContext + { + BlockIndex = 1, + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + }); + + var action = new HackAndSlashSweep9 + { + costumes = new List(), + equipments = new List(), + runeInfos = new List() + { + new RuneSlotInfo(slotIndex, runeId), + new RuneSlotInfo(slotIndex2, runeId2), + }, + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = 1, + worldId = worldId, + stageId = stageId, + }; + + Assert.Throws(exception, () => action.Execute(new ActionContext + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + else + { + throw new SheetRowNotFoundException(nameof(StageSheet), stageId); + } + } + } +} diff --git a/.Lib9c.Tests/Action/ItemEnhancement13Test.cs b/.Lib9c.Tests/Action/ItemEnhancement13Test.cs new file mode 100644 index 0000000000..9c280c1eed --- /dev/null +++ b/.Lib9c.Tests/Action/ItemEnhancement13Test.cs @@ -0,0 +1,381 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using Bencodex.Types; + using Lib9c.Tests.Fixtures.TableCSV; + using Lib9c.Tests.Fixtures.TableCSV.Cost; + using Lib9c.Tests.Fixtures.TableCSV.Item; + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Extensions; + using Nekoyume.Model.Item; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Xunit; + using static SerializeKeys; + + public class ItemEnhancement13Test + { + private readonly TableSheets _tableSheets; + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly AvatarState _avatarState; + private readonly Currency _currency; + private IAccount _initialState; + + public ItemEnhancement13Test() + { + _initialState = new Account(MockState.Empty); + Dictionary sheets; + (_initialState, sheets) = InitializeUtil.InitializeTableSheets( + _initialState, + sheetsOverride: new Dictionary + { + { + "EnhancementCostSheetV3", + EnhancementCostSheetFixtures.V3 + }, + }); + _tableSheets = new TableSheets(sheets); + foreach (var (key, value) in sheets) + { + _initialState = + _initialState.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var privateKey = new PrivateKey(); + _agentAddress = privateKey.PublicKey.Address; + var agentState = new AgentState(_agentAddress); + + _avatarAddress = _agentAddress.Derive("avatar"); + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + default + ); + + agentState.avatarAddresses.Add(0, _avatarAddress); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + var gold = new GoldCurrencyState(_currency); + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0 + )); + + var context = new ActionContext(); + _initialState = _initialState + .SetState(_agentAddress, agentState.Serialize()) + .SetState(_avatarAddress, _avatarState.Serialize()) + .SetState(slotAddress, new CombinationSlotState(slotAddress, 0).Serialize()) + .SetState(GoldCurrencyState.Address, gold.Serialize()) + .MintAsset(context, GoldCurrencyState.Address, gold.Currency * 100_000_000_000) + .TransferAsset( + context, + Addresses.GoldCurrency, + _agentAddress, + gold.Currency * 3_000_000 + ); + + Assert.Equal( + gold.Currency * 99_997_000_000, + _initialState.GetBalance(Addresses.GoldCurrency, gold.Currency) + ); + Assert.Equal( + gold.Currency * 3_000_000, + _initialState.GetBalance(_agentAddress, gold.Currency) + ); + } + + [Theory] + // from 0 to 0 using one level 0 material + [InlineData(0, false, 0, false, 1)] + [InlineData(0, false, 0, true, 1)] + [InlineData(0, true, 0, false, 1)] + [InlineData(0, true, 0, true, 1)] + // from 0 to 1 using two level 0 material + [InlineData(0, false, 0, false, 3)] + [InlineData(0, false, 0, true, 3)] + [InlineData(0, true, 0, false, 3)] + [InlineData(0, true, 0, true, 3)] + // // Duplicated > from 0 to 0 + [InlineData(0, false, 0, false, 3, true)] + [InlineData(0, false, 0, true, 3, true)] + [InlineData(0, true, 0, false, 3, true)] + [InlineData(0, true, 0, true, 3, true)] + // from 0 to N using multiple level 0 materials + [InlineData(0, false, 0, false, 7)] + [InlineData(0, false, 0, false, 31)] + [InlineData(0, false, 0, true, 7)] + [InlineData(0, false, 0, true, 31)] + [InlineData(0, true, 0, false, 7)] + [InlineData(0, true, 0, false, 31)] + [InlineData(0, true, 0, true, 7)] + [InlineData(0, true, 0, true, 31)] + // // Duplicated > from 0 to 0 + [InlineData(0, false, 0, false, 7, true)] + [InlineData(0, false, 0, false, 31, true)] + [InlineData(0, false, 0, true, 7, true)] + [InlineData(0, false, 0, true, 31, true)] + [InlineData(0, true, 0, false, 7, true)] + [InlineData(0, true, 0, false, 31, true)] + [InlineData(0, true, 0, true, 7, true)] + [InlineData(0, true, 0, true, 31, true)] + // from K to K with material(s). Check requiredBlock == 0 + [InlineData(10, false, 0, false, 1)] + [InlineData(10, false, 0, true, 1)] + [InlineData(10, true, 0, false, 1)] + [InlineData(10, true, 0, true, 1)] + // from K to N using one level X material + [InlineData(5, false, 6, false, 1)] + [InlineData(5, false, 6, true, 1)] + [InlineData(5, true, 6, false, 1)] + [InlineData(5, true, 6, true, 1)] + // from K to N using multiple materials + [InlineData(5, false, 4, false, 6)] + [InlineData(5, false, 7, false, 5)] + [InlineData(5, false, 4, true, 6)] + [InlineData(5, false, 7, true, 5)] + [InlineData(5, true, 4, false, 6)] + [InlineData(5, true, 7, false, 5)] + [InlineData(5, true, 4, true, 6)] + [InlineData(5, true, 7, true, 5)] + // // Duplicated: from K to K + [InlineData(5, true, 4, true, 6, true)] + [InlineData(5, true, 7, true, 5, true)] + [InlineData(5, true, 4, false, 6, true)] + [InlineData(5, true, 7, false, 5, true)] + [InlineData(5, false, 4, true, 6, true)] + [InlineData(5, false, 7, true, 5, true)] + [InlineData(5, false, 4, false, 6, true)] + [InlineData(5, false, 7, false, 5, true)] + // from 20 to 21 (just to reach level 21 exp) + [InlineData(20, false, 20, false, 1)] + [InlineData(20, false, 20, true, 1)] + [InlineData(20, true, 20, false, 1)] + [InlineData(20, true, 20, true, 1)] + // from 20 to 21 (over level 21) + [InlineData(20, false, 20, false, 2)] + [InlineData(20, false, 20, true, 2)] + [InlineData(20, true, 20, false, 2)] + [InlineData(20, true, 20, true, 2)] + // from 21 to 21 (no level up) + [InlineData(21, false, 1, false, 1)] + [InlineData(21, false, 21, false, 1)] + [InlineData(21, false, 1, true, 1)] + [InlineData(21, false, 21, true, 1)] + [InlineData(21, true, 1, false, 1)] + [InlineData(21, true, 21, false, 1)] + [InlineData(21, true, 1, true, 1)] + [InlineData(21, true, 21, true, 1)] + public void Execute( + int startLevel, + bool oldStart, + int materialLevel, + bool oldMaterial, + int materialCount, + bool duplicated = false + ) + { + var row = _tableSheets.EquipmentItemSheet.Values.First(r => r.Id == 10110000); + var equipment = (Equipment)ItemFactory.CreateItemUsable(row, default, 0, startLevel); + if (startLevel == 0) + { + equipment.Exp = (long)row.Exp!; + } + else + { + equipment.Exp = _tableSheets.EnhancementCostSheetV3.OrderedList.First(r => + r.ItemSubType == equipment.ItemSubType && r.Grade == equipment.Grade && + r.Level == equipment.level).Exp; + } + + var startExp = equipment.Exp; + if (oldStart) + { + equipment.Exp = 0L; + } + + _avatarState.inventory.AddItem(equipment, count: 1); + + var startRow = _tableSheets.EnhancementCostSheetV3.OrderedList.FirstOrDefault(r => + r.Grade == equipment.Grade && r.ItemSubType == equipment.ItemSubType && + r.Level == startLevel); + var expectedExpIncrement = 0L; + var materialIds = new List(); + var duplicatedGuid = Guid.NewGuid(); + for (var i = 0; i < materialCount; i++) + { + var materialId = duplicated ? duplicatedGuid : Guid.NewGuid(); + materialIds.Add(materialId); + var material = + (Equipment)ItemFactory.CreateItemUsable(row, materialId, 0, materialLevel); + if (materialLevel == 0) + { + material.Exp = (long)row.Exp!; + } + else + { + material.Exp = _tableSheets.EnhancementCostSheetV3.OrderedList.First(r => + r.ItemSubType == material.ItemSubType && r.Grade == material.Grade && + r.Level == material.level).Exp; + } + + if (!(duplicated && i > 0)) + { + expectedExpIncrement += material.Exp; + } + + if (oldMaterial) + { + material.Exp = 0L; + } + + _avatarState.inventory.AddItem(material, count: 1); + } + + var result = new CombinationConsumable5.ResultModel() + { + id = default, + gold = 0, + actionPoint = 0, + recipeId = 1, + materials = new Dictionary(), + itemUsable = equipment, + }; + var preItemUsable = new Equipment((Dictionary)equipment.Serialize()); + + for (var i = 0; i < 100; i++) + { + var mail = new CombinationMail(result, i, default, 0); + _avatarState.Update(mail); + } + + _avatarState.worldInformation.ClearStage( + 1, + 1, + 1, + _tableSheets.WorldSheet, + _tableSheets.WorldUnlockSheet + ); + + var slotAddress = + _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0 + )); + + Assert.Equal(startLevel, equipment.level); + + _initialState = _initialState + .SetState( + _avatarAddress.Derive(LegacyInventoryKey), + _avatarState.inventory.Serialize() + ) + .SetState( + _avatarAddress.Derive(LegacyWorldInformationKey), + _avatarState.worldInformation.Serialize() + ) + .SetState( + _avatarAddress.Derive(LegacyQuestListKey), + _avatarState.questList.Serialize() + ) + .SetState(_avatarAddress, _avatarState.SerializeV2()); + + var action = new ItemEnhancement13 + { + itemId = default, + materialIds = materialIds, + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + var nextState = action.Execute(new ActionContext() + { + PreviousState = _initialState, + Signer = _agentAddress, + BlockIndex = 1, + RandomSeed = 0, + }); + + var slotState = nextState.GetCombinationSlotState(_avatarAddress, 0); + var resultEquipment = (Equipment)slotState.Result.itemUsable; + var level = resultEquipment.level; + var nextAvatarState = nextState.GetAvatarState(_avatarAddress); + var expectedTargetRow = _tableSheets.EnhancementCostSheetV3.OrderedList.FirstOrDefault( + r => r.Grade == equipment.Grade && r.ItemSubType == equipment.ItemSubType && + r.Level == level); + var expectedCost = (expectedTargetRow?.Cost ?? 0) - (startRow?.Cost ?? 0); + var expectedBlockIndex = + (expectedTargetRow?.RequiredBlockIndex ?? 0) - (startRow?.RequiredBlockIndex ?? 0); + Assert.Equal(default, resultEquipment.ItemId); + Assert.Equal(startExp + expectedExpIncrement, resultEquipment.Exp); + Assert.Equal( + (3_000_000 - expectedCost) * _currency, + nextState.GetBalance(_agentAddress, _currency) + ); + + var arenaSheet = _tableSheets.ArenaSheet; + var arenaData = arenaSheet.GetRoundByBlockIndex(1); + var feeStoreAddress = + Addresses.GetBlacksmithFeeAddress(arenaData.ChampionshipId, arenaData.Round); + Assert.Equal( + expectedCost * _currency, + nextState.GetBalance(feeStoreAddress, _currency) + ); + Assert.Equal(30, nextAvatarState.mailBox.Count); + + var stateDict = (Dictionary)nextState.GetState(slotAddress); + var slot = new CombinationSlotState(stateDict); + var slotResult = (ItemEnhancement13.ResultModel)slot.Result; + if (startLevel != level) + { + var baseMinAtk = (decimal)preItemUsable.StatsMap.BaseATK; + var baseMaxAtk = (decimal)preItemUsable.StatsMap.BaseATK; + var extraMinAtk = (decimal)preItemUsable.StatsMap.AdditionalATK; + var extraMaxAtk = (decimal)preItemUsable.StatsMap.AdditionalATK; + + for (var i = startLevel + 1; i <= level; i++) + { + var currentRow = _tableSheets.EnhancementCostSheetV3.OrderedList + .First(x => + x.Grade == 1 && x.ItemSubType == equipment.ItemSubType && x.Level == i); + + baseMinAtk *= currentRow.BaseStatGrowthMin.NormalizeFromTenThousandths() + 1; + baseMaxAtk *= currentRow.BaseStatGrowthMax.NormalizeFromTenThousandths() + 1; + extraMinAtk *= currentRow.ExtraStatGrowthMin.NormalizeFromTenThousandths() + 1; + extraMaxAtk *= currentRow.ExtraStatGrowthMax.NormalizeFromTenThousandths() + 1; + } + + Assert.InRange( + resultEquipment.StatsMap.ATK, + baseMinAtk + extraMinAtk, + baseMaxAtk + extraMaxAtk + 1 + ); + } + + Assert.Equal( + expectedBlockIndex + 1, // +1 for execution + resultEquipment.RequiredBlockIndex + ); + Assert.Equal(preItemUsable.ItemId, slotResult.preItemUsable.ItemId); + Assert.Equal(preItemUsable.ItemId, resultEquipment.ItemId); + Assert.Equal(expectedCost, slotResult.gold); + } + } +} diff --git a/.Lib9c.Tests/Action/Raid6Test.cs b/.Lib9c.Tests/Action/Raid6Test.cs new file mode 100644 index 0000000000..b377efedec --- /dev/null +++ b/.Lib9c.Tests/Action/Raid6Test.cs @@ -0,0 +1,636 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Bencodex.Types; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Battle; + using Nekoyume.Extensions; + using Nekoyume.Helper; + using Nekoyume.Model.Arena; + using Nekoyume.Model.Rune; + using Nekoyume.Model.State; + using Nekoyume.TableData; + using Xunit; + using static SerializeKeys; + + public class Raid6Test + { + private readonly Dictionary _sheets; + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly TableSheets _tableSheets; + private readonly Currency _goldCurrency; + + public Raid6Test() + { + _sheets = TableSheetsImporter.ImportSheets(); + _tableSheets = new TableSheets(_sheets); + _agentAddress = new PrivateKey().Address; + _avatarAddress = new PrivateKey().Address; +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _goldCurrency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + } + + [Theory] + // Join new raid. + [InlineData(null, true, true, true, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + [InlineData(null, true, true, true, false, 0, 0L, false, false, 0, false, false, false, 5, true, 0, 10002, 1, 30001)] + // Refill by interval. + [InlineData(null, true, true, false, true, 0, -10368, false, false, 0, false, false, false, 5, true, 0, 10002, 1, 30001)] + // Refill by NCG. + [InlineData(null, true, true, false, true, 0, 200L, true, true, 0, false, false, false, 5, true, 0, 10002, 1, 30001)] + [InlineData(null, true, true, false, true, 0, 200L, true, true, 1, false, false, false, 5, true, 0, 10002, 1, 30001)] + // Boss level up. + [InlineData(null, true, true, false, true, 3, 100L, false, false, 0, true, true, false, 5, true, 0, 10002, 1, 30001)] + // Update RaidRewardInfo. + [InlineData(null, true, true, false, true, 3, 100L, false, false, 0, true, true, true, 5, true, 0, 10002, 1, 30001)] + // Boss skip level up. + [InlineData(null, true, true, false, true, 3, 100L, false, false, 0, true, false, false, 5, true, 0, 10002, 1, 30001)] + // AvatarState null. + [InlineData(typeof(FailedLoadStateException), false, false, false, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Stage not cleared. + [InlineData(typeof(NotEnoughClearedStageLevelException), true, false, false, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Insufficient CRYSTAL. + [InlineData(typeof(InsufficientBalanceException), true, true, false, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Insufficient NCG. + [InlineData(typeof(InsufficientBalanceException), true, true, false, true, 0, 0L, true, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Wait interval. + [InlineData(typeof(RequiredBlockIntervalException), true, true, false, true, 3, 10L, false, false, 0, false, false, false, 1, false, 0, 10002, 1, 30001)] + // Exceed purchase limit. + [InlineData(typeof(ExceedTicketPurchaseLimitException), true, true, false, true, 0, 100L, true, false, 1_000, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Exceed challenge count. + [InlineData(typeof(ExceedPlayCountException), true, true, false, true, 0, 100L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + [InlineData(typeof(DuplicatedRuneIdException), true, true, false, true, 3, 100L, true, false, 0, false, false, false, 5, false, 0, 30001, 1, 30001)] + [InlineData(typeof(DuplicatedRuneSlotIndexException), true, true, false, true, 3, 100L, true, false, 0, false, false, false, 5, false, 1, 10002, 1, 30001)] + public void Execute( + Type exc, + bool avatarExist, + bool stageCleared, + bool crystalExist, + bool raiderStateExist, + int remainChallengeCount, + long refillBlockIndexOffset, + bool payNcg, + bool ncgExist, + int purchaseCount, + bool kill, + bool levelUp, + bool rewardRecordExist, + long executeOffset, + bool raiderListExist, + int slotIndex, + int runeId, + int slotIndex2, + int runeId2 + ) + { + var blockIndex = _tableSheets.WorldBossListSheet.Values + .OrderBy(x => x.StartedBlockIndex) + .First(x => + { + if (exc == typeof(InsufficientBalanceException)) + { + return ncgExist ? x.TicketPrice > 0 : x.EntranceFee > 0; + } + + return true; + }) + .StartedBlockIndex; + + var action = new Raid6 + { + AvatarAddress = _avatarAddress, + EquipmentIds = new List(), + CostumeIds = new List(), + FoodIds = new List(), + RuneInfos = new List() + { + new RuneSlotInfo(slotIndex, runeId), + new RuneSlotInfo(slotIndex2, runeId2), + }, + PayNcg = payNcg, + }; + Currency crystal = CrystalCalculator.CRYSTAL; + int raidId = _tableSheets.WorldBossListSheet.FindRaidIdByBlockIndex(blockIndex); + Address raiderAddress = Addresses.GetRaiderAddress(_avatarAddress, raidId); + var goldCurrencyState = new GoldCurrencyState(_goldCurrency); + WorldBossListSheet.Row worldBossRow = _tableSheets.WorldBossListSheet.FindRowByBlockIndex(blockIndex); + var hpSheet = _tableSheets.WorldBossGlobalHpSheet; + Address bossAddress = Addresses.GetWorldBossAddress(raidId); + Address worldBossKillRewardRecordAddress = Addresses.GetWorldBossKillRewardRecordAddress(_avatarAddress, raidId); + Address raiderListAddress = Addresses.GetRaiderListAddress(raidId); + int level = 1; + if (kill & !levelUp) + { + level = hpSheet.OrderedList.Last().Level; + } + + var fee = _tableSheets.WorldBossListSheet[raidId].EntranceFee; + + var context = new ActionContext(); + IAccount state = new Account(MockState.Empty) + .SetState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetState(_agentAddress, new AgentState(_agentAddress).Serialize()); + + foreach (var (key, value) in _sheets) + { + state = state.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + + if (avatarExist) + { + var equipments = Doomfist.GetAllParts(_tableSheets, avatarState.level); + foreach (var equipment in equipments) + { + avatarState.inventory.AddItem(equipment); + } + + if (stageCleared) + { + for (int i = 0; i < 50; i++) + { + avatarState.worldInformation.ClearStage(1, i + 1, 0, _tableSheets.WorldSheet, _tableSheets.WorldUnlockSheet); + } + } + + if (crystalExist) + { + var price = _tableSheets.WorldBossListSheet[raidId].EntranceFee; + state = state.MintAsset(context, _agentAddress, price * crystal); + } + + if (raiderStateExist) + { + var raiderState = new RaiderState(); + raiderState.RefillBlockIndex = blockIndex + refillBlockIndexOffset; + raiderState.RemainChallengeCount = remainChallengeCount; + raiderState.TotalScore = 1_000; + raiderState.HighScore = 0; + raiderState.TotalChallengeCount = 1; + raiderState.PurchaseCount = purchaseCount; + raiderState.Cp = 0; + raiderState.Level = 0; + raiderState.IconId = 0; + raiderState.AvatarName = "hash"; + raiderState.AvatarAddress = _avatarAddress; + raiderState.UpdatedBlockIndex = blockIndex; + + state = state.SetState(raiderAddress, raiderState.Serialize()); + + var raiderList = new List().Add(raiderAddress.Serialize()); + + if (raiderListExist) + { + raiderList = raiderList.Add(new PrivateKey().Address.Serialize()); + } + + state = state.SetState(raiderListAddress, raiderList); + } + + if (rewardRecordExist) + { + var rewardRecord = new WorldBossKillRewardRecord + { + [0] = false, + }; + state = state.SetState(worldBossKillRewardRecordAddress, rewardRecord.Serialize()); + } + + if (ncgExist) + { + var row = _tableSheets.WorldBossListSheet.FindRowByBlockIndex(blockIndex); + state = state.MintAsset(context, _agentAddress, (row.TicketPrice + row.AdditionalTicketPrice * purchaseCount) * _goldCurrency); + } + + state = state + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) + .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) + .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) + .SetState(gameConfigState.address, gameConfigState.Serialize()); + } + + if (kill) + { + var bossState = + new WorldBossState(worldBossRow, _tableSheets.WorldBossGlobalHpSheet[level]) + { + CurrentHp = 0, + Level = level, + }; + state = state.SetState(bossAddress, bossState.Serialize()); + } + + if (exc is null) + { + var randomSeed = 0; + var ctx = new ActionContext + { + BlockIndex = blockIndex + executeOffset, + PreviousState = state, + RandomSeed = randomSeed, + Signer = _agentAddress, + }; + + var nextState = action.Execute(ctx); + + var random = new TestRandom(randomSeed); + var bossListRow = _tableSheets.WorldBossListSheet.FindRowByBlockIndex(ctx.BlockIndex); + var raidSimulatorSheets = _tableSheets.GetRaidSimulatorSheets(); + var simulator = new RaidSimulator( + bossListRow.BossId, + random, + avatarState, + action.FoodIds, + null, + raidSimulatorSheets, + _tableSheets.CostumeStatSheet); + simulator.Simulate(); + var score = simulator.DamageDealt; + + Dictionary rewardMap + = new Dictionary(); + foreach (var reward in simulator.AssetReward) + { + rewardMap[reward.Currency] = reward; + } + + if (rewardRecordExist) + { + var bossRow = raidSimulatorSheets.WorldBossCharacterSheet[bossListRow.BossId]; + Assert.True(state.TryGetState(bossAddress, out List prevRawBoss)); + var prevBossState = new WorldBossState(prevRawBoss); + int rank = WorldBossHelper.CalculateRank(bossRow, raiderStateExist ? 1_000 : 0); + var rewards = RuneHelper.CalculateReward( + rank, + prevBossState.Id, + _tableSheets.RuneWeightSheet, + _tableSheets.WorldBossKillRewardSheet, + _tableSheets.RuneSheet, + random + ); + + foreach (var reward in rewards) + { + if (!rewardMap.ContainsKey(reward.Currency)) + { + rewardMap[reward.Currency] = reward; + } + else + { + rewardMap[reward.Currency] += reward; + } + } + + foreach (var reward in rewardMap) + { + if (reward.Key.Equals(CrystalCalculator.CRYSTAL)) + { + Assert.Equal(reward.Value, nextState.GetBalance(_agentAddress, reward.Key)); + } + else + { + Assert.Equal(reward.Value, nextState.GetBalance(_avatarAddress, reward.Key)); + } + } + } + + if (rewardMap.ContainsKey(crystal)) + { + Assert.Equal(rewardMap[crystal], nextState.GetBalance(_agentAddress, crystal)); + } + + if (crystalExist) + { + Assert.Equal(fee * crystal, nextState.GetBalance(bossAddress, crystal)); + } + + Assert.True(nextState.TryGetState(raiderAddress, out List rawRaider)); + var raiderState = new RaiderState(rawRaider); + int expectedTotalScore = raiderStateExist ? 1_000 + score : score; + int expectedRemainChallenge = payNcg ? 0 : 2; + int expectedTotalChallenge = raiderStateExist ? 2 : 1; + + Assert.Equal(score, raiderState.HighScore); + Assert.Equal(expectedTotalScore, raiderState.TotalScore); + Assert.Equal(expectedRemainChallenge, raiderState.RemainChallengeCount); + Assert.Equal(expectedTotalChallenge, raiderState.TotalChallengeCount); + Assert.Equal(1, raiderState.Level); + Assert.Equal(GameConfig.DefaultAvatarArmorId, raiderState.IconId); + Assert.True(raiderState.Cp > 0); + + Assert.True(nextState.TryGetState(bossAddress, out List rawBoss)); + var bossState = new WorldBossState(rawBoss); + int expectedLevel = level; + if (kill & levelUp) + { + expectedLevel++; + } + + Assert.Equal(expectedLevel, bossState.Level); + Assert.Equal(expectedLevel, raiderState.LatestBossLevel); + if (kill) + { + Assert.Equal(hpSheet[expectedLevel].Hp, bossState.CurrentHp); + } + + if (payNcg) + { + Assert.Equal(0 * _goldCurrency, nextState.GetBalance(_agentAddress, _goldCurrency)); + Assert.Equal(purchaseCount + 1, nextState.GetRaiderState(raiderAddress).PurchaseCount); + } + + Assert.True(nextState.TryGetState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); + var rewardRecord = new WorldBossKillRewardRecord(rawRewardInfo); + Assert.Contains(expectedLevel, rewardRecord.Keys); + if (rewardRecordExist) + { + Assert.True(rewardRecord[0]); + } + else + { + if (expectedLevel == 1) + { + Assert.False(rewardRecord[1]); + } + else + { + Assert.DoesNotContain(1, rewardRecord.Keys); + } + } + + Assert.True(nextState.TryGetState(raiderListAddress, out List rawRaiderList)); + List
raiderList = rawRaiderList.ToList(StateExtensions.ToAddress); + + Assert.Contains(raiderAddress, raiderList); + } + else + { + if (exc == typeof(DuplicatedRuneIdException) || exc == typeof(DuplicatedRuneSlotIndexException)) + { + var ncgCurrency = state.GetGoldCurrency(); + state = state.MintAsset(context, _agentAddress, 99999 * ncgCurrency); + + var unlockRuneSlot = new UnlockRuneSlot() + { + AvatarAddress = _avatarAddress, + SlotIndex = 1, + }; + + state = unlockRuneSlot.Execute(new ActionContext + { + BlockIndex = 1, + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + }); + } + + Assert.Throws(exc, () => action.Execute(new ActionContext + { + BlockIndex = blockIndex + executeOffset, + PreviousState = state, + RandomSeed = 0, + Signer = _agentAddress, + })); + } + } + + [Fact] + public void Execute_With_Reward() + { + var action = new Raid6 + { + AvatarAddress = _avatarAddress, + EquipmentIds = new List(), + CostumeIds = new List(), + FoodIds = new List(), + RuneInfos = new List(), + PayNcg = false, + }; + + var worldBossRow = _tableSheets.WorldBossListSheet.First().Value; + int raidId = worldBossRow.Id; + Address raiderAddress = Addresses.GetRaiderAddress(_avatarAddress, raidId); + var goldCurrencyState = new GoldCurrencyState(_goldCurrency); + Address bossAddress = Addresses.GetWorldBossAddress(raidId); + Address worldBossKillRewardRecordAddress = Addresses.GetWorldBossKillRewardRecordAddress(_avatarAddress, raidId); + + IAccount state = new Account(MockState.Empty) + .SetState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetState(_agentAddress, new AgentState(_agentAddress).Serialize()); + + foreach (var (key, value) in _sheets) + { + state = state.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + + for (int i = 0; i < 50; i++) + { + avatarState.worldInformation.ClearStage(1, i + 1, 0, _tableSheets.WorldSheet, _tableSheets.WorldUnlockSheet); + } + + var raiderState = new RaiderState(); + raiderState.RefillBlockIndex = 0; + raiderState.RemainChallengeCount = WorldBossHelper.MaxChallengeCount; + raiderState.TotalScore = 1_000; + raiderState.TotalChallengeCount = 1; + raiderState.PurchaseCount = 0; + raiderState.Cp = 0; + raiderState.Level = 0; + raiderState.IconId = 0; + raiderState.AvatarName = "hash"; + raiderState.AvatarAddress = _avatarAddress; + state = state.SetState(raiderAddress, raiderState.Serialize()); + + var rewardRecord = new WorldBossKillRewardRecord + { + [1] = false, + }; + state = state.SetState(worldBossKillRewardRecordAddress, rewardRecord.Serialize()); + + state = state + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) + .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) + .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) + .SetState(gameConfigState.address, gameConfigState.Serialize()); + + var bossState = + new WorldBossState(worldBossRow, _tableSheets.WorldBossGlobalHpSheet[2]) + { + CurrentHp = 0, + Level = 2, + }; + state = state.SetState(bossAddress, bossState.Serialize()); + var randomSeed = 0; + var random = new TestRandom(randomSeed); + + var simulator = new RaidSimulator( + worldBossRow.BossId, + random, + avatarState, + action.FoodIds, + null, + _tableSheets.GetRaidSimulatorSheets(), + _tableSheets.CostumeStatSheet); + simulator.Simulate(); + + Dictionary rewardMap + = new Dictionary(); + foreach (var reward in simulator.AssetReward) + { + rewardMap[reward.Currency] = reward; + } + + List killRewards = RuneHelper.CalculateReward( + 0, + bossState.Id, + _tableSheets.RuneWeightSheet, + _tableSheets.WorldBossKillRewardSheet, + _tableSheets.RuneSheet, + random + ); + + var nextState = action.Execute(new ActionContext + { + BlockIndex = worldBossRow.StartedBlockIndex + gameConfigState.WorldBossRequiredInterval, + PreviousState = state, + RandomSeed = randomSeed, + Signer = _agentAddress, + }); + + Assert.True(nextState.TryGetState(raiderAddress, out List rawRaider)); + var nextRaiderState = new RaiderState(rawRaider); + Assert.Equal(simulator.DamageDealt, nextRaiderState.HighScore); + + foreach (var reward in killRewards) + { + if (!rewardMap.ContainsKey(reward.Currency)) + { + rewardMap[reward.Currency] = reward; + } + else + { + rewardMap[reward.Currency] += reward; + } + } + + foreach (var reward in rewardMap) + { + if (reward.Key.Equals(CrystalCalculator.CRYSTAL)) + { + Assert.Equal(reward.Value, nextState.GetBalance(_agentAddress, reward.Key)); + } + else + { + Assert.Equal(reward.Value, nextState.GetBalance(_avatarAddress, reward.Key)); + } + } + + Assert.Equal(1, nextRaiderState.Level); + Assert.Equal(GameConfig.DefaultAvatarArmorId, nextRaiderState.IconId); + Assert.True(nextRaiderState.Cp > 0); + Assert.Equal(3, nextRaiderState.LatestBossLevel); + Assert.True(nextState.TryGetState(bossAddress, out List rawBoss)); + var nextBossState = new WorldBossState(rawBoss); + Assert.Equal(3, nextBossState.Level); + Assert.True(nextState.TryGetState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); + var nextRewardInfo = new WorldBossKillRewardRecord(rawRewardInfo); + Assert.True(nextRewardInfo[1]); + } + + [Fact] + public void Execute_With_Free_Crystal_Fee() + { + var action = new Raid6 + { + AvatarAddress = _avatarAddress, + EquipmentIds = new List(), + CostumeIds = new List(), + FoodIds = new List(), + RuneInfos = new List(), + PayNcg = false, + }; + Currency crystal = CrystalCalculator.CRYSTAL; + + _sheets[nameof(WorldBossListSheet)] = + "id,boss_id,started_block_index,ended_block_index,fee,ticket_price,additional_ticket_price,max_purchase_count\r\n" + + "1,900002,0,100,0,1,1,40"; + + var goldCurrencyState = new GoldCurrencyState(_goldCurrency); + IAccount state = new Account(MockState.Empty) + .SetState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetState(_agentAddress, new AgentState(_agentAddress).Serialize()); + + foreach (var (key, value) in _sheets) + { + state = state.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + + for (int i = 0; i < 50; i++) + { + avatarState.worldInformation.ClearStage(1, i + 1, 0, _tableSheets.WorldSheet, _tableSheets.WorldUnlockSheet); + } + + state = state + .SetState(_avatarAddress, avatarState.SerializeV2()) + .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) + .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) + .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) + .SetState(gameConfigState.address, gameConfigState.Serialize()); + + var blockIndex = gameConfigState.WorldBossRequiredInterval; + var randomSeed = 0; + var ctx = new ActionContext + { + BlockIndex = blockIndex, + PreviousState = state, + RandomSeed = randomSeed, + Signer = _agentAddress, + }; + + IAccount nextState; + var exception = Record.Exception(() => nextState = action.Execute(ctx)); + Assert.Null(exception); + } + } +} diff --git a/.Lib9c.Tests/Action/RapidCombination9Test.cs b/.Lib9c.Tests/Action/RapidCombination9Test.cs new file mode 100644 index 0000000000..25833ae3d5 --- /dev/null +++ b/.Lib9c.Tests/Action/RapidCombination9Test.cs @@ -0,0 +1,684 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Globalization; + using System.Linq; + using Bencodex.Types; + using Lib9c.Tests.Fixtures.TableCSV; + using Lib9c.Tests.Fixtures.TableCSV.Item; + using Lib9c.Tests.Util; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Nekoyume.TableData; + using Xunit; + using static Lib9c.SerializeKeys; + + public class RapidCombination9Test + { + private readonly IAccount _initialState; + + private readonly TableSheets _tableSheets; + + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + + public RapidCombination9Test() + { + _initialState = new Account(MockState.Empty); + Dictionary sheets; + (_initialState, sheets) = InitializeUtil.InitializeTableSheets( + _initialState, + sheetsOverride: new Dictionary + { + { + "EquipmentItemRecipeSheet", + EquipmentItemRecipeSheetFixtures.Default + }, + { + "EquipmentItemSubRecipeSheet", + EquipmentItemSubRecipeSheetFixtures.V1 + }, + { + "GameConfigSheet", + GameConfigSheetFixtures.Default + }, + }); + _tableSheets = new TableSheets(sheets); + foreach (var (key, value) in sheets) + { + _initialState = + _initialState.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _agentAddress = new PrivateKey().Address; + var agentState = new AgentState(_agentAddress); + + _avatarAddress = new PrivateKey().Address; + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + default + ); + + agentState.avatarAddresses[0] = _avatarAddress; + + _initialState = _initialState + .SetState(Addresses.GameConfig, new GameConfigState(sheets[nameof(GameConfigSheet)]).Serialize()) + .SetState(_agentAddress, agentState.Serialize()) + .SetState(_avatarAddress, avatarState.Serialize()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Execute(bool backward) + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), 83); + avatarState.inventory.AddItem(ItemFactory.CreateTradableMaterial(row), 100); + Assert.True(avatarState.inventory.HasFungibleItem(row.ItemId, 0, 183)); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * 200; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var mail = new CombinationMail(result, 0, default, requiredBlockIndex); + result.id = mail.id; + avatarState.Update2(mail); + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(result, 0, requiredBlockIndex); + + var tempState = _initialState.SetState(slotAddress, slotState.Serialize()); + + if (backward) + { + tempState = tempState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + tempState = tempState + .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) + .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) + .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) + .SetState(_avatarAddress, avatarState.SerializeV2()); + } + + var action = new RapidCombination9 + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + var nextState = action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 51, + }); + + var nextAvatarState = nextState.GetAvatarStateV2(_avatarAddress); + var item = nextAvatarState.inventory.Equipments.First(); + + Assert.Empty(nextAvatarState.inventory.Materials.Select(r => r.ItemSubType == ItemSubType.Hourglass)); + Assert.Equal(equipment.ItemId, item.ItemId); + Assert.Equal(51, item.RequiredBlockIndex); + } + + [Fact] + public void Execute_Throw_CombinationSlotResultNullException() + { + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, 0); + slotState.Update(null, 0, 0); + + var tempState = _initialState + .SetState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination9 + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 1, + })); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(1, 2)] + public void Execute_Throw_NotEnoughClearedStageLevelException(int avatarClearedStage, int slotStateUnlockStage) + { + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + avatarClearedStage); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + 100); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(result, 0, 0); + + var tempState = _initialState + .SetState(_avatarAddress, avatarState.Serialize()) + .SetState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination9 + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 1, + })); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(10, 100)] + public void Execute_Throw_RequiredBlockIndexException(int itemRequiredBlockIndex, int contextBlockIndex) + { + const int avatarClearedStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + avatarClearedStage); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + itemRequiredBlockIndex); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, avatarClearedStage); + slotState.Update(result, 0, 0); + + var tempState = _initialState + .SetState(_avatarAddress, avatarState.Serialize()) + .SetState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination9 + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = contextBlockIndex, + })); + } + + [Theory] + [InlineData(0, 0, 0, 40)] + [InlineData(0, 1, 2, 40)] + [InlineData(22, 0, 0, 40)] + [InlineData(0, 22, 0, 40)] + [InlineData(0, 22, 2, 40)] + [InlineData(2, 10, 2, 40)] + public void Execute_Throw_NotEnoughMaterialException(int materialCount, int tradableCount, long blockIndex, int requiredCount) + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), count: materialCount); + if (tradableCount > 0) + { + var material = ItemFactory.CreateTradableMaterial(row); + material.RequiredBlockIndex = blockIndex; + avatarState.inventory.AddItem(material, count: tradableCount); + } + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * requiredCount; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var mail = new CombinationMail(result, 0, default, requiredBlockIndex); + result.id = mail.id; + avatarState.Update2(mail); + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(result, 0, 0); + + var tempState = _initialState + .SetState(_avatarAddress, avatarState.Serialize()) + .SetState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination9 + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 51, + })); + } + + [Theory] + [InlineData(null)] + [InlineData(1)] + public void ResultModelDeterministic(int? subRecipeId) + { + var row = _tableSheets.MaterialItemSheet.Values.First(); + var row2 = _tableSheets.MaterialItemSheet.Values.Last(); + + Assert.True(row.Id < row2.Id); + + var material = ItemFactory.CreateMaterial(row); + var material2 = ItemFactory.CreateMaterial(row2); + + var itemUsable = ItemFactory.CreateItemUsable(_tableSheets.EquipmentItemSheet.Values.First(), default, 0); + var r = new CombinationConsumable5.ResultModel + { + id = default, + gold = 0, + actionPoint = 0, + recipeId = 1, + subRecipeId = subRecipeId, + materials = new Dictionary + { + [material] = 1, + [material2] = 1, + }, + itemUsable = itemUsable, + }; + var result = new RapidCombination0.ResultModel((Dictionary)r.Serialize()) + { + cost = new Dictionary + { + [material] = 1, + [material2] = 1, + }, + }; + + var r2 = new CombinationConsumable5.ResultModel + { + id = default, + gold = 0, + actionPoint = 0, + recipeId = 1, + subRecipeId = subRecipeId, + materials = new Dictionary + { + [material2] = 1, + [material] = 1, + }, + itemUsable = itemUsable, + }; + + var result2 = new RapidCombination0.ResultModel((Dictionary)r2.Serialize()) + { + cost = new Dictionary + { + [material2] = 1, + [material] = 1, + }, + }; + + Assert.Equal(result.Serialize(), result2.Serialize()); + } + + [Fact] + public void Execute_Throw_RequiredAppraiseBlockException() + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), count: 22); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * 40; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var mail = new CombinationMail(result, 0, default, requiredBlockIndex); + result.id = mail.id; + avatarState.Update(mail); + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(result, 0, 0); + + var tempState = _initialState + .SetState(_avatarAddress, avatarState.Serialize()) + .SetState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination9 + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 1, + })); + } + + [Theory] + [InlineData(7)] + [InlineData(9)] + [InlineData(10)] + [InlineData(11)] + public void Execute_NotThrow_InvalidOperationException_When_TargetSlotCreatedBy( + int itemEnhancementResultModelNumber) + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), 83); + avatarState.inventory.AddItem(ItemFactory.CreateTradableMaterial(row), 100); + Assert.True(avatarState.inventory.HasFungibleItem(row.ItemId, 0, 183)); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet + .OrderedList.First(e => e.Grade >= 1); + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * 200; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + var materialEquipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + avatarState.inventory.AddItem(materialEquipment); + + AttachmentActionResult resultModel = null; + var random = new TestRandom(); + var mailId = random.GenerateRandomGuid(); + var preItemUsable = new Equipment((Dictionary)equipment.Serialize()); + switch (itemEnhancementResultModelNumber) + { + case 7: + { + equipment = ItemEnhancement7.UpgradeEquipment(equipment); + resultModel = new ItemEnhancement7.ResultModel + { + id = mailId, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + }; + + break; + } + + case 9: + { + Assert.True(ItemEnhancement9.TryGetRow( + equipment, + _tableSheets.EnhancementCostSheetV2, + out var costRow)); + var equipmentResult = ItemEnhancement9.GetEnhancementResult(costRow, random); + equipment.LevelUp( + random, + costRow, + equipmentResult == ItemEnhancement9.EnhancementResult.GreatSuccess); + resultModel = new ItemEnhancement9.ResultModel + { + id = mailId, + preItemUsable = preItemUsable, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + gold = 0, + actionPoint = 0, + enhancementResult = ItemEnhancement9.EnhancementResult.GreatSuccess, + }; + + break; + } + + case 10: + { + Assert.True(ItemEnhancement10.TryGetRow( + equipment, + _tableSheets.EnhancementCostSheetV2, + out var costRow)); + var equipmentResult = ItemEnhancement10.GetEnhancementResult(costRow, random); + equipment.LevelUp( + random, + costRow, + equipmentResult == ItemEnhancement10.EnhancementResult.GreatSuccess); + resultModel = new ItemEnhancement10.ResultModel + { + id = mailId, + preItemUsable = preItemUsable, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + gold = 0, + actionPoint = 0, + enhancementResult = ItemEnhancement10.EnhancementResult.GreatSuccess, + }; + + break; + } + + case 11: + { + Assert.True(ItemEnhancement11.TryGetRow( + equipment, + _tableSheets.EnhancementCostSheetV2, + out var costRow)); + var equipmentResult = ItemEnhancement11.GetEnhancementResult(costRow, random); + equipment.LevelUp( + random, + costRow, + equipmentResult == ItemEnhancement11.EnhancementResult.GreatSuccess); + resultModel = new ItemEnhancement11.ResultModel + { + id = mailId, + preItemUsable = preItemUsable, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + gold = 0, + actionPoint = 0, + enhancementResult = ItemEnhancement11.EnhancementResult.GreatSuccess, + CRYSTAL = 0 * CrystalCalculator.CRYSTAL, + }; + + break; + } + + default: + break; + } + + // NOTE: Do not update `mail`, because this test assumes that the `mail` was removed. + { + // var mail = new ItemEnhanceMail(resultModel, 0, random.GenerateRandomGuid(), requiredBlockIndex); + // avatarState.Update(mail); + } + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(resultModel, 0, requiredBlockIndex); + + var tempState = _initialState.SetState(slotAddress, slotState.Serialize()) + .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) + .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) + .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) + .SetState(_avatarAddress, avatarState.SerializeV2()); + + var action = new RapidCombination9 + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 51, + }); + } + } +} From 536e5091ee3256d6ad2fb20d6779db1d9fd50a0a Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Thu, 14 Mar 2024 14:26:09 +0900 Subject: [PATCH 02/16] Basic renaming --- ...e8Test.cs => CombinationConsumableTest.cs} | 6 ++-- ...ateAvatar10Test.cs => CreateAvatarTest.cs} | 16 +++++----- ...tleV5Test.cs => EventDungeonBattleTest.cs} | 6 ++-- ...Sweep9Test.cs => HackAndSlashSweepTest.cs} | 30 +++++++++---------- ...cement13Test.cs => ItemEnhancementTest.cs} | 6 ++-- .../Action/{Raid6Test.cs => RaidTest.cs} | 10 +++---- ...nation9Test.cs => RapidCombinationTest.cs} | 18 +++++------ 7 files changed, 46 insertions(+), 46 deletions(-) rename .Lib9c.Tests/Action/{CombinationConsumable8Test.cs => CombinationConsumableTest.cs} (97%) rename .Lib9c.Tests/Action/{CreateAvatar10Test.cs => CreateAvatarTest.cs} (96%) rename .Lib9c.Tests/Action/{EventDungeonBattleV5Test.cs => EventDungeonBattleTest.cs} (99%) rename .Lib9c.Tests/Action/{HackAndSlashSweep9Test.cs => HackAndSlashSweepTest.cs} (98%) rename .Lib9c.Tests/Action/{ItemEnhancement13Test.cs => ItemEnhancementTest.cs} (99%) rename .Lib9c.Tests/Action/{Raid6Test.cs => RaidTest.cs} (99%) rename .Lib9c.Tests/Action/{RapidCombination9Test.cs => RapidCombinationTest.cs} (98%) diff --git a/.Lib9c.Tests/Action/CombinationConsumable8Test.cs b/.Lib9c.Tests/Action/CombinationConsumableTest.cs similarity index 97% rename from .Lib9c.Tests/Action/CombinationConsumable8Test.cs rename to .Lib9c.Tests/Action/CombinationConsumableTest.cs index 90fc3bb85d..bc66bc3981 100644 --- a/.Lib9c.Tests/Action/CombinationConsumable8Test.cs +++ b/.Lib9c.Tests/Action/CombinationConsumableTest.cs @@ -15,7 +15,7 @@ namespace Lib9c.Tests.Action using Xunit; using static Lib9c.SerializeKeys; - public class CombinationConsumable8Test + public class CombinationConsumableTest { private readonly Address _agentAddress; private readonly Address _avatarAddress; @@ -23,7 +23,7 @@ public class CombinationConsumable8Test private readonly TableSheets _tableSheets; private IAccount _initialState; - public CombinationConsumable8Test() + public CombinationConsumableTest() { _agentAddress = new PrivateKey().Address; _avatarAddress = _agentAddress.Derive("avatar"); @@ -113,7 +113,7 @@ public void Execute(bool backward) .SetState(_avatarAddress, avatarState.SerializeV2()); } - var action = new CombinationConsumable8 + var action = new CombinationConsumable { avatarAddress = _avatarAddress, recipeId = row.Id, diff --git a/.Lib9c.Tests/Action/CreateAvatar10Test.cs b/.Lib9c.Tests/Action/CreateAvatarTest.cs similarity index 96% rename from .Lib9c.Tests/Action/CreateAvatar10Test.cs rename to .Lib9c.Tests/Action/CreateAvatarTest.cs index 71a257306d..c06f32e5b2 100644 --- a/.Lib9c.Tests/Action/CreateAvatar10Test.cs +++ b/.Lib9c.Tests/Action/CreateAvatarTest.cs @@ -17,12 +17,12 @@ namespace Lib9c.Tests.Action using Xunit; using static Lib9c.SerializeKeys; - public class CreateAvatar10Test + public class CreateAvatarTest { private readonly Address _agentAddress; private readonly TableSheets _tableSheets; - public CreateAvatar10Test() + public CreateAvatarTest() { _agentAddress = default; _tableSheets = new TableSheets(TableSheetsImporter.ImportSheets()); @@ -34,7 +34,7 @@ public CreateAvatar10Test() [InlineData(7_210_001L)] public void Execute(long blockIndex) { - var action = new CreateAvatar10() + var action = new CreateAvatar() { index = 0, hair = 0, @@ -106,7 +106,7 @@ public void ExecuteThrowInvalidNamePatterException(string nickName) { var agentAddress = default(Address); - var action = new CreateAvatar10() + var action = new CreateAvatar() { index = 0, hair = 0, @@ -147,7 +147,7 @@ public void ExecuteThrowInvalidAddressException() default ); - var action = new CreateAvatar10() + var action = new CreateAvatar() { index = 0, hair = 0, @@ -175,7 +175,7 @@ public void ExecuteThrowAvatarIndexOutOfRangeException(int index) { var agentState = new AgentState(_agentAddress); var state = new Account(MockState.Empty).SetState(_agentAddress, agentState.Serialize()); - var action = new CreateAvatar10() + var action = new CreateAvatar() { index = index, hair = 0, @@ -211,7 +211,7 @@ public void ExecuteThrowAvatarIndexAlreadyUsedException(int index) agentState.avatarAddresses[index] = avatarAddress; var state = new Account(MockState.Empty).SetState(_agentAddress, agentState.Serialize()); - var action = new CreateAvatar10() + var action = new CreateAvatar() { index = index, hair = 0, @@ -234,7 +234,7 @@ public void ExecuteThrowAvatarIndexAlreadyUsedException(int index) public void Serialize_With_DotnetAPI() { var formatter = new BinaryFormatter(); - var action = new CreateAvatar10() + var action = new CreateAvatar() { index = 2, hair = 1, diff --git a/.Lib9c.Tests/Action/EventDungeonBattleV5Test.cs b/.Lib9c.Tests/Action/EventDungeonBattleTest.cs similarity index 99% rename from .Lib9c.Tests/Action/EventDungeonBattleV5Test.cs rename to .Lib9c.Tests/Action/EventDungeonBattleTest.cs index 07197699ad..940acbfa93 100644 --- a/.Lib9c.Tests/Action/EventDungeonBattleV5Test.cs +++ b/.Lib9c.Tests/Action/EventDungeonBattleTest.cs @@ -20,7 +20,7 @@ namespace Lib9c.Tests.Action using Xunit; using static Lib9c.SerializeKeys; - public class EventDungeonBattleV5Test + public class EventDungeonBattleTest { private readonly Currency _ncgCurrency; private readonly TableSheets _tableSheets; @@ -29,7 +29,7 @@ public class EventDungeonBattleV5Test private readonly Address _avatarAddress; private IAccount _initialStates; - public EventDungeonBattleV5Test() + public EventDungeonBattleTest() { _initialStates = new Account(MockState.Empty); @@ -456,7 +456,7 @@ private IAccount Execute( previousAvatarState.inventory.AddItem(equipment, iLock: null); } - var action = new EventDungeonBattleV5 + var action = new EventDungeonBattle { AvatarAddress = _avatarAddress, EventScheduleId = eventScheduleId, diff --git a/.Lib9c.Tests/Action/HackAndSlashSweep9Test.cs b/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs similarity index 98% rename from .Lib9c.Tests/Action/HackAndSlashSweep9Test.cs rename to .Lib9c.Tests/Action/HackAndSlashSweepTest.cs index 53a7dafbed..f5ee35ae51 100644 --- a/.Lib9c.Tests/Action/HackAndSlashSweep9Test.cs +++ b/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs @@ -20,7 +20,7 @@ namespace Lib9c.Tests.Action using Xunit; using static Lib9c.SerializeKeys; - public class HackAndSlashSweep9Test + public class HackAndSlashSweepTest { private readonly Dictionary _sheets; private readonly TableSheets _tableSheets; @@ -40,7 +40,7 @@ public class HackAndSlashSweep9Test private readonly IAccount _initialState; private readonly IRandom _random; - public HackAndSlashSweep9Test() + public HackAndSlashSweepTest() { _random = new TestRandom(); _sheets = TableSheetsImporter.ImportSheets(); @@ -214,7 +214,7 @@ public void Execute(int apStoneCount, int worldId, int stageId, bool challenge, _tableSheets.MaterialItemSheet); var (equipments, costumes) = GetDummyItems(avatarState); - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { actionPoint = avatarState.actionPoint, costumes = costumes, @@ -253,7 +253,7 @@ public void Execute(int apStoneCount, int worldId, int stageId, bool challenge, [InlineData(false)] public void Execute_FailedLoadStateException(bool backward) { - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { runeInfos = new List(), apStoneCount = 1, @@ -284,7 +284,7 @@ public void Execute_FailedLoadStateException(bool backward) [InlineData(100, 1)] public void Execute_SheetRowNotFoundException(int worldId, int stageId) { - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { runeInfos = new List(), apStoneCount = 1, @@ -311,7 +311,7 @@ public void Execute_SheetRowNotFoundException(int worldId, int stageId) [InlineData(2, 50)] public void Execute_SheetRowColumnException(int worldId, int stageId) { - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { runeInfos = new List(), apStoneCount = 1, @@ -340,7 +340,7 @@ public void Execute_SheetRowColumnException(int worldId, int stageId) [InlineData(1, 49, 2, 51, false)] public void Execute_InvalidStageException(int clearedWorldId, int clearedStageId, int worldId, int stageId, bool backward) { - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { runeInfos = new List(), apStoneCount = 1, @@ -427,7 +427,7 @@ public void Execute_InvalidWorldException(int worldId, bool backward, int stageI ); } - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { runeInfos = new List(), apStoneCount = 1, @@ -482,7 +482,7 @@ public void Execute_UsageLimitExceedException(int apStoneCount, bool backward) avatarState.questList.Serialize()); } - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { runeInfos = new List(), apStoneCount = apStoneCount, @@ -558,7 +558,7 @@ public void Execute_NotEnoughMaterialException(int useApStoneCount, int holdingA var (equipments, costumes) = GetDummyItems(avatarState); - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { equipments = equipments, costumes = costumes, @@ -633,7 +633,7 @@ public void Execute_NotEnoughActionPointException(bool backward) playCount); var (equipments, costumes) = GetDummyItems(avatarState); - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { runeInfos = new List(), costumes = costumes, @@ -709,7 +709,7 @@ public void Execute_PlayCountIsZeroException(bool backward) playCount); var (equipments, costumes) = GetDummyItems(avatarState); - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { costumes = costumes, equipments = equipments, @@ -784,7 +784,7 @@ public void Execute_NotEnoughCombatPointException(int worldId, int stageId, bool stageId, playCount); - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { costumes = new List(), equipments = new List(), @@ -858,7 +858,7 @@ public void ExecuteWithStake(int stakingLevel) stageId, playCount); - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { costumes = new List(), equipments = new List(), @@ -953,7 +953,7 @@ public void ExecuteDuplicatedException(int slotIndex, int runeId, int slotIndex2 RandomSeed = 0, }); - var action = new HackAndSlashSweep9 + var action = new HackAndSlashSweep { costumes = new List(), equipments = new List(), diff --git a/.Lib9c.Tests/Action/ItemEnhancement13Test.cs b/.Lib9c.Tests/Action/ItemEnhancementTest.cs similarity index 99% rename from .Lib9c.Tests/Action/ItemEnhancement13Test.cs rename to .Lib9c.Tests/Action/ItemEnhancementTest.cs index 9c280c1eed..1ed3009a8c 100644 --- a/.Lib9c.Tests/Action/ItemEnhancement13Test.cs +++ b/.Lib9c.Tests/Action/ItemEnhancementTest.cs @@ -21,7 +21,7 @@ namespace Lib9c.Tests.Action using Xunit; using static SerializeKeys; - public class ItemEnhancement13Test + public class ItemEnhancementTest { private readonly TableSheets _tableSheets; private readonly Address _agentAddress; @@ -30,7 +30,7 @@ public class ItemEnhancement13Test private readonly Currency _currency; private IAccount _initialState; - public ItemEnhancement13Test() + public ItemEnhancementTest() { _initialState = new Account(MockState.Empty); Dictionary sheets; @@ -297,7 +297,7 @@ public void Execute( ) .SetState(_avatarAddress, _avatarState.SerializeV2()); - var action = new ItemEnhancement13 + var action = new ItemEnhancement { itemId = default, materialIds = materialIds, diff --git a/.Lib9c.Tests/Action/Raid6Test.cs b/.Lib9c.Tests/Action/RaidTest.cs similarity index 99% rename from .Lib9c.Tests/Action/Raid6Test.cs rename to .Lib9c.Tests/Action/RaidTest.cs index b377efedec..e13531a033 100644 --- a/.Lib9c.Tests/Action/Raid6Test.cs +++ b/.Lib9c.Tests/Action/RaidTest.cs @@ -19,7 +19,7 @@ namespace Lib9c.Tests.Action using Xunit; using static SerializeKeys; - public class Raid6Test + public class RaidTest { private readonly Dictionary _sheets; private readonly Address _agentAddress; @@ -27,7 +27,7 @@ public class Raid6Test private readonly TableSheets _tableSheets; private readonly Currency _goldCurrency; - public Raid6Test() + public RaidTest() { _sheets = TableSheetsImporter.ImportSheets(); _tableSheets = new TableSheets(_sheets); @@ -105,7 +105,7 @@ int runeId2 }) .StartedBlockIndex; - var action = new Raid6 + var action = new Raid { AvatarAddress = _avatarAddress, EquipmentIds = new List(), @@ -417,7 +417,7 @@ Dictionary rewardMap [Fact] public void Execute_With_Reward() { - var action = new Raid6 + var action = new Raid { AvatarAddress = _avatarAddress, EquipmentIds = new List(), @@ -571,7 +571,7 @@ Dictionary rewardMap [Fact] public void Execute_With_Free_Crystal_Fee() { - var action = new Raid6 + var action = new Raid { AvatarAddress = _avatarAddress, EquipmentIds = new List(), diff --git a/.Lib9c.Tests/Action/RapidCombination9Test.cs b/.Lib9c.Tests/Action/RapidCombinationTest.cs similarity index 98% rename from .Lib9c.Tests/Action/RapidCombination9Test.cs rename to .Lib9c.Tests/Action/RapidCombinationTest.cs index 25833ae3d5..a89d5ba078 100644 --- a/.Lib9c.Tests/Action/RapidCombination9Test.cs +++ b/.Lib9c.Tests/Action/RapidCombinationTest.cs @@ -23,7 +23,7 @@ namespace Lib9c.Tests.Action using Xunit; using static Lib9c.SerializeKeys; - public class RapidCombination9Test + public class RapidCombinationTest { private readonly IAccount _initialState; @@ -32,7 +32,7 @@ public class RapidCombination9Test private readonly Address _agentAddress; private readonly Address _avatarAddress; - public RapidCombination9Test() + public RapidCombinationTest() { _initialState = new Account(MockState.Empty); Dictionary sheets; @@ -147,7 +147,7 @@ public void Execute(bool backward) .SetState(_avatarAddress, avatarState.SerializeV2()); } - var action = new RapidCombination9 + var action = new RapidCombination { avatarAddress = _avatarAddress, slotIndex = 0, @@ -181,7 +181,7 @@ public void Execute_Throw_CombinationSlotResultNullException() var tempState = _initialState .SetState(slotAddress, slotState.Serialize()); - var action = new RapidCombination9 + var action = new RapidCombination { avatarAddress = _avatarAddress, slotIndex = 0, @@ -235,7 +235,7 @@ public void Execute_Throw_NotEnoughClearedStageLevelException(int avatarClearedS .SetState(_avatarAddress, avatarState.Serialize()) .SetState(slotAddress, slotState.Serialize()); - var action = new RapidCombination9 + var action = new RapidCombination { avatarAddress = _avatarAddress, slotIndex = 0, @@ -291,7 +291,7 @@ public void Execute_Throw_RequiredBlockIndexException(int itemRequiredBlockIndex .SetState(_avatarAddress, avatarState.Serialize()) .SetState(slotAddress, slotState.Serialize()); - var action = new RapidCombination9 + var action = new RapidCombination { avatarAddress = _avatarAddress, slotIndex = 0, @@ -367,7 +367,7 @@ public void Execute_Throw_NotEnoughMaterialException(int materialCount, int trad .SetState(_avatarAddress, avatarState.Serialize()) .SetState(slotAddress, slotState.Serialize()); - var action = new RapidCombination9 + var action = new RapidCombination { avatarAddress = _avatarAddress, slotIndex = 0, @@ -496,7 +496,7 @@ public void Execute_Throw_RequiredAppraiseBlockException() .SetState(_avatarAddress, avatarState.Serialize()) .SetState(slotAddress, slotState.Serialize()); - var action = new RapidCombination9 + var action = new RapidCombination { avatarAddress = _avatarAddress, slotIndex = 0, @@ -667,7 +667,7 @@ public void Execute_NotThrow_InvalidOperationException_When_TargetSlotCreatedBy( .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) .SetState(_avatarAddress, avatarState.SerializeV2()); - var action = new RapidCombination9 + var action = new RapidCombination { avatarAddress = _avatarAddress, slotIndex = 0, From 31f60f393c31a2369d685f2580179d020c7c9bae Mon Sep 17 00:00:00 2001 From: Yang Chun Ung Date: Thu, 21 Mar 2024 15:12:23 +0900 Subject: [PATCH 03/16] Fix broken test from skill id changed --- .Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs | 9 ++++----- .Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs | 2 +- .Lib9c.Tests/Model/Skill/CombatTest.cs | 8 +++----- .Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs | 2 +- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs b/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs index 97307544db..1cee8a57dd 100644 --- a/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs +++ b/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs @@ -6,6 +6,7 @@ namespace Lib9c.Tests.Model.Skill.Arena using Nekoyume.Arena; using Nekoyume.Model; using Nekoyume.Model.Buff; + using Nekoyume.Model.Skill; using Nekoyume.Model.Skill.Arena; using Nekoyume.Model.Stat; using Nekoyume.Model.State; @@ -13,8 +14,6 @@ namespace Lib9c.Tests.Model.Skill.Arena public class ArenaCombatTest { - private const int ActionBuffId = 708000; // Dispel with duration - private readonly TableSheets _tableSheets; private readonly AvatarState _avatar1; private readonly AvatarState _avatar2; @@ -48,7 +47,7 @@ public ArenaCombatTest() [Theory] [InlineData(700009, new[] { 600001 })] - [InlineData(700010, new[] { 600001, 704000 })] + [InlineData(700009, new[] { 600001, 704000 })] public void DispelOnUse(int dispelId, int[] debuffIdList) { var arenaSheets = _tableSheets.GetArenaSimulatorSheets(); @@ -122,7 +121,7 @@ public void DispelOnDuration_Block() ); // Use Dispel first - var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.Id == ActionBuffId); + var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); challenger.AddBuff(BuffFactory.GetActionBuff(challenger.Stats, dispel)); Assert.Single(challenger.Buffs); @@ -170,7 +169,7 @@ public void DispelOnDuration_Affect() ); // Use Dispel first - var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.Id == ActionBuffId); + var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); challenger.AddBuff(BuffFactory.GetActionBuff(challenger.Stats, dispel)); Assert.Single(challenger.Buffs); diff --git a/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs b/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs index 4aaec7502b..c616f3e393 100644 --- a/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs +++ b/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs @@ -73,7 +73,7 @@ public void Use(int ratioBp) new List() ); - var skillRow = _tableSheets.SkillSheet.OrderedList.First(s => s.Id == 700011); + var skillRow = _tableSheets.SkillSheet.OrderedList.First(s => s.Id == 700010); var shatterStrike = new ArenaShatterStrike(skillRow, 0, 0, ratioBp, StatType.NONE); var used = shatterStrike.Use(challenger, enemy, simulator.Turn, new List()); Assert.Single(used.SkillInfos); diff --git a/.Lib9c.Tests/Model/Skill/CombatTest.cs b/.Lib9c.Tests/Model/Skill/CombatTest.cs index 652f03b4b2..b01d7fce99 100644 --- a/.Lib9c.Tests/Model/Skill/CombatTest.cs +++ b/.Lib9c.Tests/Model/Skill/CombatTest.cs @@ -139,7 +139,7 @@ public void Bleed() [Theory] [InlineData(700009, new[] { 600001 })] - [InlineData(700010, new[] { 600001, 704000 })] + [InlineData(700009, new[] { 600001, 704000 })] public void DispelOnUse(int dispelId, int[] debuffIdList) { var actionBuffSheet = _tableSheets.ActionBuffSheet; @@ -182,11 +182,10 @@ public void DispelOnUse(int dispelId, int[] debuffIdList) [Fact] public void DispelOnDuration_Block() { - const int actionBuffId = 708000; // Dispel with duration var actionBuffSheet = _tableSheets.ActionBuffSheet; // Use Dispel first - var dispel = actionBuffSheet.Values.First(bf => bf.Id == actionBuffId); + var dispel = actionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); _player.AddBuff(BuffFactory.GetActionBuff(_player.Stats, dispel)); Assert.Single(_player.Buffs); @@ -217,11 +216,10 @@ public void DispelOnDuration_Block() [Fact] public void DispelOnDuration_Affect() { - const int actionBuffId = 708000; // Dispel with duration var actionBuffSheet = _tableSheets.ActionBuffSheet; // Use Dispel first - var dispel = actionBuffSheet.Values.First(bf => bf.Id == actionBuffId); + var dispel = actionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); _player.AddBuff(BuffFactory.GetActionBuff(_player.Stats, dispel)); Assert.Single(_player.Buffs); diff --git a/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs b/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs index 190ce2540c..142a08efbb 100644 --- a/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs +++ b/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs @@ -30,7 +30,7 @@ public class ShatterStrikeTest public void Use(int ratioBp, bool copyCharacter) { Assert.True( - _tableSheets.SkillSheet.TryGetValue(700011, out var skillRow) + _tableSheets.SkillSheet.TryGetValue(700010, out var skillRow) ); // 700011 is ShatterStrike var shatterStrike = new ShatterStrike(skillRow, 0, 0, ratioBp, StatType.NONE); From 6261c0e30e309480dc54557c71cd828544a77c1f Mon Sep 17 00:00:00 2001 From: Yang Chun Ung Date: Thu, 21 Mar 2024 15:12:50 +0900 Subject: [PATCH 04/16] Restore CombinationConsumableTest --- .../Action/CombinationConsumableTest.cs | 38 ++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/.Lib9c.Tests/Action/CombinationConsumableTest.cs b/.Lib9c.Tests/Action/CombinationConsumableTest.cs index bc66bc3981..44844b56f3 100644 --- a/.Lib9c.Tests/Action/CombinationConsumableTest.cs +++ b/.Lib9c.Tests/Action/CombinationConsumableTest.cs @@ -12,8 +12,8 @@ namespace Lib9c.Tests.Action using Nekoyume.Model.Item; using Nekoyume.Model.Mail; using Nekoyume.Model.State; + using Nekoyume.Module; using Xunit; - using static Lib9c.SerializeKeys; public class CombinationConsumableTest { @@ -21,7 +21,7 @@ public class CombinationConsumableTest private readonly Address _avatarAddress; private readonly IRandom _random; private readonly TableSheets _tableSheets; - private IAccount _initialState; + private IWorld _initialState; public CombinationConsumableTest() { @@ -57,27 +57,25 @@ public CombinationConsumableTest() var gold = new GoldCurrencyState(Currency.Legacy("NCG", 2, null)); #pragma warning restore CS0618 - _initialState = new Account(MockState.Empty) - .SetState(_agentAddress, agentState.Serialize()) - .SetState(_avatarAddress, avatarState.Serialize()) - .SetState( + _initialState = new World(new MockWorldState()) + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState( slotAddress, new CombinationSlotState( slotAddress, GameConfig.RequireClearedStageLevel.CombinationConsumableAction).Serialize()) - .SetState(GameConfigState.Address, gold.Serialize()); + .SetLegacyState(GameConfigState.Address, gold.Serialize()); foreach (var (key, value) in sheets) { _initialState = - _initialState.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + _initialState.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Execute(bool backward) + [Fact] + public void Execute() { var avatarState = _initialState.GetAvatarState(_avatarAddress); var row = _tableSheets.ConsumableItemRecipeSheet.Values.First(); @@ -99,19 +97,7 @@ public void Execute(bool backward) _tableSheets.WorldSheet, GameConfig.RequireClearedStageLevel.CombinationConsumableAction); - IAccount previousState; - if (backward) - { - previousState = _initialState.SetState(_avatarAddress, avatarState.Serialize()); - } - else - { - previousState = _initialState - .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) - .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) - .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) - .SetState(_avatarAddress, avatarState.SerializeV2()); - } + IWorld previousState = _initialState.SetAvatarState(_avatarAddress, avatarState); var action = new CombinationConsumable { @@ -135,7 +121,7 @@ public void Execute(bool backward) var consumable = (Consumable)slotState.Result.itemUsable; Assert.NotNull(consumable); - var nextAvatarState = nextState.GetAvatarStateV2(_avatarAddress); + var nextAvatarState = nextState.GetAvatarState(_avatarAddress); Assert.Equal(previousActionPoint - costActionPoint, nextAvatarState.actionPoint); Assert.Equal(previousMailCount + 1, nextAvatarState.mailBox.Count); Assert.IsType(nextAvatarState.mailBox.First()); From d8296649aa8f23cf4aeecbcd2fba1e1521c6285a Mon Sep 17 00:00:00 2001 From: Yang Chun Ung Date: Thu, 21 Mar 2024 15:13:05 +0900 Subject: [PATCH 05/16] Restore CreateAvatarTest --- .Lib9c.Tests/Action/CreateAvatarTest.cs | 61 +++++++------------------ 1 file changed, 17 insertions(+), 44 deletions(-) diff --git a/.Lib9c.Tests/Action/CreateAvatarTest.cs b/.Lib9c.Tests/Action/CreateAvatarTest.cs index c06f32e5b2..127a3d172e 100644 --- a/.Lib9c.Tests/Action/CreateAvatarTest.cs +++ b/.Lib9c.Tests/Action/CreateAvatarTest.cs @@ -13,6 +13,7 @@ namespace Lib9c.Tests.Action using Nekoyume.Action; using Nekoyume.Helper; using Nekoyume.Model.State; + using Nekoyume.Module; using Nekoyume.TableData; using Xunit; using static Lib9c.SerializeKeys; @@ -45,15 +46,15 @@ public void Execute(long blockIndex) }; var sheets = TableSheetsImporter.ImportSheets(); - var state = new Account(MockState.Empty) - .SetState( + var state = new World(new MockWorldState()) + .SetLegacyState( Addresses.GameConfig, new GameConfigState(sheets[nameof(GameConfigSheet)]).Serialize() ); foreach (var (key, value) in sheets) { - state = state.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } Assert.Equal(0 * CrystalCalculator.CRYSTAL, state.GetBalance(_agentAddress, CrystalCalculator.CRYSTAL)); @@ -69,17 +70,17 @@ public void Execute(long blockIndex) var avatarAddress = _agentAddress.Derive( string.Format( CultureInfo.InvariantCulture, - CreateAvatar2.DeriveFormat, + CreateAvatar.DeriveFormat, 0 ) ); - Assert.True(nextState.TryGetAgentAvatarStatesV2( + Assert.True(nextState.TryGetAvatarState( default, avatarAddress, - out var agentState, - out var nextAvatarState, - out _) + out var nextAvatarState) ); + var agentState = nextState.GetAgentState(default); + Assert.NotNull(agentState); Assert.True(agentState.avatarAddresses.Any()); Assert.Equal("test", nextAvatarState.name); Assert.Equal(200_000 * CrystalCalculator.CRYSTAL, nextState.GetBalance(_agentAddress, CrystalCalculator.CRYSTAL)); @@ -116,7 +117,7 @@ public void ExecuteThrowInvalidNamePatterException(string nickName) name = nickName, }; - var state = new Account(MockState.Empty); + var state = new World(new MockWorldState()); Assert.Throws(() => action.Execute(new ActionContext() { @@ -133,7 +134,7 @@ public void ExecuteThrowInvalidAddressException() var avatarAddress = _agentAddress.Derive( string.Format( CultureInfo.InvariantCulture, - CreateAvatar2.DeriveFormat, + CreateAvatar.DeriveFormat, 0 ) ); @@ -157,7 +158,7 @@ public void ExecuteThrowInvalidAddressException() name = "test", }; - var state = new Account(MockState.Empty).SetState(avatarAddress, avatarState.Serialize()); + var state = new World(new MockWorldState()).SetAvatarState(avatarAddress, avatarState); Assert.Throws(() => action.Execute(new ActionContext() { @@ -174,7 +175,7 @@ public void ExecuteThrowInvalidAddressException() public void ExecuteThrowAvatarIndexOutOfRangeException(int index) { var agentState = new AgentState(_agentAddress); - var state = new Account(MockState.Empty).SetState(_agentAddress, agentState.Serialize()); + var state = new World(new MockWorldState()).SetAgentState(_agentAddress, agentState); var action = new CreateAvatar() { index = index, @@ -204,12 +205,12 @@ public void ExecuteThrowAvatarIndexAlreadyUsedException(int index) var avatarAddress = _agentAddress.Derive( string.Format( CultureInfo.InvariantCulture, - CreateAvatar2.DeriveFormat, + CreateAvatar.DeriveFormat, 0 ) ); agentState.avatarAddresses[index] = avatarAddress; - var state = new Account(MockState.Empty).SetState(_agentAddress, agentState.Serialize()); + var state = new World(new MockWorldState()).SetAgentState(_agentAddress, agentState); var action = new CreateAvatar() { @@ -230,34 +231,6 @@ public void ExecuteThrowAvatarIndexAlreadyUsedException(int index) ); } - [Fact] - public void Serialize_With_DotnetAPI() - { - var formatter = new BinaryFormatter(); - var action = new CreateAvatar() - { - index = 2, - hair = 1, - ear = 4, - lens = 5, - tail = 7, - name = "test", - }; - - using var ms = new MemoryStream(); - formatter.Serialize(ms, action); - - ms.Seek(0, SeekOrigin.Begin); - var deserialized = (CreateAvatar10)formatter.Deserialize(ms); - - Assert.Equal(2, deserialized.index); - Assert.Equal(1, deserialized.hair); - Assert.Equal(4, deserialized.ear); - Assert.Equal(5, deserialized.lens); - Assert.Equal(7, deserialized.tail); - Assert.Equal("test", deserialized.name); - } - [Fact] public void AddItem() { @@ -269,7 +242,7 @@ public void AddItem() 600201,2 "); var avatarState = new AvatarState(default, default, 0L, _tableSheets.GetAvatarSheets(), new GameConfigState(), default, "test"); - CreateAvatar10.AddItem(itemSheet, createAvatarItemSheet, avatarState, new TestRandom()); + CreateAvatar.AddItem(itemSheet, createAvatarItemSheet, avatarState, new TestRandom()); foreach (var row in createAvatarItemSheet.Values) { Assert.True(avatarState.inventory.HasItem(row.ItemId, row.Count)); @@ -294,7 +267,7 @@ public void MintAsset() var avatarAddress = new PrivateKey().Address; var agentAddress = new PrivateKey().Address; var avatarState = new AvatarState(avatarAddress, agentAddress, 0L, _tableSheets.GetAvatarSheets(), new GameConfigState(), default, "test"); - var nextState = CreateAvatar10.MintAsset(createAvatarFavSheet, avatarState, new Account(MockState.Empty), new ActionContext()); + var nextState = CreateAvatar.MintAsset(createAvatarFavSheet, avatarState, new World(new MockWorldState()), new ActionContext()); foreach (var row in createAvatarFavSheet.Values) { var targetAddress = row.Target == CreateAvatarFavSheet.Target.Agent From e96956df1500cffb2549aaaa2c8721cc89ebfb3b Mon Sep 17 00:00:00 2001 From: Yang Chun Ung Date: Thu, 21 Mar 2024 15:13:12 +0900 Subject: [PATCH 06/16] Restore EventDungeonBattleTest --- .Lib9c.Tests/Action/EventDungeonBattleTest.cs | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/.Lib9c.Tests/Action/EventDungeonBattleTest.cs b/.Lib9c.Tests/Action/EventDungeonBattleTest.cs index 940acbfa93..92d23c0ced 100644 --- a/.Lib9c.Tests/Action/EventDungeonBattleTest.cs +++ b/.Lib9c.Tests/Action/EventDungeonBattleTest.cs @@ -9,16 +9,15 @@ namespace Lib9c.Tests.Action using Libplanet.Types.Assets; using Nekoyume; using Nekoyume.Action; - using Nekoyume.Blockchain.Policy; using Nekoyume.Exceptions; using Nekoyume.Extensions; using Nekoyume.Model.Event; using Nekoyume.Model.Rune; using Nekoyume.Model.State; + using Nekoyume.Module; using Nekoyume.TableData; using Nekoyume.TableData.Event; using Xunit; - using static Lib9c.SerializeKeys; public class EventDungeonBattleTest { @@ -27,33 +26,30 @@ public class EventDungeonBattleTest private readonly Address _agentAddress; private readonly Address _avatarAddress; - private IAccount _initialStates; + private IWorld _initialStates; public EventDungeonBattleTest() { - _initialStates = new Account(MockState.Empty); + _initialStates = new World(new MockWorldState()); #pragma warning disable CS0618 // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 _ncgCurrency = Currency.Legacy("NCG", 2, null); #pragma warning restore CS0618 - _initialStates = _initialStates.SetState( + _initialStates = _initialStates.SetLegacyState( GoldCurrencyState.Address, new GoldCurrencyState(_ncgCurrency).Serialize()); var sheets = TableSheetsImporter.ImportSheets(); foreach (var (key, value) in sheets) { _initialStates = _initialStates - .SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + .SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } _tableSheets = new TableSheets(sheets); _agentAddress = new PrivateKey().Address; _avatarAddress = _agentAddress.Derive("avatar"); - var inventoryAddr = _avatarAddress.Derive(LegacyInventoryKey); - var worldInformationAddr = _avatarAddress.Derive(LegacyWorldInformationKey); - var questListAddr = _avatarAddress.Derive(LegacyQuestListKey); var agentState = new AgentState(_agentAddress); agentState.avatarAddresses.Add(0, _avatarAddress); @@ -72,12 +68,9 @@ public EventDungeonBattleTest() }; _initialStates = _initialStates - .SetState(_agentAddress, agentState.Serialize()) - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState(inventoryAddr, avatarState.inventory.Serialize()) - .SetState(worldInformationAddr, avatarState.worldInformation.Serialize()) - .SetState(questListAddr, avatarState.questList.Serialize()) - .SetState(gameConfigState.address, gameConfigState.Serialize()); + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); } [Theory] @@ -99,7 +92,7 @@ public void Execute_Success_Within_Event_Period( var eventDungeonInfoAddr = EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); var eventDungeonInfo = - new EventDungeonInfo(nextStates.GetState(eventDungeonInfoAddr)); + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); Assert.Equal( scheduleRow.DungeonTicketsMax - 1, eventDungeonInfo.RemainingTickets); @@ -112,7 +105,7 @@ public void Execute_Success_Within_Event_Period( eventDungeonStageId, blockIndex: contextBlockIndex); eventDungeonInfo = - new EventDungeonInfo(nextStates.GetState(eventDungeonInfoAddr)); + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); Assert.Equal( scheduleRow.DungeonTicketsMax - 1, eventDungeonInfo.RemainingTickets); @@ -148,7 +141,7 @@ public void Execute_Success_With_Ticket_Purchase( $",{dungeonTicketAdditionalPrice}" + $",{scheduleRow.DungeonExpSeedValue}" + $",{scheduleRow.RecipeEndBlockIndex}"); - previousStates = previousStates.SetState( + previousStates = previousStates.SetLegacyState( Addresses.GetSheetAddress(), sb.ToString().Serialize()); @@ -157,7 +150,7 @@ public void Execute_Success_With_Ticket_Purchase( var eventDungeonInfo = new EventDungeonInfo( remainingTickets: 0, numberOfTicketPurchases: numberOfTicketPurchases); - previousStates = previousStates.SetState( + previousStates = previousStates.SetLegacyState( eventDungeonInfoAddr, eventDungeonInfo.Serialize()); @@ -179,7 +172,7 @@ public void Execute_Success_With_Ticket_Purchase( buyTicketIfNeeded: true, blockIndex: scheduleRow.StartBlockIndex); var nextEventDungeonInfoList = - (Bencodex.Types.List)nextStates.GetState(eventDungeonInfoAddr)!; + (Bencodex.Types.List)nextStates.GetLegacyState(eventDungeonInfoAddr)!; Assert.Equal( numberOfTicketPurchases + 1, nextEventDungeonInfoList[2].ToInteger()); @@ -284,7 +277,7 @@ public void Execute_Throw_NotEnoughEventDungeonTicketsException( EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); var eventDungeonInfo = new EventDungeonInfo(); previousStates = previousStates - .SetState(eventDungeonInfoAddr, eventDungeonInfo.Serialize()); + .SetLegacyState(eventDungeonInfoAddr, eventDungeonInfo.Serialize()); Assert.True(_tableSheets.EventScheduleSheet .TryGetValue(eventScheduleId, out var scheduleRow)); Assert.Throws(() => @@ -313,7 +306,7 @@ public void Execute_Throw_InsufficientBalanceException( remainingTickets: 0, numberOfTicketPurchases: numberOfTicketPurchases); previousStates = previousStates - .SetState(eventDungeonInfoAddr, eventDungeonInfo.Serialize()); + .SetLegacyState(eventDungeonInfoAddr, eventDungeonInfo.Serialize()); Assert.True(_tableSheets.EventScheduleSheet .TryGetValue(eventScheduleId, out var scheduleRow)); @@ -401,7 +394,7 @@ public void Execute_V100301() var csv = $@"id,_name,start_block_index,dungeon_end_block_index,dungeon_tickets_max,dungeon_tickets_reset_interval_block_range,dungeon_ticket_price,dungeon_ticket_additional_price,dungeon_exp_seed_value,recipe_end_block_index 1001,2022 Summer Event,{ActionObsoleteConfig.V100301ExecutedBlockIndex},{ActionObsoleteConfig.V100301ExecutedBlockIndex + 100},5,7200,5,2,1,5018000"; _initialStates = - _initialStates.SetState( + _initialStates.SetLegacyState( Addresses.GetSheetAddress(), csv.Serialize()); var sheet = new EventScheduleSheet(); @@ -417,7 +410,7 @@ public void Execute_V100301() var eventDungeonInfoAddr = EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); var eventDungeonInfo = - new EventDungeonInfo(nextStates.GetState(eventDungeonInfoAddr)); + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); Assert.Equal( scheduleRow.DungeonTicketsMax - 1, eventDungeonInfo.RemainingTickets); @@ -430,14 +423,14 @@ public void Execute_V100301() eventDungeonStageId, blockIndex: contextBlockIndex); eventDungeonInfo = - new EventDungeonInfo(nextStates.GetState(eventDungeonInfoAddr)); + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); Assert.Equal( scheduleRow.DungeonTicketsMax - 1, eventDungeonInfo.RemainingTickets); } - private IAccount Execute( - IAccount previousStates, + private IWorld Execute( + IWorld previousStates, int eventScheduleId, int eventDungeonId, int eventDungeonStageId, @@ -448,7 +441,7 @@ private IAccount Execute( int slotIndex2 = 1, int runeId2 = 30001) { - var previousAvatarState = previousStates.GetAvatarStateV2(_avatarAddress); + var previousAvatarState = previousStates.GetAvatarState(_avatarAddress); var equipments = Doomfist.GetAllParts(_tableSheets, previousAvatarState.level); foreach (var equipment in equipments) @@ -486,7 +479,7 @@ private IAccount Execute( Assert.True(nextStates.GetSheet().TryGetValue( eventScheduleId, out var scheduleRow)); - var nextAvatarState = nextStates.GetAvatarStateV2(_avatarAddress); + var nextAvatarState = nextStates.GetAvatarState(_avatarAddress); var expectExp = scheduleRow.GetStageExp( eventDungeonStageId.ToEventDungeonStageNumber()); Assert.Equal( From e524d48c3042ea58bcb868db304b4e07b466aec2 Mon Sep 17 00:00:00 2001 From: Yang Chun Ung Date: Thu, 21 Mar 2024 15:13:22 +0900 Subject: [PATCH 07/16] Restore HackAndSlashSweepTest --- .Lib9c.Tests/Action/HackAndSlashSweepTest.cs | 377 +++---------------- 1 file changed, 52 insertions(+), 325 deletions(-) diff --git a/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs b/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs index f5ee35ae51..f14fe1e177 100644 --- a/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs +++ b/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs @@ -16,9 +16,9 @@ namespace Lib9c.Tests.Action using Nekoyume.Model.Item; using Nekoyume.Model.Rune; using Nekoyume.Model.State; + using Nekoyume.Module; using Nekoyume.TableData; using Xunit; - using static Lib9c.SerializeKeys; public class HackAndSlashSweepTest { @@ -30,14 +30,10 @@ public class HackAndSlashSweepTest private readonly Address _avatarAddress; private readonly AvatarState _avatarState; - private readonly Address _inventoryAddress; - private readonly Address _worldInformationAddress; - private readonly Address _questListAddress; - private readonly Address _rankingMapAddress; private readonly WeeklyArenaState _weeklyArenaState; - private readonly IAccount _initialState; + private readonly IWorld _initialState; private readonly IRandom _random; public HackAndSlashSweepTest() @@ -64,9 +60,6 @@ public HackAndSlashSweepTest() { level = 100, }; - _inventoryAddress = _avatarAddress.Derive(LegacyInventoryKey); - _worldInformationAddress = _avatarAddress.Derive(LegacyWorldInformationKey); - _questListAddress = _avatarAddress.Derive(LegacyQuestListKey); agentState.avatarAddresses.Add(0, _avatarAddress); #pragma warning disable CS0618 @@ -75,20 +68,17 @@ public HackAndSlashSweepTest() #pragma warning restore CS0618 var goldCurrencyState = new GoldCurrencyState(currency); _weeklyArenaState = new WeeklyArenaState(0); - _initialState = new Account(MockState.Empty) - .SetState(_weeklyArenaState.address, _weeklyArenaState.Serialize()) - .SetState(_agentAddress, agentState.SerializeV2()) - .SetState(_avatarAddress, _avatarState.SerializeV2()) - .SetState(_inventoryAddress, _avatarState.inventory.Serialize()) - .SetState(_worldInformationAddress, _avatarState.worldInformation.Serialize()) - .SetState(_questListAddress, _avatarState.questList.Serialize()) - .SetState(gameConfigState.address, gameConfigState.Serialize()) - .SetState(Addresses.GoldCurrency, goldCurrencyState.Serialize()); + _initialState = new World(new MockWorldState()) + .SetLegacyState(_weeklyArenaState.address, _weeklyArenaState.Serialize()) + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, _avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()) + .SetLegacyState(Addresses.GoldCurrency, goldCurrencyState.Serialize()); foreach (var (key, value) in _sheets) { _initialState = _initialState - .SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + .SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } foreach (var address in _avatarState.combinationSlotAddresses) @@ -96,7 +86,7 @@ public HackAndSlashSweepTest() var slotState = new CombinationSlotState( address, GameConfig.RequireClearedStageLevel.CombinationEquipmentAction); - _initialState = _initialState.SetState(address, slotState.Serialize()); + _initialState = _initialState.SetLegacyState(address, slotState.Serialize()); } } @@ -123,135 +113,8 @@ public HackAndSlashSweepTest() return (equipments, costumes); } - [Theory] - [InlineData(1, 1, 1, false, true)] - [InlineData(1, 1, 1, false, false)] - [InlineData(2, 1, 2, false, true)] - [InlineData(2, 1, 2, false, false)] - [InlineData(2, 2, 51, false, true)] - [InlineData(2, 2, 51, false, false)] - [InlineData(2, 2, 52, false, true)] - [InlineData(2, 2, 52, false, false)] - [InlineData(2, 1, 1, true, true)] - [InlineData(2, 1, 1, true, false)] - [InlineData(2, 1, 2, true, true)] - [InlineData(2, 1, 2, true, false)] - [InlineData(2, 2, 51, true, true)] - [InlineData(2, 2, 51, true, false)] - [InlineData(2, 2, 52, true, true)] - [InlineData(2, 2, 52, true, false)] - public void Execute(int apStoneCount, int worldId, int stageId, bool challenge, bool backward) - { - var gameConfigState = _initialState.GetGameConfigState(); - var prevStageId = stageId - 1; - var worldInformation = new WorldInformation( - 0, _initialState.GetSheet(), challenge ? prevStageId : stageId); - - if (challenge) - { - worldInformation.UnlockWorld(worldId, 0, _tableSheets.WorldSheet); - } - - var avatarState = new AvatarState( - _avatarAddress, - _agentAddress, - 0, - _initialState.GetAvatarSheets(), - gameConfigState, - _rankingMapAddress) - { - worldInformation = worldInformation, - level = 400, - }; - - var row = _tableSheets.MaterialItemSheet.Values.First(r => - r.ItemSubType == ItemSubType.ApStone); - var apStone = ItemFactory.CreateTradableMaterial(row); - avatarState.inventory.AddItem(apStone, apStoneCount); - - IAccount state; - if (backward) - { - state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); - } - else - { - state = _initialState - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState( - _avatarAddress.Derive(LegacyInventoryKey), - avatarState.inventory.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyWorldInformationKey), - avatarState.worldInformation.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyQuestListKey), - avatarState.questList.Serialize()); - } - - state = state.SetState( - _avatarAddress.Derive("world_ids"), - List.Empty.Add(worldId.Serialize()) - ); - - var stageSheet = _initialState.GetSheet(); - var (expectedLevel, expectedExp) = (0, 0L); - if (stageSheet.TryGetValue(stageId, out var stageRow)) - { - var itemPlayCount = gameConfigState.ActionPointMax / stageRow.CostAP * apStoneCount; - var apPlayCount = avatarState.actionPoint / stageRow.CostAP; - var playCount = apPlayCount + itemPlayCount; - (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( - _tableSheets.CharacterLevelSheet, - stageId, - playCount); - - var random = new TestRandom(_random.Seed); - var expectedRewardItems = HackAndSlashSweep6.GetRewardItems( - random, - playCount, - stageRow, - _tableSheets.MaterialItemSheet); - - var (equipments, costumes) = GetDummyItems(avatarState); - var action = new HackAndSlashSweep - { - actionPoint = avatarState.actionPoint, - costumes = costumes, - equipments = equipments, - runeInfos = new List(), - avatarAddress = _avatarAddress, - apStoneCount = apStoneCount, - worldId = worldId, - stageId = stageId, - }; - - state = action.Execute(new ActionContext - { - PreviousState = state, - Signer = _agentAddress, - RandomSeed = _random.Seed, - }); - - var nextAvatarState = state.GetAvatarStateV2(_avatarAddress); - - Assert.Equal(expectedLevel, nextAvatarState.level); - Assert.Equal(expectedExp, nextAvatarState.exp); - Assert.Equal( - expectedRewardItems.Count(), - nextAvatarState.inventory.Items.Sum(x => x.count)); - foreach (var i in nextAvatarState.inventory.Items) - { - nextAvatarState.inventory.TryGetItem(i.item.Id, out var item); - Assert.Equal(expectedRewardItems.Count(x => x.Id == i.item.Id), item.count); - } - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Execute_FailedLoadStateException(bool backward) + [Fact] + public void Execute_FailedLoadStateException() { var action = new HackAndSlashSweep { @@ -262,15 +125,7 @@ public void Execute_FailedLoadStateException(bool backward) stageId = 1, }; - var state = backward ? new Account(MockState.Empty) : _initialState; - if (!backward) - { - state = _initialState - .SetState(_avatarAddress, _avatarState.SerializeV2()) - .SetNull(_avatarAddress.Derive(LegacyInventoryKey)) - .SetNull(_avatarAddress.Derive(LegacyWorldInformationKey)) - .SetNull(_avatarAddress.Derive(LegacyQuestListKey)); - } + IWorld state = new World(new MockWorldState()); Assert.Throws(() => action.Execute(new ActionContext() { @@ -293,7 +148,7 @@ public void Execute_SheetRowNotFoundException(int worldId, int stageId) stageId = stageId, }; - var state = _initialState.SetState( + var state = _initialState.SetLegacyState( _avatarAddress.Derive("world_ids"), List.Empty.Add(worldId.Serialize()) ); @@ -320,7 +175,7 @@ public void Execute_SheetRowColumnException(int worldId, int stageId) stageId = stageId, }; - var state = _initialState.SetState( + var state = _initialState.SetLegacyState( _avatarAddress.Derive("world_ids"), List.Empty.Add(worldId.Serialize()) ); @@ -334,11 +189,9 @@ public void Execute_SheetRowColumnException(int worldId, int stageId) } [Theory] - [InlineData(1, 48, 1, 50, true)] - [InlineData(1, 48, 1, 50, false)] - [InlineData(1, 49, 2, 51, true)] - [InlineData(1, 49, 2, 51, false)] - public void Execute_InvalidStageException(int clearedWorldId, int clearedStageId, int worldId, int stageId, bool backward) + [InlineData(1, 48, 1, 50)] + [InlineData(1, 49, 2, 51)] + public void Execute_InvalidStageException(int clearedWorldId, int clearedStageId, int worldId, int stageId) { var action = new HackAndSlashSweep { @@ -353,22 +206,12 @@ public void Execute_InvalidStageException(int clearedWorldId, int clearedStageId _avatarState.worldInformation.ClearStage(clearedWorldId, clearedStageId, 1, worldSheet, worldUnlockSheet); - var state = _initialState.SetState( + var state = _initialState + .SetLegacyState( _avatarAddress.Derive("world_ids"), List.Empty.Add(worldId.Serialize()) - ); - - if (backward) - { - state = state.SetState(_avatarAddress, _avatarState.Serialize()); - } - else - { - state = state - .SetState( - _avatarAddress.Derive(LegacyWorldInformationKey), - _avatarState.worldInformation.Serialize()); - } + ) + .SetAvatarState(_avatarAddress, _avatarState); Assert.Throws(() => action.Execute(new ActionContext() { @@ -379,12 +222,11 @@ public void Execute_InvalidStageException(int clearedWorldId, int clearedStageId } [Theory] - [InlineData(GameConfig.MimisbrunnrWorldId, true, 10000001, false)] - [InlineData(GameConfig.MimisbrunnrWorldId, false, 10000001, true)] + [InlineData(GameConfig.MimisbrunnrWorldId, 10000001, false)] + [InlineData(GameConfig.MimisbrunnrWorldId, 10000001, true)] // Unlock CRYSTAL first. - [InlineData(2, false, 51, false)] - [InlineData(2, true, 51, false)] - public void Execute_InvalidWorldException(int worldId, bool backward, int stageId, bool unlockedIdsExist) + [InlineData(2, 51, false)] + public void Execute_InvalidWorldException(int worldId, int stageId, bool unlockedIdsExist) { var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); var avatarState = new AvatarState( @@ -399,29 +241,11 @@ public void Execute_InvalidWorldException(int worldId, bool backward, int stageI new WorldInformation(0, _initialState.GetSheet(), 10000001), }; - IAccount state; - if (backward) - { - state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); - } - else - { - state = _initialState - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState( - _avatarAddress.Derive(LegacyInventoryKey), - avatarState.inventory.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyWorldInformationKey), - avatarState.worldInformation.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyQuestListKey), - avatarState.questList.Serialize()); - } + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); if (unlockedIdsExist) { - state = state.SetState( + state = state.SetLegacyState( _avatarAddress.Derive("world_ids"), List.Empty.Add(worldId.Serialize()) ); @@ -444,10 +268,8 @@ public void Execute_InvalidWorldException(int worldId, bool backward, int stageI })); } - [Theory] - [InlineData(99, true)] - [InlineData(99, false)] - public void Execute_UsageLimitExceedException(int apStoneCount, bool backward) + [Fact] + public void Execute_UsageLimitExceedException() { var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); var avatarState = new AvatarState( @@ -462,30 +284,12 @@ public void Execute_UsageLimitExceedException(int apStoneCount, bool backward) new WorldInformation(0, _initialState.GetSheet(), 25), }; - IAccount state; - if (backward) - { - state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); - } - else - { - state = _initialState - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState( - _avatarAddress.Derive(LegacyInventoryKey), - avatarState.inventory.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyWorldInformationKey), - avatarState.worldInformation.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyQuestListKey), - avatarState.questList.Serialize()); - } + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); var action = new HackAndSlashSweep { runeInfos = new List(), - apStoneCount = apStoneCount, + apStoneCount = 99, avatarAddress = _avatarAddress, worldId = 1, stageId = 2, @@ -500,9 +304,9 @@ public void Execute_UsageLimitExceedException(int apStoneCount, bool backward) } [Theory] - [InlineData(3, 2, true)] - [InlineData(7, 5, false)] - public void Execute_NotEnoughMaterialException(int useApStoneCount, int holdingApStoneCount, bool backward) + [InlineData(3, 2)] + [InlineData(7, 5)] + public void Execute_NotEnoughMaterialException(int useApStoneCount, int holdingApStoneCount) { var gameConfigState = _initialState.GetGameConfigState(); var avatarState = new AvatarState( @@ -523,25 +327,7 @@ public void Execute_NotEnoughMaterialException(int useApStoneCount, int holdingA var apStone = ItemFactory.CreateTradableMaterial(row); avatarState.inventory.AddItem(apStone, holdingApStoneCount); - IAccount state; - if (backward) - { - state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); - } - else - { - state = _initialState - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState( - _avatarAddress.Derive(LegacyInventoryKey), - avatarState.inventory.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyWorldInformationKey), - avatarState.worldInformation.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyQuestListKey), - avatarState.questList.Serialize()); - } + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); var stageSheet = _initialState.GetSheet(); var (expectedLevel, expectedExp) = (0, 0L); @@ -579,10 +365,8 @@ public void Execute_NotEnoughMaterialException(int useApStoneCount, int holdingA } } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Execute_NotEnoughActionPointException(bool backward) + [Fact] + public void Execute_NotEnoughActionPointException() { var gameConfigState = _initialState.GetGameConfigState(); var avatarState = new AvatarState( @@ -599,25 +383,7 @@ public void Execute_NotEnoughActionPointException(bool backward) actionPoint = 0, }; - IAccount state; - if (backward) - { - state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); - } - else - { - state = _initialState - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState( - _avatarAddress.Derive(LegacyInventoryKey), - avatarState.inventory.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyWorldInformationKey), - avatarState.worldInformation.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyQuestListKey), - avatarState.questList.Serialize()); - } + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); var stageSheet = _initialState.GetSheet(); var (expectedLevel, expectedExp) = (0, 0L); @@ -655,10 +421,8 @@ public void Execute_NotEnoughActionPointException(bool backward) } } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Execute_PlayCountIsZeroException(bool backward) + [Fact] + public void Execute_PlayCountIsZeroException() { var gameConfigState = _initialState.GetGameConfigState(); var avatarState = new AvatarState( @@ -675,25 +439,7 @@ public void Execute_PlayCountIsZeroException(bool backward) actionPoint = 0, }; - IAccount state; - if (backward) - { - state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); - } - else - { - state = _initialState - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState( - _avatarAddress.Derive(LegacyInventoryKey), - avatarState.inventory.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyWorldInformationKey), - avatarState.worldInformation.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyQuestListKey), - avatarState.questList.Serialize()); - } + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); var stageSheet = _initialState.GetSheet(); var (expectedLevel, expectedExp) = (0, 0L); @@ -731,10 +477,8 @@ public void Execute_PlayCountIsZeroException(bool backward) } } - [Theory] - [InlineData(1, 24, true)] - [InlineData(1, 24, false)] - public void Execute_NotEnoughCombatPointException(int worldId, int stageId, bool backward) + [Fact] + public void Execute_NotEnoughCombatPointException() { var gameConfigState = _initialState.GetGameConfigState(); var avatarState = new AvatarState( @@ -751,28 +495,11 @@ public void Execute_NotEnoughCombatPointException(int worldId, int stageId, bool level = 1, }; - IAccount state; - if (backward) - { - state = _initialState.SetState(_avatarAddress, avatarState.Serialize()); - } - else - { - state = _initialState - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState( - _avatarAddress.Derive(LegacyInventoryKey), - avatarState.inventory.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyWorldInformationKey), - avatarState.worldInformation.Serialize()) - .SetState( - _avatarAddress.Derive(LegacyQuestListKey), - avatarState.questList.Serialize()); - } + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); var stageSheet = _initialState.GetSheet(); var (expectedLevel, expectedExp) = (0, 0L); + int stageId = 24; if (stageSheet.TryGetValue(stageId, out var stageRow)) { var itemPlayCount = @@ -792,7 +519,7 @@ public void Execute_NotEnoughCombatPointException(int worldId, int stageId, bool avatarAddress = _avatarAddress, actionPoint = avatarState.actionPoint, apStoneCount = 1, - worldId = worldId, + worldId = 1, stageId = stageId, }; @@ -841,8 +568,8 @@ public void ExecuteWithStake(int stakingLevel) .FirstOrDefault(r => r.Level == stakingLevel)?.RequiredGold ?? 0; var context = new ActionContext(); var state = _initialState - .SetState(_avatarAddress, avatarState.Serialize()) - .SetState(stakeStateAddress, stakeState.Serialize()) + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(stakeStateAddress, stakeState.Serialize()) .MintAsset(context, stakeStateAddress, requiredGold * _initialState.GetGoldCurrency()); var stageSheet = _initialState.GetSheet(); if (stageSheet.TryGetValue(stageId, out var stageRow)) @@ -876,7 +603,7 @@ public void ExecuteWithStake(int stakingLevel) Signer = _agentAddress, RandomSeed = 0, }); - var nextAvatar = nextState.GetAvatarStateV2(_avatarAddress); + var nextAvatar = nextState.GetAvatarState(_avatarAddress); Assert.Equal(expectedLevel, nextAvatar.level); Assert.Equal(expectedExp, nextAvatar.exp); } @@ -919,8 +646,8 @@ public void ExecuteDuplicatedException(int slotIndex, int runeId, int slotIndex2 .FirstOrDefault(r => r.Level == stakingLevel)?.RequiredGold ?? 0; var context = new ActionContext(); var state = _initialState - .SetState(_avatarAddress, avatarState.Serialize()) - .SetState(stakeStateAddress, stakeState.Serialize()) + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(stakeStateAddress, stakeState.Serialize()) .MintAsset(context, stakeStateAddress, requiredGold * _initialState.GetGoldCurrency()); var stageSheet = _initialState.GetSheet(); if (stageSheet.TryGetValue(stageId, out var stageRow)) From 72b478c645af08930f2eef0b8451d54575b26c90 Mon Sep 17 00:00:00 2001 From: Yang Chun Ung Date: Thu, 21 Mar 2024 15:13:35 +0900 Subject: [PATCH 08/16] Restore ItemEnhancementTest --- .Lib9c.Tests/Action/ItemEnhancementTest.cs | 35 +++++++--------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/.Lib9c.Tests/Action/ItemEnhancementTest.cs b/.Lib9c.Tests/Action/ItemEnhancementTest.cs index 1ed3009a8c..901db08f20 100644 --- a/.Lib9c.Tests/Action/ItemEnhancementTest.cs +++ b/.Lib9c.Tests/Action/ItemEnhancementTest.cs @@ -5,9 +5,7 @@ namespace Lib9c.Tests.Action using System.Globalization; using System.Linq; using Bencodex.Types; - using Lib9c.Tests.Fixtures.TableCSV; using Lib9c.Tests.Fixtures.TableCSV.Cost; - using Lib9c.Tests.Fixtures.TableCSV.Item; using Lib9c.Tests.Util; using Libplanet.Action.State; using Libplanet.Crypto; @@ -18,8 +16,8 @@ namespace Lib9c.Tests.Action using Nekoyume.Model.Item; using Nekoyume.Model.Mail; using Nekoyume.Model.State; + using Nekoyume.Module; using Xunit; - using static SerializeKeys; public class ItemEnhancementTest { @@ -28,11 +26,11 @@ public class ItemEnhancementTest private readonly Address _avatarAddress; private readonly AvatarState _avatarState; private readonly Currency _currency; - private IAccount _initialState; + private IWorld _initialState; public ItemEnhancementTest() { - _initialState = new Account(MockState.Empty); + _initialState = new World(new MockWorldState()); Dictionary sheets; (_initialState, sheets) = InitializeUtil.InitializeTableSheets( _initialState, @@ -47,7 +45,7 @@ public ItemEnhancementTest() foreach (var (key, value) in sheets) { _initialState = - _initialState.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + _initialState.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } var privateKey = new PrivateKey(); @@ -79,10 +77,10 @@ public ItemEnhancementTest() var context = new ActionContext(); _initialState = _initialState - .SetState(_agentAddress, agentState.Serialize()) - .SetState(_avatarAddress, _avatarState.Serialize()) - .SetState(slotAddress, new CombinationSlotState(slotAddress, 0).Serialize()) - .SetState(GoldCurrencyState.Address, gold.Serialize()) + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, _avatarState) + .SetLegacyState(slotAddress, new CombinationSlotState(slotAddress, 0).Serialize()) + .SetLegacyState(GoldCurrencyState.Address, gold.Serialize()) .MintAsset(context, GoldCurrencyState.Address, gold.Currency * 100_000_000_000) .TransferAsset( context, @@ -282,20 +280,7 @@ public void Execute( Assert.Equal(startLevel, equipment.level); - _initialState = _initialState - .SetState( - _avatarAddress.Derive(LegacyInventoryKey), - _avatarState.inventory.Serialize() - ) - .SetState( - _avatarAddress.Derive(LegacyWorldInformationKey), - _avatarState.worldInformation.Serialize() - ) - .SetState( - _avatarAddress.Derive(LegacyQuestListKey), - _avatarState.questList.Serialize() - ) - .SetState(_avatarAddress, _avatarState.SerializeV2()); + _initialState = _initialState.SetAvatarState(_avatarAddress, _avatarState); var action = new ItemEnhancement { @@ -340,7 +325,7 @@ public void Execute( ); Assert.Equal(30, nextAvatarState.mailBox.Count); - var stateDict = (Dictionary)nextState.GetState(slotAddress); + var stateDict = (Dictionary)nextState.GetLegacyState(slotAddress); var slot = new CombinationSlotState(stateDict); var slotResult = (ItemEnhancement13.ResultModel)slot.Result; if (startLevel != level) From 5c03b5ccf3cca055f88026a230b180d556b91e83 Mon Sep 17 00:00:00 2001 From: Yang Chun Ung Date: Thu, 21 Mar 2024 15:13:44 +0900 Subject: [PATCH 09/16] Restore RaidTest --- .Lib9c.Tests/Action/RaidTest.cs | 92 +++++++++++++++------------------ 1 file changed, 41 insertions(+), 51 deletions(-) diff --git a/.Lib9c.Tests/Action/RaidTest.cs b/.Lib9c.Tests/Action/RaidTest.cs index e13531a033..3a383361a0 100644 --- a/.Lib9c.Tests/Action/RaidTest.cs +++ b/.Lib9c.Tests/Action/RaidTest.cs @@ -14,7 +14,9 @@ namespace Lib9c.Tests.Action using Nekoyume.Helper; using Nekoyume.Model.Arena; using Nekoyume.Model.Rune; + using Nekoyume.Model.Stat; using Nekoyume.Model.State; + using Nekoyume.Module; using Nekoyume.TableData; using Xunit; using static SerializeKeys; @@ -56,8 +58,6 @@ public RaidTest() [InlineData(null, true, true, false, true, 3, 100L, false, false, 0, true, false, false, 5, true, 0, 10002, 1, 30001)] // AvatarState null. [InlineData(typeof(FailedLoadStateException), false, false, false, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] - // Stage not cleared. - [InlineData(typeof(NotEnoughClearedStageLevelException), true, false, false, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] // Insufficient CRYSTAL. [InlineData(typeof(InsufficientBalanceException), true, true, false, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] // Insufficient NCG. @@ -136,13 +136,13 @@ int runeId2 var fee = _tableSheets.WorldBossListSheet[raidId].EntranceFee; var context = new ActionContext(); - IAccount state = new Account(MockState.Empty) - .SetState(goldCurrencyState.address, goldCurrencyState.Serialize()) - .SetState(_agentAddress, new AgentState(_agentAddress).Serialize()); + IWorld state = new World(new MockWorldState()) + .SetLegacyState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetAgentState(_agentAddress, new AgentState(_agentAddress)); foreach (var (key, value) in _sheets) { - state = state.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); @@ -193,7 +193,7 @@ int runeId2 raiderState.AvatarAddress = _avatarAddress; raiderState.UpdatedBlockIndex = blockIndex; - state = state.SetState(raiderAddress, raiderState.Serialize()); + state = state.SetLegacyState(raiderAddress, raiderState.Serialize()); var raiderList = new List().Add(raiderAddress.Serialize()); @@ -202,7 +202,7 @@ int runeId2 raiderList = raiderList.Add(new PrivateKey().Address.Serialize()); } - state = state.SetState(raiderListAddress, raiderList); + state = state.SetLegacyState(raiderListAddress, raiderList); } if (rewardRecordExist) @@ -211,7 +211,7 @@ int runeId2 { [0] = false, }; - state = state.SetState(worldBossKillRewardRecordAddress, rewardRecord.Serialize()); + state = state.SetLegacyState(worldBossKillRewardRecordAddress, rewardRecord.Serialize()); } if (ncgExist) @@ -221,11 +221,8 @@ int runeId2 } state = state - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) - .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) - .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) - .SetState(gameConfigState.address, gameConfigState.Serialize()); + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); } if (kill) @@ -236,7 +233,7 @@ int runeId2 CurrentHp = 0, Level = level, }; - state = state.SetState(bossAddress, bossState.Serialize()); + state = state.SetLegacyState(bossAddress, bossState.Serialize()); } if (exc is null) @@ -262,7 +259,8 @@ int runeId2 action.FoodIds, null, raidSimulatorSheets, - _tableSheets.CostumeStatSheet); + _tableSheets.CostumeStatSheet, + new List()); simulator.Simulate(); var score = simulator.DamageDealt; @@ -276,7 +274,7 @@ Dictionary rewardMap if (rewardRecordExist) { var bossRow = raidSimulatorSheets.WorldBossCharacterSheet[bossListRow.BossId]; - Assert.True(state.TryGetState(bossAddress, out List prevRawBoss)); + Assert.True(state.TryGetLegacyState(bossAddress, out List prevRawBoss)); var prevBossState = new WorldBossState(prevRawBoss); int rank = WorldBossHelper.CalculateRank(bossRow, raiderStateExist ? 1_000 : 0); var rewards = RuneHelper.CalculateReward( @@ -323,9 +321,9 @@ Dictionary rewardMap Assert.Equal(fee * crystal, nextState.GetBalance(bossAddress, crystal)); } - Assert.True(nextState.TryGetState(raiderAddress, out List rawRaider)); + Assert.True(nextState.TryGetLegacyState(raiderAddress, out List rawRaider)); var raiderState = new RaiderState(rawRaider); - int expectedTotalScore = raiderStateExist ? 1_000 + score : score; + long expectedTotalScore = raiderStateExist ? 1_000 + score : score; int expectedRemainChallenge = payNcg ? 0 : 2; int expectedTotalChallenge = raiderStateExist ? 2 : 1; @@ -337,7 +335,7 @@ Dictionary rewardMap Assert.Equal(GameConfig.DefaultAvatarArmorId, raiderState.IconId); Assert.True(raiderState.Cp > 0); - Assert.True(nextState.TryGetState(bossAddress, out List rawBoss)); + Assert.True(nextState.TryGetLegacyState(bossAddress, out List rawBoss)); var bossState = new WorldBossState(rawBoss); int expectedLevel = level; if (kill & levelUp) @@ -358,7 +356,7 @@ Dictionary rewardMap Assert.Equal(purchaseCount + 1, nextState.GetRaiderState(raiderAddress).PurchaseCount); } - Assert.True(nextState.TryGetState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); + Assert.True(nextState.TryGetLegacyState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); var rewardRecord = new WorldBossKillRewardRecord(rawRewardInfo); Assert.Contains(expectedLevel, rewardRecord.Keys); if (rewardRecordExist) @@ -377,7 +375,7 @@ Dictionary rewardMap } } - Assert.True(nextState.TryGetState(raiderListAddress, out List rawRaiderList)); + Assert.True(nextState.TryGetLegacyState(raiderListAddress, out List rawRaiderList)); List
raiderList = rawRaiderList.ToList(StateExtensions.ToAddress); Assert.Contains(raiderAddress, raiderList); @@ -434,13 +432,13 @@ public void Execute_With_Reward() Address bossAddress = Addresses.GetWorldBossAddress(raidId); Address worldBossKillRewardRecordAddress = Addresses.GetWorldBossKillRewardRecordAddress(_avatarAddress, raidId); - IAccount state = new Account(MockState.Empty) - .SetState(goldCurrencyState.address, goldCurrencyState.Serialize()) - .SetState(_agentAddress, new AgentState(_agentAddress).Serialize()); + IWorld state = new World(new MockWorldState()) + .SetLegacyState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetAgentState(_agentAddress, new AgentState(_agentAddress)); foreach (var (key, value) in _sheets) { - state = state.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); @@ -469,20 +467,17 @@ public void Execute_With_Reward() raiderState.IconId = 0; raiderState.AvatarName = "hash"; raiderState.AvatarAddress = _avatarAddress; - state = state.SetState(raiderAddress, raiderState.Serialize()); + state = state.SetLegacyState(raiderAddress, raiderState.Serialize()); var rewardRecord = new WorldBossKillRewardRecord { [1] = false, }; - state = state.SetState(worldBossKillRewardRecordAddress, rewardRecord.Serialize()); + state = state.SetLegacyState(worldBossKillRewardRecordAddress, rewardRecord.Serialize()); state = state - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) - .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) - .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) - .SetState(gameConfigState.address, gameConfigState.Serialize()); + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); var bossState = new WorldBossState(worldBossRow, _tableSheets.WorldBossGlobalHpSheet[2]) @@ -490,7 +485,7 @@ public void Execute_With_Reward() CurrentHp = 0, Level = 2, }; - state = state.SetState(bossAddress, bossState.Serialize()); + state = state.SetLegacyState(bossAddress, bossState.Serialize()); var randomSeed = 0; var random = new TestRandom(randomSeed); @@ -501,7 +496,8 @@ public void Execute_With_Reward() action.FoodIds, null, _tableSheets.GetRaidSimulatorSheets(), - _tableSheets.CostumeStatSheet); + _tableSheets.CostumeStatSheet, + new List()); simulator.Simulate(); Dictionary rewardMap @@ -528,7 +524,7 @@ Dictionary rewardMap Signer = _agentAddress, }); - Assert.True(nextState.TryGetState(raiderAddress, out List rawRaider)); + Assert.True(nextState.TryGetLegacyState(raiderAddress, out List rawRaider)); var nextRaiderState = new RaiderState(rawRaider); Assert.Equal(simulator.DamageDealt, nextRaiderState.HighScore); @@ -560,10 +556,10 @@ Dictionary rewardMap Assert.Equal(GameConfig.DefaultAvatarArmorId, nextRaiderState.IconId); Assert.True(nextRaiderState.Cp > 0); Assert.Equal(3, nextRaiderState.LatestBossLevel); - Assert.True(nextState.TryGetState(bossAddress, out List rawBoss)); + Assert.True(nextState.TryGetLegacyState(bossAddress, out List rawBoss)); var nextBossState = new WorldBossState(rawBoss); Assert.Equal(3, nextBossState.Level); - Assert.True(nextState.TryGetState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); + Assert.True(nextState.TryGetLegacyState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); var nextRewardInfo = new WorldBossKillRewardRecord(rawRewardInfo); Assert.True(nextRewardInfo[1]); } @@ -587,13 +583,13 @@ public void Execute_With_Free_Crystal_Fee() "1,900002,0,100,0,1,1,40"; var goldCurrencyState = new GoldCurrencyState(_goldCurrency); - IAccount state = new Account(MockState.Empty) - .SetState(goldCurrencyState.address, goldCurrencyState.Serialize()) - .SetState(_agentAddress, new AgentState(_agentAddress).Serialize()); + IWorld state = new World(new MockWorldState()) + .SetLegacyState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetAgentState(_agentAddress, new AgentState(_agentAddress)); foreach (var (key, value) in _sheets) { - state = state.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); @@ -612,11 +608,8 @@ public void Execute_With_Free_Crystal_Fee() } state = state - .SetState(_avatarAddress, avatarState.SerializeV2()) - .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) - .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) - .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) - .SetState(gameConfigState.address, gameConfigState.Serialize()); + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); var blockIndex = gameConfigState.WorldBossRequiredInterval; var randomSeed = 0; @@ -627,10 +620,7 @@ public void Execute_With_Free_Crystal_Fee() RandomSeed = randomSeed, Signer = _agentAddress, }; - - IAccount nextState; - var exception = Record.Exception(() => nextState = action.Execute(ctx)); - Assert.Null(exception); + action.Execute(ctx); } } } From c5a21416f839bfacd6616017d4484d2936192c00 Mon Sep 17 00:00:00 2001 From: Yang Chun Ung Date: Thu, 21 Mar 2024 15:13:49 +0900 Subject: [PATCH 10/16] Restore RapidCombinationTest --- .Lib9c.Tests/Action/RapidCombinationTest.cs | 113 ++++---------------- 1 file changed, 22 insertions(+), 91 deletions(-) diff --git a/.Lib9c.Tests/Action/RapidCombinationTest.cs b/.Lib9c.Tests/Action/RapidCombinationTest.cs index a89d5ba078..fa6976dbcf 100644 --- a/.Lib9c.Tests/Action/RapidCombinationTest.cs +++ b/.Lib9c.Tests/Action/RapidCombinationTest.cs @@ -19,13 +19,14 @@ namespace Lib9c.Tests.Action using Nekoyume.Model.Item; using Nekoyume.Model.Mail; using Nekoyume.Model.State; + using Nekoyume.Module; using Nekoyume.TableData; using Xunit; using static Lib9c.SerializeKeys; public class RapidCombinationTest { - private readonly IAccount _initialState; + private readonly IWorld _initialState; private readonly TableSheets _tableSheets; @@ -34,7 +35,7 @@ public class RapidCombinationTest public RapidCombinationTest() { - _initialState = new Account(MockState.Empty); + _initialState = new World(new MockWorldState()); Dictionary sheets; (_initialState, sheets) = InitializeUtil.InitializeTableSheets( _initialState, @@ -57,7 +58,7 @@ public RapidCombinationTest() foreach (var (key, value) in sheets) { _initialState = - _initialState.SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + _initialState.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } _agentAddress = new PrivateKey().Address; @@ -76,15 +77,13 @@ public RapidCombinationTest() agentState.avatarAddresses[0] = _avatarAddress; _initialState = _initialState - .SetState(Addresses.GameConfig, new GameConfigState(sheets[nameof(GameConfigSheet)]).Serialize()) - .SetState(_agentAddress, agentState.Serialize()) - .SetState(_avatarAddress, avatarState.Serialize()); + .SetLegacyState(Addresses.GameConfig, new GameConfigState(sheets[nameof(GameConfigSheet)]).Serialize()) + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, avatarState); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Execute(bool backward) + [Fact] + public void Execute() { const int slotStateUnlockStage = 1; @@ -132,20 +131,9 @@ public void Execute(bool backward) var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); slotState.Update(result, 0, requiredBlockIndex); - var tempState = _initialState.SetState(slotAddress, slotState.Serialize()); - - if (backward) - { - tempState = tempState.SetState(_avatarAddress, avatarState.Serialize()); - } - else - { - tempState = tempState - .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) - .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) - .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) - .SetState(_avatarAddress, avatarState.SerializeV2()); - } + var tempState = _initialState + .SetLegacyState(slotAddress, slotState.Serialize()) + .SetAvatarState(_avatarAddress, avatarState); var action = new RapidCombination { @@ -160,7 +148,7 @@ public void Execute(bool backward) BlockIndex = 51, }); - var nextAvatarState = nextState.GetAvatarStateV2(_avatarAddress); + var nextAvatarState = nextState.GetAvatarState(_avatarAddress); var item = nextAvatarState.inventory.Equipments.First(); Assert.Empty(nextAvatarState.inventory.Materials.Select(r => r.ItemSubType == ItemSubType.Hourglass)); @@ -179,7 +167,7 @@ public void Execute_Throw_CombinationSlotResultNullException() slotState.Update(null, 0, 0); var tempState = _initialState - .SetState(slotAddress, slotState.Serialize()); + .SetLegacyState(slotAddress, slotState.Serialize()); var action = new RapidCombination { @@ -195,60 +183,6 @@ public void Execute_Throw_CombinationSlotResultNullException() })); } - [Theory] - [InlineData(0, 1)] - [InlineData(1, 2)] - public void Execute_Throw_NotEnoughClearedStageLevelException(int avatarClearedStage, int slotStateUnlockStage) - { - var avatarState = _initialState.GetAvatarState(_avatarAddress); - avatarState.worldInformation = new WorldInformation( - 0, - _initialState.GetSheet(), - avatarClearedStage); - - var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; - Assert.NotNull(firstEquipmentRow); - - var equipment = (Equipment)ItemFactory.CreateItemUsable( - firstEquipmentRow, - Guid.NewGuid(), - 100); - - var result = new CombinationConsumable5.ResultModel - { - actionPoint = 0, - gold = 0, - materials = new Dictionary(), - itemUsable = equipment, - recipeId = 0, - itemType = ItemType.Equipment, - }; - - var slotAddress = _avatarAddress.Derive(string.Format( - CultureInfo.InvariantCulture, - CombinationSlotState.DeriveFormat, - 0)); - var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); - slotState.Update(result, 0, 0); - - var tempState = _initialState - .SetState(_avatarAddress, avatarState.Serialize()) - .SetState(slotAddress, slotState.Serialize()); - - var action = new RapidCombination - { - avatarAddress = _avatarAddress, - slotIndex = 0, - }; - - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = tempState, - Signer = _agentAddress, - BlockIndex = 1, - })); - } - [Theory] [InlineData(0, 0)] [InlineData(10, 100)] @@ -288,8 +222,8 @@ public void Execute_Throw_RequiredBlockIndexException(int itemRequiredBlockIndex slotState.Update(result, 0, 0); var tempState = _initialState - .SetState(_avatarAddress, avatarState.Serialize()) - .SetState(slotAddress, slotState.Serialize()); + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(slotAddress, slotState.Serialize()); var action = new RapidCombination { @@ -364,8 +298,8 @@ public void Execute_Throw_NotEnoughMaterialException(int materialCount, int trad slotState.Update(result, 0, 0); var tempState = _initialState - .SetState(_avatarAddress, avatarState.Serialize()) - .SetState(slotAddress, slotState.Serialize()); + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(slotAddress, slotState.Serialize()); var action = new RapidCombination { @@ -493,8 +427,8 @@ public void Execute_Throw_RequiredAppraiseBlockException() slotState.Update(result, 0, 0); var tempState = _initialState - .SetState(_avatarAddress, avatarState.Serialize()) - .SetState(slotAddress, slotState.Serialize()); + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(slotAddress, slotState.Serialize()); var action = new RapidCombination { @@ -661,11 +595,8 @@ public void Execute_NotThrow_InvalidOperationException_When_TargetSlotCreatedBy( var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); slotState.Update(resultModel, 0, requiredBlockIndex); - var tempState = _initialState.SetState(slotAddress, slotState.Serialize()) - .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) - .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) - .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) - .SetState(_avatarAddress, avatarState.SerializeV2()); + var tempState = _initialState.SetLegacyState(slotAddress, slotState.Serialize()) + .SetAvatarState(_avatarAddress, avatarState); var action = new RapidCombination { From c45e56e84a9e2f8255fcf04725f4012851e0cd3b Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Thu, 21 Mar 2024 16:54:52 +0900 Subject: [PATCH 11/16] Removed DeletedAvatarState --- Lib9c/Model/State/DeletedAvatarState.cs | 33 ------------------------- 1 file changed, 33 deletions(-) delete mode 100644 Lib9c/Model/State/DeletedAvatarState.cs diff --git a/Lib9c/Model/State/DeletedAvatarState.cs b/Lib9c/Model/State/DeletedAvatarState.cs deleted file mode 100644 index dc5328e46b..0000000000 --- a/Lib9c/Model/State/DeletedAvatarState.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Bencodex.Types; - -namespace Nekoyume.Model.State -{ - [Serializable] - public class DeletedAvatarState : AvatarState - { - public long deletedAt; - - public DeletedAvatarState(AvatarState avatarState, long blockIndex) - : base(avatarState) - { - deletedAt = blockIndex; - } - - public DeletedAvatarState(Dictionary serialized) - : base(serialized) - { - deletedAt = serialized["deletedAt"].ToLong(); - } - - public override IValue Serialize() => -#pragma warning disable LAA1002 - new Dictionary(new Dictionary - { - [(Text) "deletedAt"] = deletedAt.Serialize(), - }.Union((Dictionary) base.Serialize())); -#pragma warning restore LAA1002 - } -} From dd881253b33be9ac4d861a3343f9a50cb9e482ac Mon Sep 17 00:00:00 2001 From: Suho Lee Date: Tue, 12 Mar 2024 14:03:02 +0900 Subject: [PATCH 12/16] bump: libplanet 4.1.0 --- .Libplanet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.Libplanet b/.Libplanet index 1016fbce88..7d95f209e8 160000 --- a/.Libplanet +++ b/.Libplanet @@ -1 +1 @@ -Subproject commit 1016fbce882309452a45eda1a19c9a8b213801a5 +Subproject commit 7d95f209e8358f2f90568cfc4de9ff819c8eea40 From f29d96e4adc717d755ac89639f311061f58966e3 Mon Sep 17 00:00:00 2001 From: Suho Lee Date: Tue, 12 Mar 2024 14:03:18 +0900 Subject: [PATCH 13/16] chore: disable MsgPack003 --- Lib9c.MessagePack/Action/NCActionEvaluation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib9c.MessagePack/Action/NCActionEvaluation.cs b/Lib9c.MessagePack/Action/NCActionEvaluation.cs index 35f2b94a68..f977a3489d 100644 --- a/Lib9c.MessagePack/Action/NCActionEvaluation.cs +++ b/Lib9c.MessagePack/Action/NCActionEvaluation.cs @@ -49,7 +49,9 @@ public struct NCActionEvaluation [Key(8)] [MessagePackFormatter(typeof(TxIdFormatter))] + #pragma warning disable MsgPack003 public TxId? TxId { get; set; } + #pragma warning restore MsgPack003 [SerializationConstructor] public NCActionEvaluation( From 5354eb0cf006ef4f1cad58d6c2efe519bcc2e88d Mon Sep 17 00:00:00 2001 From: Chanhyuck Ko Date: Wed, 20 Mar 2024 17:29:07 +0900 Subject: [PATCH 14/16] bump: libplanet --- .Libplanet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.Libplanet b/.Libplanet index 7d95f209e8..96970ec83a 160000 --- a/.Libplanet +++ b/.Libplanet @@ -1 +1 @@ -Subproject commit 7d95f209e8358f2f90568cfc4de9ff819c8eea40 +Subproject commit 96970ec83a91f1590560b82834f8cb64518754a8 From 083ee2e2569a07696279c0e3128ee4138fccab9d Mon Sep 17 00:00:00 2001 From: Ko Chanhyuck Date: Fri, 15 Mar 2024 14:32:16 +0900 Subject: [PATCH 15/16] feat: add dpos related actions --- Lib9c.DPoS/Action/DPoSModule.cs | 22 ++ Lib9c.DPoS/Action/PoSAction.cs | 65 ++++ Lib9c.DPoS/Action/Sys/CancelUndelegation.cs | 81 +++++ Lib9c.DPoS/Action/Sys/Delegate.cs | 77 +++++ Lib9c.DPoS/Action/Sys/PromoteValidator.cs | 86 +++++ Lib9c.DPoS/Action/Sys/Redelegate.cs | 89 +++++ Lib9c.DPoS/Action/Sys/Undelegate.cs | 80 +++++ Lib9c.DPoS/Action/Sys/WithdrawDelegator.cs | 82 +++++ Lib9c.DPoS/Action/Sys/WithdrawValidator.cs | 61 ++++ Lib9c.DPoS/Control/AllocateReward.cs | 184 +++++++++++ Lib9c.DPoS/Control/Bond.cs | 147 +++++++++ Lib9c.DPoS/Control/DelegateCtrl.cs | 155 +++++++++ Lib9c.DPoS/Control/RedelegateCtrl.cs | 225 +++++++++++++ Lib9c.DPoS/Control/UnbondingSetCtrl.cs | 129 ++++++++ Lib9c.DPoS/Control/UndelegateCtrl.cs | 311 ++++++++++++++++++ Lib9c.DPoS/Control/ValidatorCtrl.cs | 263 +++++++++++++++ .../Control/ValidatorDelegationSetCtrl.cs | 59 ++++ Lib9c.DPoS/Control/ValidatorPowerIndexCtrl.cs | 69 ++++ Lib9c.DPoS/Control/ValidatorRewardsCtrl.cs | 85 +++++ Lib9c.DPoS/Control/ValidatorSetCtrl.cs | 127 +++++++ .../Exception/DuplicatedValidatorException.cs | 12 + ...InsufficientFungibleAssetValueException.cs | 13 + .../Exception/InvalidCurrencyException.cs | 12 + .../Exception/InvalidExchangeRateException.cs | 12 + .../Exception/JailedValidatorException.cs | 12 + .../MaximumRedelegationEntriesException.cs | 12 + .../MaximumUndelegationEntriesException.cs | 12 + .../Exception/NullDelegationException.cs | 12 + .../Exception/NullNativeTokensException.cs | 12 + .../Exception/NullRedelegationException.cs | 12 + .../Exception/NullUndelegationException.cs | 12 + .../Exception/NullValidatorException.cs | 12 + .../PostmatureUndelegationEntryException.cs | 14 + .../PublicKeyAddressMatchingException.cs | 13 + Lib9c.DPoS/Lib9c.DPoS.csproj | 15 + Lib9c.DPoS/Misc/Asset.cs | 28 ++ Lib9c.DPoS/Misc/ReservedAddress.cs | 40 +++ Lib9c.DPoS/Model/BondingStatus.cs | 21 ++ Lib9c.DPoS/Model/Delegation.cs | 88 +++++ Lib9c.DPoS/Model/Redelegation.cs | 137 ++++++++ Lib9c.DPoS/Model/RedelegationEntry.cs | 147 +++++++++ Lib9c.DPoS/Model/UnbondingSet.cs | 59 ++++ Lib9c.DPoS/Model/Undelegation.cs | 125 +++++++ Lib9c.DPoS/Model/UndelegationEntry.cs | 107 ++++++ Lib9c.DPoS/Model/Validator.cs | 134 ++++++++ Lib9c.DPoS/Model/ValidatorDelegationSet.cs | 63 ++++ Lib9c.DPoS/Model/ValidatorPower.cs | 111 +++++++ Lib9c.DPoS/Model/ValidatorPowerIndex.cs | 36 ++ Lib9c.DPoS/Model/ValidatorRewards.cs | 87 +++++ Lib9c.DPoS/Model/ValidatorSet.cs | 58 ++++ Lib9c.DPoS/Util/AddressHelper.cs | 25 ++ Lib9c.DPoS/Util/MarshalHelper.cs | 82 +++++ Lib9c.sln | 6 + 53 files changed, 3938 insertions(+) create mode 100644 Lib9c.DPoS/Action/DPoSModule.cs create mode 100644 Lib9c.DPoS/Action/PoSAction.cs create mode 100644 Lib9c.DPoS/Action/Sys/CancelUndelegation.cs create mode 100644 Lib9c.DPoS/Action/Sys/Delegate.cs create mode 100644 Lib9c.DPoS/Action/Sys/PromoteValidator.cs create mode 100644 Lib9c.DPoS/Action/Sys/Redelegate.cs create mode 100644 Lib9c.DPoS/Action/Sys/Undelegate.cs create mode 100644 Lib9c.DPoS/Action/Sys/WithdrawDelegator.cs create mode 100644 Lib9c.DPoS/Action/Sys/WithdrawValidator.cs create mode 100644 Lib9c.DPoS/Control/AllocateReward.cs create mode 100644 Lib9c.DPoS/Control/Bond.cs create mode 100644 Lib9c.DPoS/Control/DelegateCtrl.cs create mode 100644 Lib9c.DPoS/Control/RedelegateCtrl.cs create mode 100644 Lib9c.DPoS/Control/UnbondingSetCtrl.cs create mode 100644 Lib9c.DPoS/Control/UndelegateCtrl.cs create mode 100644 Lib9c.DPoS/Control/ValidatorCtrl.cs create mode 100644 Lib9c.DPoS/Control/ValidatorDelegationSetCtrl.cs create mode 100644 Lib9c.DPoS/Control/ValidatorPowerIndexCtrl.cs create mode 100644 Lib9c.DPoS/Control/ValidatorRewardsCtrl.cs create mode 100644 Lib9c.DPoS/Control/ValidatorSetCtrl.cs create mode 100644 Lib9c.DPoS/Exception/DuplicatedValidatorException.cs create mode 100644 Lib9c.DPoS/Exception/InsufficientFungibleAssetValueException.cs create mode 100644 Lib9c.DPoS/Exception/InvalidCurrencyException.cs create mode 100644 Lib9c.DPoS/Exception/InvalidExchangeRateException.cs create mode 100644 Lib9c.DPoS/Exception/JailedValidatorException.cs create mode 100644 Lib9c.DPoS/Exception/MaximumRedelegationEntriesException.cs create mode 100644 Lib9c.DPoS/Exception/MaximumUndelegationEntriesException.cs create mode 100644 Lib9c.DPoS/Exception/NullDelegationException.cs create mode 100644 Lib9c.DPoS/Exception/NullNativeTokensException.cs create mode 100644 Lib9c.DPoS/Exception/NullRedelegationException.cs create mode 100644 Lib9c.DPoS/Exception/NullUndelegationException.cs create mode 100644 Lib9c.DPoS/Exception/NullValidatorException.cs create mode 100644 Lib9c.DPoS/Exception/PostmatureUndelegationEntryException.cs create mode 100644 Lib9c.DPoS/Exception/PublicKeyAddressMatchingException.cs create mode 100644 Lib9c.DPoS/Lib9c.DPoS.csproj create mode 100644 Lib9c.DPoS/Misc/Asset.cs create mode 100644 Lib9c.DPoS/Misc/ReservedAddress.cs create mode 100644 Lib9c.DPoS/Model/BondingStatus.cs create mode 100644 Lib9c.DPoS/Model/Delegation.cs create mode 100644 Lib9c.DPoS/Model/Redelegation.cs create mode 100644 Lib9c.DPoS/Model/RedelegationEntry.cs create mode 100644 Lib9c.DPoS/Model/UnbondingSet.cs create mode 100644 Lib9c.DPoS/Model/Undelegation.cs create mode 100644 Lib9c.DPoS/Model/UndelegationEntry.cs create mode 100644 Lib9c.DPoS/Model/Validator.cs create mode 100644 Lib9c.DPoS/Model/ValidatorDelegationSet.cs create mode 100644 Lib9c.DPoS/Model/ValidatorPower.cs create mode 100644 Lib9c.DPoS/Model/ValidatorPowerIndex.cs create mode 100644 Lib9c.DPoS/Model/ValidatorRewards.cs create mode 100644 Lib9c.DPoS/Model/ValidatorSet.cs create mode 100644 Lib9c.DPoS/Util/AddressHelper.cs create mode 100644 Lib9c.DPoS/Util/MarshalHelper.cs diff --git a/Lib9c.DPoS/Action/DPoSModule.cs b/Lib9c.DPoS/Action/DPoSModule.cs new file mode 100644 index 0000000000..c6f7e5e9ea --- /dev/null +++ b/Lib9c.DPoS/Action/DPoSModule.cs @@ -0,0 +1,22 @@ +using Bencodex.Types; +using Lib9c.DPoS.Misc; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Action +{ + public static class DPoSModule + { + public static IValue? GetDPoSState(this IWorld world, Address address) + { + return world.GetAccount(ReservedAddress.DPoSAccountAddress).GetState(address); + } + + public static IWorld SetDPoSState(this IWorld world, Address address, IValue value) + { + var account = world.GetAccount(ReservedAddress.DPoSAccountAddress); + account = account.SetState(address, value); + return world.SetAccount(ReservedAddress.DPoSAccountAddress, account); + } + } +} diff --git a/Lib9c.DPoS/Action/PoSAction.cs b/Lib9c.DPoS/Action/PoSAction.cs new file mode 100644 index 0000000000..67980d39f1 --- /dev/null +++ b/Lib9c.DPoS/Action/PoSAction.cs @@ -0,0 +1,65 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Action +{ + /// + /// A block action for DPoS that updates . + /// + public sealed class PoSAction : IAction + { + /// + /// Creates a new instance of . + /// + public PoSAction() + { + } + + /// + public IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public IWorld Execute(IActionContext context) + { + IActionContext ctx = context; + var states = ctx.PreviousState; + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + states = ValidatorSetCtrl.Update(states, ctx); + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + states = AllocateReward.Execute( + states, + ctx, + nativeTokens, + ctx.LastCommit?.Votes, + ctx.Miner); + + // Endblock, Update ValidatorSet + var bondedSet = ValidatorSetCtrl.FetchBondedValidatorSet(states).Item2.Set; + foreach (var validator in bondedSet) + { + states = states.SetValidator( + new Libplanet.Types.Consensus.Validator( + validator.OperatorPublicKey, + validator.ConsensusToken.RawValue)); + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Action/Sys/CancelUndelegation.cs b/Lib9c.DPoS/Action/Sys/CancelUndelegation.cs new file mode 100644 index 0000000000..8ad2470ba5 --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/CancelUndelegation.cs @@ -0,0 +1,81 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that cancel specified + /// of tokens to a given . + /// + public sealed class CancelUndelegation : IAction + { + /// + /// Creates a new instance of action. + /// + /// The of the validator + /// to delegate tokens. + /// The amount of the asset to be delegated. + public CancelUndelegation(Address validator, FungibleAssetValue amount) + { + Validator = validator; + Amount = amount; + } + + internal CancelUndelegation() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator + /// to cancel the and . + /// + public Address Validator { get; set; } + + /// + /// The amount of the asset to be delegated. + /// + public FungibleAssetValue Amount { get; set; } + + /// + public IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("validator", Validator.Serialize()) + .Add("amount", Amount.Serialize()); + + /// + public void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToAddress(); + Amount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public IWorld Execute(IActionContext context) + { + IActionContext ctx = context; + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + states = UndelegateCtrl.Cancel( + states, + ctx, + Undelegation.DeriveAddress(ctx.Signer, Validator), + Amount, + nativeTokens); + + return states; + } + } +} diff --git a/Lib9c.DPoS/Action/Sys/Delegate.cs b/Lib9c.DPoS/Action/Sys/Delegate.cs new file mode 100644 index 0000000000..feaa0d0790 --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/Delegate.cs @@ -0,0 +1,77 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that specified + /// of tokens to a given . + /// + public sealed class Delegate : IAction + { + /// + /// Creates a new instance of action. + /// + /// The of the validator + /// to delegate tokens. + /// The amount of the asset to be delegated. + public Delegate(Address validator, FungibleAssetValue amount) + { + Validator = validator; + Amount = amount; + } + + internal Delegate() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator to . + /// + public Address Validator { get; set; } + + public FungibleAssetValue Amount { get; set; } + + /// + public IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("validator", Validator.Serialize()) + .Add("amount", Amount.Serialize()); + + /// + public void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToAddress(); + Amount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public IWorld Execute(IActionContext context) + { + IActionContext ctx = context; + var states = ctx.PreviousState; + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + states = DelegateCtrl.Execute( + states, + ctx, + ctx.Signer, + Validator, + Amount, + nativeTokens); + + return states; + } + } +} diff --git a/Lib9c.DPoS/Action/Sys/PromoteValidator.cs b/Lib9c.DPoS/Action/Sys/PromoteValidator.cs new file mode 100644 index 0000000000..ab1b26061e --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/PromoteValidator.cs @@ -0,0 +1,86 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that promotes non-validator node to a validator. + /// + public sealed class PromoteValidator : IAction + { + /// + /// Create a new instance of action. + /// + /// The of the target + /// to promote validator. + /// The amount of the asset to be initialize delegation. + public PromoteValidator(PublicKey validator, FungibleAssetValue amount) + { + Validator = validator; + Amount = amount; + } + + internal PromoteValidator() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + // FIXME: do not fill ambiguous validator field. + // Suggestion: https://gist.github.com/riemannulus/7405e0d361364c6afa0ab433905ae81c + Validator = new PrivateKey().PublicKey; + } + + /// + /// The of the target promoting to a validator. + /// + public PublicKey Validator { get; set; } + + /// + /// The amount of the asset to be initially delegated. + /// + public FungibleAssetValue Amount { get; set; } + + /// + public IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("validator", Validator.Serialize()) + .Add("amount", Amount.Serialize()); + + /// + public void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToPublicKey(); + Amount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public IWorld Execute(IActionContext context) + { + IActionContext ctx = context; + if (!ctx.Signer.Equals(Validator.Address)) + { + throw new PublicKeyAddressMatchingException(ctx.Signer, Validator); + } + + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + states = ValidatorCtrl.Create( + states, + ctx, + ctx.Signer, + Validator, + Amount, + nativeTokens); + + return states; + } + } +} diff --git a/Lib9c.DPoS/Action/Sys/Redelegate.cs b/Lib9c.DPoS/Action/Sys/Redelegate.cs new file mode 100644 index 0000000000..32e4685238 --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/Redelegate.cs @@ -0,0 +1,89 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that specified + /// of shared tokens to from . + /// + public sealed class Redelegate : IAction + { + /// + /// Creates a new instance of action. + /// + /// The of the validator that + /// delegated previously. + /// The of the validator + /// to be newly delegated. + /// The amount of the shared asset to be re-delegated. + public Redelegate(Address src, Address dst, FungibleAssetValue amount) + { + SrcValidator = src; + DstValidator = dst; + ShareAmount = amount; + } + + internal Redelegate() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator that was previously delegated to. + /// + public Address SrcValidator { get; set; } + + /// + /// The of the validator as a destination of moved voting power. + /// + public Address DstValidator { get; set; } + + /// + /// The amount of the shared token to move delegation. + /// + public FungibleAssetValue ShareAmount { get; set; } + + /// + public IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("src", SrcValidator.Serialize()) + .Add("dst", DstValidator.Serialize()) + .Add("amount", ShareAmount.Serialize()); + + /// + public void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + SrcValidator = dict["src"].ToAddress(); + DstValidator = dict["dst"].ToAddress(); + ShareAmount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public IWorld Execute(IActionContext context) + { + IActionContext ctx = context; + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + states = RedelegateCtrl.Execute( + states, + ctx, + ctx.Signer, + SrcValidator, + DstValidator, + ShareAmount, + nativeTokens); + + return states; + } + } +} diff --git a/Lib9c.DPoS/Action/Sys/Undelegate.cs b/Lib9c.DPoS/Action/Sys/Undelegate.cs new file mode 100644 index 0000000000..11627cec4e --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/Undelegate.cs @@ -0,0 +1,80 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that cancels specified + /// of shared tokens to a given . + /// + public sealed class Undelegate : IAction + { + /// + /// Creates a new instance of action. + /// + /// The of the validator + /// to undelegate tokens. + /// The amount of the asset to be undelegated. + public Undelegate(Address validator, FungibleAssetValue amount) + { + Validator = validator; + ShareAmount = amount; + } + + internal Undelegate() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator to cancel the . + /// + public Address Validator { get; set; } + + /// + /// The amount of the asset to be undelegated. + /// + public FungibleAssetValue ShareAmount { get; set; } + + /// + public IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("validator", Validator.Serialize()) + .Add("amount", ShareAmount.Serialize()); + + /// + public void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToAddress(); + ShareAmount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public IWorld Execute(IActionContext context) + { + IActionContext ctx = context; + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + states = UndelegateCtrl.Execute( + states, + ctx, + ctx.Signer, + Validator, + ShareAmount, + nativeTokens); + + return states; + } + } +} diff --git a/Lib9c.DPoS/Action/Sys/WithdrawDelegator.cs b/Lib9c.DPoS/Action/Sys/WithdrawDelegator.cs new file mode 100644 index 0000000000..7ef60cec9d --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/WithdrawDelegator.cs @@ -0,0 +1,82 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that withdraws reward tokens from given . + /// + public sealed class WithdrawDelegator : IAction + { + /// + /// Creates a new instance of action. + /// + /// The of the validator + /// from which to withdraw the tokens. + public WithdrawDelegator(Address validator) + { + Validator = validator; + } + + internal WithdrawDelegator() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator to withdraw. + /// + public Address Validator { get; set; } + + /// + public IValue PlainValue => Validator.Serialize(); + + /// + public void LoadPlainValue(IValue plainValue) + { + Validator = plainValue.ToAddress(); + } + + /// + public IWorld Execute(IActionContext context) + { + IActionContext ctx = context; + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + states = DelegateCtrl.Distribute( + states, + ctx, + nativeTokens, + Delegation.DeriveAddress(ctx.Signer, Validator)); + + foreach (Currency nativeToken in nativeTokens) + { + FungibleAssetValue reward = states.GetBalance( + AllocateReward.RewardAddress(ctx.Signer), nativeToken); + if (reward.Sign > 0) + { + states = states.TransferAsset( + ctx, + AllocateReward.RewardAddress(ctx.Signer), + ctx.Signer, + reward); + } + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Action/Sys/WithdrawValidator.cs b/Lib9c.DPoS/Action/Sys/WithdrawValidator.cs new file mode 100644 index 0000000000..1b90360fb3 --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/WithdrawValidator.cs @@ -0,0 +1,61 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that withdraws commission tokens from . + /// + public sealed class WithdrawValidator : IAction + { + /// + /// Creates a new instance of action. + /// + public WithdrawValidator() + { + } + + /// + public IValue PlainValue => Bencodex.Types.Dictionary.Empty; + + /// + public void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public IWorld Execute(IActionContext context) + { + IActionContext ctx = context; + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + foreach (Currency nativeToken in nativeTokens) + { + FungibleAssetValue reward = states.GetBalance( + AllocateReward.RewardAddress(ctx.Signer), nativeToken); + if (reward.Sign > 0) + { + states = states.TransferAsset( + ctx, + AllocateReward.RewardAddress(ctx.Signer), + ctx.Signer, + reward); + } + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/AllocateReward.cs b/Lib9c.DPoS/Control/AllocateReward.cs new file mode 100644 index 0000000000..6cc0039330 --- /dev/null +++ b/Lib9c.DPoS/Control/AllocateReward.cs @@ -0,0 +1,184 @@ +using System.Collections.Immutable; +using System.Numerics; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.PoS; +using Libplanet.Types.Assets; +using Libplanet.Types.Consensus; +using Nekoyume.Module; +using Validator = Lib9c.DPoS.Model.Validator; +using ValidatorSet = Lib9c.DPoS.Model.ValidatorSet; + +namespace Lib9c.DPoS.Control +{ + public static class AllocateReward + { + public static BigInteger BaseProposerRewardNumer => 1; + + public static BigInteger BaseProposerRewardDenom => 100; + + public static BigInteger BonusProposerRewardNumer => 4; + + public static BigInteger BonusProposerRewardDenom => 100; + + public static Address RewardAddress(Address holderAddress) + { + return holderAddress.Derive("RewardAddress"); + } + + internal static IWorld Execute( + IWorld states, + IActionContext ctx, + IImmutableSet? nativeTokens, + IEnumerable? votes, + Address miner) + { + ValidatorSet bondedValidatorSet; + (states, bondedValidatorSet) = ValidatorSetCtrl.FetchBondedValidatorSet(states); + + if (nativeTokens is null) + { + throw new NullNativeTokensException(); + } + + foreach (Currency nativeToken in nativeTokens) + { + if (votes is { } lastVotes) + { + states = DistributeProposerReward( + states, ctx, nativeToken, miner, bondedValidatorSet, lastVotes); + + // TODO: Check if this is correct? + states = DistributeValidatorReward( + states, ctx, nativeToken, bondedValidatorSet, votes); + } + + FungibleAssetValue communityFund = states.GetBalance( + ReservedAddress.RewardPool, nativeToken); + + if (communityFund.Sign > 0) + { + states = states.TransferAsset( + ctx, + ReservedAddress.RewardPool, + ReservedAddress.CommunityPool, + communityFund); + } + } + + return states; + } + + internal static IWorld DistributeProposerReward( + IWorld states, + IActionContext ctx, + Currency nativeToken, + Address proposer, + ValidatorSet bondedValidatorSet, + IEnumerable votes) + { + FungibleAssetValue blockReward = states.GetBalance( + ReservedAddress.RewardPool, nativeToken); + + if (blockReward.Sign <= 0) + { + return states; + } + + ImmutableDictionary bondedValidatorDict + = bondedValidatorSet.Set.ToImmutableDictionary( + bondedValidator => bondedValidator.OperatorPublicKey); + + FungibleAssetValue votePowerNumer + = votes.Aggregate( + Asset.ConsensusToken * 0, (total, next) + => total + bondedValidatorDict[next.ValidatorPublicKey].ConsensusToken); + + FungibleAssetValue votePowerDenom + = bondedValidatorSet.TotalConsensusToken; + + var (baseProposerReward, _) + = (blockReward * BaseProposerRewardNumer).DivRem(BaseProposerRewardDenom); + var (bonusProposerReward, _) + = (blockReward * votePowerNumer.RawValue * BonusProposerRewardNumer) + .DivRem(votePowerDenom.RawValue * BonusProposerRewardDenom); + FungibleAssetValue proposerReward = baseProposerReward + bonusProposerReward; + + states = states.TransferAsset( + ctx, ReservedAddress.RewardPool, RewardAddress(proposer), proposerReward); + + return states; + } + + internal static IWorld DistributeValidatorReward( + IWorld states, + IActionContext ctx, + Currency nativeToken, + ValidatorSet bondedValidatorSet, + IEnumerable votes) + { + long blockHeight = ctx.BlockIndex; + FungibleAssetValue validatorRewardSum = states.GetBalance( + ReservedAddress.RewardPool, nativeToken); + + if (validatorRewardSum.Sign <= 0) + { + return states; + } + + ImmutableDictionary bondedValidatorDict + = bondedValidatorSet.Set.ToImmutableDictionary( + bondedValidator => bondedValidator.OperatorPublicKey); + + foreach (Vote vote in votes) + { + if (vote.Flag == VoteFlag.Null || vote.Flag == VoteFlag.Unknown) + { + continue; + } + + ValidatorPower bondedValidator = bondedValidatorDict[vote.ValidatorPublicKey]; + + FungibleAssetValue powerNumer + = bondedValidator.ConsensusToken; + + FungibleAssetValue powerDenom + = bondedValidatorSet.TotalConsensusToken; + + var (validatorReward, _) + = (validatorRewardSum * powerNumer.RawValue) + .DivRem(powerDenom.RawValue); + var (commission, _) + = (validatorReward * Validator.CommissionNumer) + .DivRem(Validator.CommissionDenom); + + FungibleAssetValue delegationRewardSum = validatorReward - commission; + + states = states.TransferAsset( + ctx, + ReservedAddress.RewardPool, + RewardAddress(vote.ValidatorPublicKey.Address), + commission); + + states = states.TransferAsset( + ctx, + ReservedAddress.RewardPool, + ValidatorRewards.DeriveAddress(bondedValidator.ValidatorAddress, nativeToken), + delegationRewardSum); + + states = ValidatorRewardsCtrl.Add( + states, + bondedValidator.ValidatorAddress, + nativeToken, + blockHeight, + delegationRewardSum); + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/Bond.cs b/Lib9c.DPoS/Control/Bond.cs new file mode 100644 index 0000000000..cbf2b2bbf9 --- /dev/null +++ b/Lib9c.DPoS/Control/Bond.cs @@ -0,0 +1,147 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class Bond + { + internal static (IWorld, FungibleAssetValue) Execute( + IWorld states, + IActionContext ctx, + FungibleAssetValue consensusToken, + Address validatorAddress, + Address delegationAddress, + IImmutableSet nativeTokens) + { + // TODO: Failure condition + // 1. Validator does not exist + // 2. Exchange rate is invalid(validator has no tokens but there are outstanding shares) + // 3. Amount is less than the minimum amount + // 4. Delegator does not have sufficient consensus token (fail or apply maximum) + if (!consensusToken.Currency.Equals(Asset.ConsensusToken)) + { + throw new InvalidCurrencyException(Asset.ConsensusToken, consensusToken.Currency); + } + + if (!(ValidatorCtrl.GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + // If validator share is zero, exchange rate is 1 + // Else, exchange rate is validator share / token + if (!(ValidatorCtrl.ShareFromConsensusToken( + states, validator.Address, consensusToken) is { } issuedShare)) + { + throw new InvalidExchangeRateException(validator.Address); + } + + // Mint consensus token to validator + states = states.MintAsset(ctx, validator.Address, consensusToken); + + // Mint share to delegation + states = states.MintAsset(ctx, delegationAddress, issuedShare); + + // Track total shares minted from validator + validator.DelegatorShares += issuedShare; + states = states.SetDPoSState(validator.Address, validator.Serialize()); + states = ValidatorPowerIndexCtrl.Update(states, validator.Address); + + ValidatorDelegationSet validatorDelegationSet; + (states, validatorDelegationSet) = + ValidatorDelegationSetCtrl.FetchValidatorDelegationSet(states, validator.Address); + + foreach (Address addrs in validatorDelegationSet.Set) + { + states = DelegateCtrl.Distribute(states, ctx, nativeTokens, addrs); + } + + return (states, issuedShare); + } + + internal static (IWorld, FungibleAssetValue) Cancel( + IWorld states, + IActionContext ctx, + FungibleAssetValue share, + Address validatorAddress, + Address delegationAddress, + IImmutableSet nativeTokens) + { + long blockHeight = ctx.BlockIndex; + + // Currency check + if (!share.Currency.Equals(Asset.Share)) + { + throw new InvalidCurrencyException(Asset.Share, share.Currency); + } + + FungibleAssetValue delegationShareBalance = states.GetBalance( + delegationAddress, Asset.Share); + if (share > delegationShareBalance) + { + throw new InsufficientFungibleAssetValueException( + share, + delegationShareBalance, + $"Delegation {delegationAddress} has insufficient share"); + } + + if (!(ValidatorCtrl.GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + // Delegator share burn + states = states.BurnAsset(ctx, delegationAddress, share); + + // Jailing check + FungibleAssetValue delegationShare = states.GetBalance(delegationAddress, Asset.Share); + if (delegationAddress.Equals(validator.OperatorAddress) + && !validator.Jailed + && ValidatorCtrl.ConsensusTokenFromShare(states, validator.Address, delegationShare) + < Validator.MinSelfDelegation) + { + validator.Jailed = true; + } + + // Calculate consensus token amount + if (!(ValidatorCtrl.ConsensusTokenFromShare( + states, validator.Address, share) is { } unbondingConsensusToken)) + { + throw new InvalidExchangeRateException(validator.Address); + } + + if (share.Equals(validator.DelegatorShares)) + { + unbondingConsensusToken = states.GetBalance( + validator.Address, Asset.ConsensusToken); + } + + // Subtracting from DelegatorShare have to be calculated last + // since it will affect ConsensusTokenFromShare() + validator.DelegatorShares -= share; + states = states.BurnAsset(ctx, validator.Address, unbondingConsensusToken); + states = states.SetDPoSState(validator.Address, validator.Serialize()); + + states = ValidatorPowerIndexCtrl.Update(states, validator.Address); + + ValidatorDelegationSet validatorDelegationSet; + (states, validatorDelegationSet) = + ValidatorDelegationSetCtrl.FetchValidatorDelegationSet(states, validator.Address); + + foreach (Address addrs in validatorDelegationSet.Set) + { + states = DelegateCtrl.Distribute(states, ctx, nativeTokens, addrs); + } + + return (states, unbondingConsensusToken); + } + } +} diff --git a/Lib9c.DPoS/Control/DelegateCtrl.cs b/Lib9c.DPoS/Control/DelegateCtrl.cs new file mode 100644 index 0000000000..791bff0be3 --- /dev/null +++ b/Lib9c.DPoS/Control/DelegateCtrl.cs @@ -0,0 +1,155 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class DelegateCtrl + { + internal static Delegation? GetDelegation( + IWorld states, + Address delegationAddress) + { + if (states.GetDPoSState(delegationAddress) is { } value) + { + return new Delegation(value); + } + + return null; + } + + internal static (IWorld, Delegation) FetchDelegation( + IWorld states, + Address delegatorAddress, + Address validatorAddress) + { + Address delegationAddress = Delegation.DeriveAddress( + delegatorAddress, validatorAddress); + Delegation delegation; + if (states.GetDPoSState(delegationAddress) is { } value) + { + delegation = new Delegation(value); + } + else + { + delegation = new Delegation(delegatorAddress, validatorAddress); + states = states.SetDPoSState(delegation.Address, delegation.Serialize()); + } + + return (states, delegation); + } + + internal static IWorld Execute( + IWorld states, + IActionContext ctx, + Address delegatorAddress, + Address validatorAddress, + FungibleAssetValue governanceToken, + IImmutableSet nativeTokens) + { + if (!governanceToken.Currency.Equals(Asset.GovernanceToken)) + { + throw new InvalidCurrencyException(Asset.GovernanceToken, governanceToken.Currency); + } + + FungibleAssetValue delegatorGovernanceTokenBalance = states.GetBalance( + delegatorAddress, Asset.GovernanceToken); + if (governanceToken > delegatorGovernanceTokenBalance) + { + throw new InsufficientFungibleAssetValueException( + governanceToken, + delegatorGovernanceTokenBalance, + $"Delegator {delegatorAddress} has insufficient governanceToken"); + } + + if (!(ValidatorCtrl.GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + Delegation? delegation; + (states, delegation) = FetchDelegation(states, delegatorAddress, validatorAddress); + + FungibleAssetValue consensusToken = Asset.ConsensusFromGovernance(governanceToken); + Address poolAddress = validator.Status == BondingStatus.Bonded + ? ReservedAddress.BondedPool + : ReservedAddress.UnbondedPool; + + states = states.TransferAsset( + ctx, delegatorAddress, poolAddress, governanceToken); + (states, _) = Bond.Execute( + states, + ctx, + consensusToken, + delegation.ValidatorAddress, + delegation.Address, + nativeTokens); + + states = states.SetDPoSState(delegation.Address, delegation.Serialize()); + + return states; + } + + internal static IWorld Distribute( + IWorld states, + IActionContext ctx, + IImmutableSet nativeTokens, + Address delegationAddress) + { + long blockHeight = ctx.BlockIndex; + if (!(GetDelegation(states, delegationAddress) is { } delegation)) + { + throw new NullDelegationException(delegationAddress); + } + + if (!(ValidatorCtrl.GetValidator(states, delegation.ValidatorAddress) is { } validator)) + { + throw new NullValidatorException(delegation.ValidatorAddress); + } + + foreach (Currency nativeToken in nativeTokens) + { + FungibleAssetValue delegationRewardSum = ValidatorRewardsCtrl.RewardSumBetween( + states, + delegation.ValidatorAddress, + nativeToken, + delegation.LatestDistributeHeight, + blockHeight); + + if (!(ValidatorCtrl.TokenPortionByShare( + states, + delegation.ValidatorAddress, + delegationRewardSum, + states.GetBalance(delegationAddress, Asset.Share)) is { } reward)) + { + throw new InvalidExchangeRateException(validator.Address); + } + + if (reward.Sign > 0) + { + Address validatorRewardAddress + = ValidatorRewards.DeriveAddress(delegation.ValidatorAddress, nativeToken); + + states = states.TransferAsset( + ctx, + validatorRewardAddress, + AllocateReward.RewardAddress(delegation.DelegatorAddress), + reward); + } + } + + delegation.LatestDistributeHeight = blockHeight; + + states = states.SetDPoSState(delegation.Address, delegation.Serialize()); + + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/RedelegateCtrl.cs b/Lib9c.DPoS/Control/RedelegateCtrl.cs new file mode 100644 index 0000000000..ae1eaca279 --- /dev/null +++ b/Lib9c.DPoS/Control/RedelegateCtrl.cs @@ -0,0 +1,225 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class RedelegateCtrl + { + internal static Redelegation? GetRedelegation( + IWorld states, + Address redelegationAddress) + { + if (states.GetDPoSState(redelegationAddress) is { } value) + { + return new Redelegation(value); + } + + return null; + } + + internal static (IWorld, Redelegation) FetchRedelegation( + IWorld states, + Address delegatorAddress, + Address srcValidatorAddress, + Address dstValidatorAddress) + { + Address redelegationAddress = Redelegation.DeriveAddress( + delegatorAddress, srcValidatorAddress, dstValidatorAddress); + + Redelegation redelegation; + if (states.GetDPoSState(redelegationAddress) is { } value) + { + redelegation = new Redelegation(value); + } + else + { + redelegation = new Redelegation( + delegatorAddress, + srcValidatorAddress, + dstValidatorAddress); + states = states.SetDPoSState(redelegation.Address, redelegation.Serialize()); + } + + return (states, redelegation); + } + + internal static IWorld Execute( + IWorld states, + IActionContext ctx, + Address delegatorAddress, + Address srcValidatorAddress, + Address dstValidatorAddress, + FungibleAssetValue redelegatingShare, + IImmutableSet nativeTokens) + { + // TODO: Failure condition + // 1. Delegation does not exist + // 2. Source validator does not exist + // 3. Target validator does not exist + // 3. Delegation has less shares than worth of amount + // 4. Existing redelegation has maximum entries + // 5?. Delegation does not have sufficient token (fail or apply maximum) + long blockHeight = ctx.BlockIndex; + if (!redelegatingShare.Currency.Equals(Asset.Share)) + { + throw new InvalidCurrencyException(Asset.Share, redelegatingShare.Currency); + } + + if (ValidatorCtrl.GetValidator(states, srcValidatorAddress) is null) + { + throw new NullValidatorException(srcValidatorAddress); + } + + if (ValidatorCtrl.GetValidator(states, dstValidatorAddress) is null) + { + throw new NullValidatorException(dstValidatorAddress); + } + + Redelegation redelegation; + (states, redelegation) = FetchRedelegation( + states, + delegatorAddress, + srcValidatorAddress, + dstValidatorAddress); + + if (redelegation.RedelegationEntryAddresses.Count + >= Redelegation.MaximumRedelegationEntries) + { + throw new MaximumRedelegationEntriesException( + redelegation.Address, redelegation.RedelegationEntryAddresses.Count); + } + + // Add new destination delegation, if not exist + (states, _) = DelegateCtrl.FetchDelegation( + states, delegatorAddress, dstValidatorAddress); + FungibleAssetValue unbondingConsensusToken; + FungibleAssetValue issuedShare; + (states, unbondingConsensusToken) = Bond.Cancel( + states, + ctx, + redelegatingShare, + srcValidatorAddress, + redelegation.SrcDelegationAddress, + nativeTokens); + (states, issuedShare) = Bond.Execute( + states, + ctx, + unbondingConsensusToken, + dstValidatorAddress, + redelegation.DstDelegationAddress, + nativeTokens); + + if (!(ValidatorCtrl.GetValidator(states, srcValidatorAddress) is { } srcValidator)) + { + throw new NullValidatorException(srcValidatorAddress); + } + + if (!(ValidatorCtrl.GetValidator(states, dstValidatorAddress) is { } dstValidator)) + { + throw new NullValidatorException(dstValidatorAddress); + } + + states = (srcValidator.Status, dstValidator.Status) switch + { + (BondingStatus.Bonded, BondingStatus.Unbonding) => states.TransferAsset( + ctx, + ReservedAddress.BondedPool, + ReservedAddress.UnbondedPool, + Asset.GovernanceFromConsensus(unbondingConsensusToken)), + (BondingStatus.Bonded, BondingStatus.Unbonded) => states.TransferAsset( + ctx, + ReservedAddress.BondedPool, + ReservedAddress.UnbondedPool, + Asset.GovernanceFromConsensus(unbondingConsensusToken)), + (BondingStatus.Unbonding, BondingStatus.Bonded) => states.TransferAsset( + ctx, + ReservedAddress.UnbondedPool, + ReservedAddress.BondedPool, + Asset.GovernanceFromConsensus(unbondingConsensusToken)), + (BondingStatus.Unbonded, BondingStatus.Bonded) => states.TransferAsset( + ctx, + ReservedAddress.UnbondedPool, + ReservedAddress.BondedPool, + Asset.GovernanceFromConsensus(unbondingConsensusToken)), + _ => states, + }; + + RedelegationEntry redelegationEntry = new RedelegationEntry( + redelegation.Address, + redelegatingShare, + unbondingConsensusToken, + issuedShare, + redelegation.RedelegationEntryIndex, + blockHeight); + redelegation.RedelegationEntryAddresses.Add( + redelegationEntry.Index, redelegationEntry.Address); + redelegation.RedelegationEntryIndex += 1; + + states = states.SetDPoSState(redelegationEntry.Address, redelegationEntry.Serialize()); + states = states.SetDPoSState(redelegation.Address, redelegation.Serialize()); + + states = UnbondingSetCtrl.AddRedelegationAddressSet(states, redelegation.Address); + + return states; + } + + // This have to be called for each block, + // to update staking status and generate block with updated validators. + // Would it be better to declare this on out of this class? + internal static IWorld Complete( + IWorld states, + IActionContext ctx, + Address redelegationAddress) + { + long blockHeight = ctx.BlockIndex; + if (!(GetRedelegation(states, redelegationAddress) is { } redelegation)) + { + throw new NullRedelegationException(redelegationAddress); + } + + List completedIndices = new List(); + foreach (KeyValuePair redelegationEntryAddressKV + in redelegation.RedelegationEntryAddresses) + { + IValue? serializedRedelegationEntry + = states.GetDPoSState(redelegationEntryAddressKV.Value); + if (serializedRedelegationEntry == null) + { + continue; + } + + RedelegationEntry redelegationEntry + = new RedelegationEntry(serializedRedelegationEntry); + + if (redelegationEntry.IsMatured(blockHeight)) + { + completedIndices.Add(redelegationEntry.Index); + } + } + + foreach (long index in completedIndices) + { + redelegation.RedelegationEntryAddresses.Remove(index); + } + + states = states.SetDPoSState(redelegation.Address, redelegation.Serialize()); + + if (redelegation.RedelegationEntryAddresses.Count == 0) + { + states = UnbondingSetCtrl.RemoveRedelegationAddressSet( + states, redelegation.Address); + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/UnbondingSetCtrl.cs b/Lib9c.DPoS/Control/UnbondingSetCtrl.cs new file mode 100644 index 0000000000..aea9974758 --- /dev/null +++ b/Lib9c.DPoS/Control/UnbondingSetCtrl.cs @@ -0,0 +1,129 @@ +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Control +{ + internal static class UnbondingSetCtrl + { + internal static (IWorld, UnbondingSet) FetchUnbondingSet(IWorld states) + { + UnbondingSet unbondingSet; + if (states.GetDPoSState(ReservedAddress.UnbondingSet) is { } value) + { + unbondingSet = new UnbondingSet(value); + } + else + { + unbondingSet = new UnbondingSet(); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + } + + return (states, unbondingSet); + } + + internal static IWorld CompleteValidatorSet(IWorld states, IActionContext ctx) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + foreach (Address address in unbondingSet.ValidatorAddressSet) + { + states = ValidatorCtrl.Complete(states, ctx, address); + } + + return states; + } + + internal static IWorld CompleteUndelegationSet(IWorld states, IActionContext ctx) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + foreach (Address address in unbondingSet.UndelegationAddressSet) + { + states = UndelegateCtrl.Complete(states, ctx, address); + } + + return states; + } + + internal static IWorld CompleteRedelegationSet(IWorld states, IActionContext ctx) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + foreach (Address address in unbondingSet.RedelegationAddressSet) + { + states = RedelegateCtrl.Complete(states, ctx, address); + } + + return states; + } + + internal static IWorld Complete(IWorld states, IActionContext ctx) + { + states = CompleteValidatorSet(states, ctx); + states = CompleteUndelegationSet(states, ctx); + states = CompleteRedelegationSet(states, ctx); + + return states; + } + + internal static IWorld AddValidatorAddressSet(IWorld states, Address validatorAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.ValidatorAddressSet.Add(validatorAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + + internal static IWorld AddUndelegationAddressSet(IWorld states, Address undelegationAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.UndelegationAddressSet.Add(undelegationAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + + internal static IWorld AddRedelegationAddressSet(IWorld states, Address redelegationAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.RedelegationAddressSet.Add(redelegationAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + + internal static IWorld RemoveValidatorAddressSet(IWorld states, Address validatorAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.ValidatorAddressSet.Remove(validatorAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + + internal static IWorld RemoveUndelegationAddressSet( + IWorld states, Address undelegationAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.UndelegationAddressSet.Remove(undelegationAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + + internal static IWorld RemoveRedelegationAddressSet( + IWorld states, Address redelegationAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.RedelegationAddressSet.Remove(redelegationAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/UndelegateCtrl.cs b/Lib9c.DPoS/Control/UndelegateCtrl.cs new file mode 100644 index 0000000000..782144ef2e --- /dev/null +++ b/Lib9c.DPoS/Control/UndelegateCtrl.cs @@ -0,0 +1,311 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class UndelegateCtrl + { + internal static Undelegation? GetUndelegation( + IWorld state, + Address undelegationAddress) + { + if (state.GetDPoSState(undelegationAddress) is { } value) + { + return new Undelegation(value); + } + + return null; + } + + internal static (IWorld, Undelegation) FetchUndelegation( + IWorld state, + Address delegatorAddress, + Address validatorAddress) + { + Address undelegationAddress = Undelegation.DeriveAddress( + delegatorAddress, validatorAddress); + Undelegation undelegation; + if (state.GetDPoSState(undelegationAddress) is { } value) + { + undelegation = new Undelegation(value); + } + else + { + undelegation = new Undelegation(delegatorAddress, validatorAddress); + state = state.SetDPoSState(undelegation.Address, undelegation.Serialize()); + } + + return (state, undelegation); + } + + internal static IWorld Execute( + IWorld states, + IActionContext ctx, + Address delegatorAddress, + Address validatorAddress, + FungibleAssetValue share, + IImmutableSet nativeTokens) + { + // TODO: Failure condition + // 1. Delegation does not exist + // 2. Validator does not exist + // 3. Delegation has less shares than worth of amount + // 4. Existing undelegation has maximum entries + + long blockHeight = ctx.BlockIndex; + + // Currency check + if (!share.Currency.Equals(Asset.Share)) + { + throw new InvalidCurrencyException(Asset.Share, share.Currency); + } + + Undelegation undelegation; + (states, undelegation) = FetchUndelegation(states, delegatorAddress, validatorAddress); + + if (undelegation.UndelegationEntryAddresses.Count + >= Undelegation.MaximumUndelegationEntries) + { + throw new MaximumUndelegationEntriesException( + undelegation.Address, undelegation.UndelegationEntryAddresses.Count); + } + + // Validator loading + if (!(ValidatorCtrl.GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + // Unbonding + FungibleAssetValue unbondingConsensusToken; + (states, unbondingConsensusToken) = Bond.Cancel( + states, + ctx, + share, + undelegation.ValidatorAddress, + undelegation.DelegationAddress, + nativeTokens); + + // Governance token pool transfer + if (validator.Status == BondingStatus.Bonded) + { + states = states.TransferAsset( + ctx, + ReservedAddress.BondedPool, + ReservedAddress.UnbondedPool, + Asset.GovernanceFromConsensus(unbondingConsensusToken)); + } + + // Entry register + UndelegationEntry undelegationEntry = new UndelegationEntry( + undelegation.Address, + unbondingConsensusToken, + undelegation.UndelegationEntryIndex, + blockHeight); + undelegation.UndelegationEntryAddresses.Add( + undelegationEntry.Index, undelegationEntry.Address); + undelegation.UndelegationEntryIndex += 1; + + // TODO: Global state indexing is also needed + states = states.SetDPoSState(undelegationEntry.Address, undelegationEntry.Serialize()); + + states = states.SetDPoSState(undelegation.Address, undelegation.Serialize()); + + states = UnbondingSetCtrl.AddUndelegationAddressSet(states, undelegation.Address); + + return states; + } + + internal static IWorld Cancel( + IWorld states, + IActionContext ctx, + Address undelegationAddress, + FungibleAssetValue cancelledConsensusToken, + IImmutableSet nativeTokens) + { + long blockHeight = ctx.BlockIndex; + + // Currency check + if (!cancelledConsensusToken.Currency.Equals(Asset.ConsensusToken)) + { + throw new InvalidCurrencyException( + Asset.ConsensusToken, cancelledConsensusToken.Currency); + } + + if (!(GetUndelegation(states, undelegationAddress) is { } undelegation)) + { + throw new NullUndelegationException(undelegationAddress); + } + + // Validator loading + if (!(ValidatorCtrl.GetValidator( + states, undelegation.ValidatorAddress) is { } validator)) + { + throw new NullValidatorException(undelegation.ValidatorAddress); + } + + // Copy of cancelling amount + FungibleAssetValue cancellingConsensusToken = + new FungibleAssetValue( + Asset.ConsensusToken, + cancelledConsensusToken.MajorUnit, + cancelledConsensusToken.MinorUnit); + + // Iterate all entries + List undelegationEntryIndices = new List(); + foreach (KeyValuePair undelegationEntryAddressKV + in undelegation.UndelegationEntryAddresses) + { + // Load entry + IValue? serializedUndelegationEntry + = states.GetDPoSState(undelegationEntryAddressKV.Value); + + // Skip empty entry + if (serializedUndelegationEntry == null) + { + continue; + } + + UndelegationEntry undelegationEntry + = new UndelegationEntry(serializedUndelegationEntry); + + // Double check for unbonded entry + if (blockHeight >= undelegationEntry.CompletionBlockHeight) + { + throw new PostmatureUndelegationEntryException( + blockHeight, + undelegationEntry.CompletionBlockHeight, + undelegationEntry.Address); + } + + // Check if cancelledConsensusToken is less than total undelegation + if (cancellingConsensusToken.RawValue < 0) + { + throw new InsufficientFungibleAssetValueException( + cancelledConsensusToken, + cancelledConsensusToken + cancellingConsensusToken, + $"Undelegation {undelegationAddress} has insufficient consensusToken"); + } + + // Apply unbonding + if (cancellingConsensusToken < undelegationEntry.UnbondingConsensusToken) + { + undelegationEntry.UnbondingConsensusToken -= cancellingConsensusToken; + states = states.SetDPoSState( + undelegationEntry.Address, undelegationEntry.Serialize()); + break; + } + + // If cancelling amount is more than current entry, save and skip + else + { + cancellingConsensusToken -= undelegationEntry.UnbondingConsensusToken; + undelegationEntryIndices.Add(undelegationEntry.Index); + } + } + + (states, _) = Bond.Execute( + states, + ctx, + cancelledConsensusToken, + undelegation.ValidatorAddress, + undelegation.DelegationAddress, + nativeTokens); + + if (validator.Status == BondingStatus.Bonded) + { + states = states.TransferAsset( + ctx, + ReservedAddress.UnbondedPool, + ReservedAddress.BondedPool, + Asset.GovernanceFromConsensus(cancelledConsensusToken)); + } + + undelegationEntryIndices.ForEach( + idx => undelegation.UndelegationEntryAddresses.Remove(idx)); + + states = states.SetDPoSState(undelegation.Address, undelegation.Serialize()); + + if (undelegation.UndelegationEntryAddresses.Count == 0) + { + states = UnbondingSetCtrl.RemoveUndelegationAddressSet( + states, undelegation.Address); + } + + return states; + } + + // This have to be called for each block, + // to update staking status and generate block with updated validators. + // Would it be better to declare this on out of this class? + internal static IWorld Complete( + IWorld states, + IActionContext ctx, + Address undelegationAddress) + { + long blockHeight = ctx.BlockIndex; + if (!(GetUndelegation(states, undelegationAddress) is { } undelegation)) + { + throw new NullUndelegationException(undelegationAddress); + } + + List completedIndices = new List(); + + // Iterate all entries + foreach (KeyValuePair undelegationEntryAddressKV + in undelegation.UndelegationEntryAddresses) + { + // Load entry + IValue? serializedUndelegationEntry + = states.GetDPoSState(undelegationEntryAddressKV.Value); + + // Skip empty entry + if (serializedUndelegationEntry == null) + { + continue; + } + + UndelegationEntry undelegationEntry + = new UndelegationEntry(serializedUndelegationEntry); + + // Complete matured entries + if (undelegationEntry.IsMatured(blockHeight)) + { + // Pay back governance token to delegator + states = states.TransferAsset( + ctx, + ReservedAddress.UnbondedPool, + undelegation.DelegatorAddress, + Asset.GovernanceFromConsensus(undelegationEntry.UnbondingConsensusToken)); + + // Remove entry + completedIndices.Add(undelegationEntry.Index); + } + } + + foreach (long index in completedIndices) + { + undelegation.UndelegationEntryAddresses.Remove(index); + } + + states = states.SetDPoSState(undelegation.Address, undelegation.Serialize()); + + if (undelegation.UndelegationEntryAddresses.Count == 0) + { + states = UnbondingSetCtrl.RemoveUndelegationAddressSet( + states, undelegation.Address); + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/ValidatorCtrl.cs b/Lib9c.DPoS/Control/ValidatorCtrl.cs new file mode 100644 index 0000000000..3fb6f4189a --- /dev/null +++ b/Lib9c.DPoS/Control/ValidatorCtrl.cs @@ -0,0 +1,263 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class ValidatorCtrl + { + internal static Validator? GetValidator( + IWorld states, + Address validatorAddress) + { + if (states.GetDPoSState(validatorAddress) is { } value) + { + return new Validator(value); + } + + return null; + } + + internal static (IWorld, Validator) FetchValidator( + IWorld states, + Address operatorAddress, + PublicKey operatorPublicKey) + { + if (!operatorAddress.Equals(operatorPublicKey.Address)) + { + throw new PublicKeyAddressMatchingException(operatorAddress, operatorPublicKey); + } + + Address validatorAddress = Validator.DeriveAddress(operatorAddress); + Validator validator; + if (states.GetDPoSState(validatorAddress) is { } value) + { + validator = new Validator(value); + } + else + { + validator = new Validator(operatorAddress, operatorPublicKey); + states = states.SetDPoSState(validator.Address, validator.Serialize()); + } + + return (states, validator); + } + + internal static IWorld Create( + IWorld states, + IActionContext ctx, + Address operatorAddress, + PublicKey operatorPublicKey, + FungibleAssetValue governanceToken, + IImmutableSet nativeTokens) + { + if (!governanceToken.Currency.Equals(Asset.GovernanceToken)) + { + throw new InvalidCurrencyException(Asset.GovernanceToken, governanceToken.Currency); + } + + FungibleAssetValue consensusToken = Asset.ConsensusFromGovernance(governanceToken); + if (consensusToken < Validator.MinSelfDelegation) + { + throw new InsufficientFungibleAssetValueException( + Validator.MinSelfDelegation, consensusToken, "Insufficient self delegation"); + } + + Address validatorAddress = Validator.DeriveAddress(operatorAddress); + if (states.GetDPoSState(validatorAddress) != null) + { + throw new DuplicatedValidatorException(validatorAddress); + } + + Validator validator; + (states, validator) = FetchValidator(states, operatorAddress, operatorPublicKey); + + states = DelegateCtrl.Execute( + states, + ctx, + operatorAddress, + validator.Address, + governanceToken, + nativeTokens); + + // Does not save current instance, since it's done on delegation + return states; + } + + internal static FungibleAssetValue? ShareFromConsensusToken( + IWorld states, Address validatorAddress, FungibleAssetValue consensusToken) + { + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + FungibleAssetValue validatorConsensusToken + = states.GetBalance(validator.Address, Asset.ConsensusToken); + + if (validator.DelegatorShares.Equals(Asset.Share * 0)) + { + return new FungibleAssetValue( + Asset.Share, consensusToken.MajorUnit, consensusToken.MinorUnit); + } + + if (validatorConsensusToken.RawValue == 0) + { + return null; + } + + FungibleAssetValue share + = (validator.DelegatorShares + * consensusToken.RawValue) + .DivRem(validatorConsensusToken.RawValue, out _); + + return share; + } + + internal static FungibleAssetValue? TokenPortionByShare( + IWorld states, + Address validatorAddress, + FungibleAssetValue token, + FungibleAssetValue share) + { + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + if (validator.DelegatorShares.RawValue == 0) + { + return null; + } + + var (tokenPortion, _) + = (token * share.RawValue) + .DivRem(validator.DelegatorShares.RawValue); + + return tokenPortion; + } + + internal static FungibleAssetValue? ConsensusTokenFromShare( + IWorld states, Address validatorAddress, FungibleAssetValue share) + { + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + FungibleAssetValue validatorConsensusToken + = states.GetBalance(validator.Address, Asset.ConsensusToken); + + // Is below conditional statement right? + // Need to be investigated + if (validatorConsensusToken.RawValue == 0) + { + return null; + } + + if (validator.DelegatorShares.RawValue == 0) + { + return null; + } + + FungibleAssetValue consensusToken + = (validatorConsensusToken + * share.RawValue) + .DivRem(validator.DelegatorShares.RawValue, out _); + + return consensusToken; + } + + internal static IWorld Bond( + IWorld states, + IActionContext ctx, + Address validatorAddress) + { + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + validator.UnbondingCompletionBlockHeight = -1; + if (validator.Status != BondingStatus.Bonded) + { + states = states.TransferAsset( + ctx, + ReservedAddress.UnbondedPool, + ReservedAddress.BondedPool, + Asset.GovernanceFromConsensus( + states.GetBalance(validator.Address, Asset.ConsensusToken))); + } + + validator.Status = BondingStatus.Bonded; + states = states.SetDPoSState(validator.Address, validator.Serialize()); + return states; + } + + internal static IWorld Unbond( + IWorld states, + IActionContext ctx, + Address validatorAddress) + { + long blockHeight = ctx.BlockIndex; + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + validator.UnbondingCompletionBlockHeight = blockHeight + UnbondingSet.Period; + if (validator.Status == BondingStatus.Bonded) + { + states = states.TransferAsset( + ctx, + ReservedAddress.BondedPool, + ReservedAddress.UnbondedPool, + Asset.GovernanceFromConsensus( + states.GetBalance(validator.Address, Asset.ConsensusToken))); + } + + validator.Status = BondingStatus.Unbonding; + states = states.SetDPoSState(validator.Address, validator.Serialize()); + + states = UnbondingSetCtrl.AddValidatorAddressSet(states, validator.Address); + + return states; + } + + internal static IWorld Complete( + IWorld states, + IActionContext ctx, + Address validatorAddress) + { + long blockHeight = ctx.BlockIndex; + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + if (!validator.IsMatured(blockHeight) || (validator.Status != BondingStatus.Unbonding)) + { + return states; + } + + validator.Status = BondingStatus.Unbonded; + states = states.SetDPoSState(validator.Address, validator.Serialize()); + + states = UnbondingSetCtrl.RemoveValidatorAddressSet(states, validator.Address); + + // Later implemented get rid of validator + if (validator.DelegatorShares == Asset.Share * 0) + { + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/ValidatorDelegationSetCtrl.cs b/Lib9c.DPoS/Control/ValidatorDelegationSetCtrl.cs new file mode 100644 index 0000000000..967dda206e --- /dev/null +++ b/Lib9c.DPoS/Control/ValidatorDelegationSetCtrl.cs @@ -0,0 +1,59 @@ +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Control +{ + internal static class ValidatorDelegationSetCtrl + { + internal static (IWorld, ValidatorDelegationSet) FetchValidatorDelegationSet( + IWorld states, Address validatorAddress) + { + Address validatorDelegationSetAddress = ValidatorDelegationSet.DeriveAddress( + validatorAddress); + + ValidatorDelegationSet validatorDelegationSet; + if (states.GetDPoSState(validatorDelegationSetAddress) is { } value) + { + validatorDelegationSet = new ValidatorDelegationSet(value); + } + else + { + validatorDelegationSet = new ValidatorDelegationSet(validatorAddress); + states = states.SetDPoSState( + validatorDelegationSetAddress, validatorDelegationSet.Serialize()); + } + + return (states, validatorDelegationSet); + } + + internal static IWorld Add( + IWorld states, + Address validatorAddress, + Address delegationAddress) + { + ValidatorDelegationSet validatorDelegationSet; + (states, validatorDelegationSet) + = FetchValidatorDelegationSet(states, validatorAddress); + validatorDelegationSet.Add(delegationAddress); + states = states.SetDPoSState( + validatorDelegationSet.Address, validatorDelegationSet.Serialize()); + return states; + } + + internal static IWorld Remove( + IWorld states, + Address validatorAddress, + Address delegationAddress) + { + ValidatorDelegationSet validatorDelegationSet; + (states, validatorDelegationSet) + = FetchValidatorDelegationSet(states, validatorAddress); + validatorDelegationSet.Remove(delegationAddress); + states = states.SetDPoSState( + validatorDelegationSet.Address, validatorDelegationSet.Serialize()); + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/ValidatorPowerIndexCtrl.cs b/Lib9c.DPoS/Control/ValidatorPowerIndexCtrl.cs new file mode 100644 index 0000000000..fee7a3640c --- /dev/null +++ b/Lib9c.DPoS/Control/ValidatorPowerIndexCtrl.cs @@ -0,0 +1,69 @@ +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class ValidatorPowerIndexCtrl + { + internal static (IWorld, ValidatorPowerIndex) FetchValidatorPowerIndex( + IWorld states) + { + ValidatorPowerIndex validatorPowerIndex; + if (states.GetDPoSState(ReservedAddress.ValidatorPowerIndex) is { } value) + { + validatorPowerIndex = new ValidatorPowerIndex(value); + } + else + { + validatorPowerIndex = new ValidatorPowerIndex(); + states = states.SetDPoSState( + validatorPowerIndex.Address, validatorPowerIndex.Serialize()); + } + + return (states, validatorPowerIndex); + } + + internal static IWorld Update( + IWorld states, + Address validatorAddress) + { + ValidatorPowerIndex validatorPowerIndex; + (states, validatorPowerIndex) = FetchValidatorPowerIndex(states); + validatorPowerIndex.Index.RemoveWhere( + key => key.ValidatorAddress.Equals(validatorAddress)); + if (!(ValidatorCtrl.GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + if (validator.Jailed) + { + return states; + } + + FungibleAssetValue consensusToken = states.GetBalance( + validatorAddress, Asset.ConsensusToken); + ValidatorPower validatorPower + = new ValidatorPower(validatorAddress, validator.OperatorPublicKey, consensusToken); + validatorPowerIndex.Index.Add(validatorPower); + states = states.SetDPoSState(validatorPowerIndex.Address, validatorPowerIndex.Serialize()); + return states; + } + + internal static IWorld Update(IWorld states, IEnumerable
validatorAddresses) + { + foreach (Address validatorAddress in validatorAddresses) + { + states = Update(states, validatorAddress); + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/ValidatorRewardsCtrl.cs b/Lib9c.DPoS/Control/ValidatorRewardsCtrl.cs new file mode 100644 index 0000000000..ef001fef83 --- /dev/null +++ b/Lib9c.DPoS/Control/ValidatorRewardsCtrl.cs @@ -0,0 +1,85 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Control +{ + internal static class ValidatorRewardsCtrl + { + internal static ValidatorRewards? GetValidatorRewards( + IWorld states, + Address validatorAddress) + { + if (states.GetDPoSState(validatorAddress) is { } value) + { + return new ValidatorRewards(value); + } + + return null; + } + + internal static (IWorld, ValidatorRewards) FetchValidatorRewards( + IWorld states, + Address validatorAddress, + Currency currency) + { + Address validatorRewardsAddress + = ValidatorRewards.DeriveAddress(validatorAddress, currency); + ValidatorRewards validatorRewards; + if (states.GetDPoSState(validatorRewardsAddress) is { } value) + { + validatorRewards = new ValidatorRewards(value); + } + else + { + validatorRewards = new ValidatorRewards(validatorAddress, currency); + states = states.SetDPoSState(validatorRewards.Address, validatorRewards.Serialize()); + } + + return (states, validatorRewards); + } + + internal static ImmutableSortedDictionary RewardsBetween( + IWorld states, + Address validatorAddress, + Currency currency, + long minBlockHeight, + long maxBlockHeight) + { + ValidatorRewards validatorRewards; + (_, validatorRewards) = FetchValidatorRewards(states, validatorAddress, currency); + return validatorRewards.Rewards.Where( + kv => minBlockHeight <= kv.Key && kv.Key < maxBlockHeight) + .ToImmutableSortedDictionary(); + } + + internal static FungibleAssetValue RewardSumBetween( + IWorld states, + Address validatorAddress, + Currency currency, + long minBlockHeight, + long maxBlockHeight) + { + return RewardsBetween( + states, validatorAddress, currency, minBlockHeight, maxBlockHeight) + .Aggregate(currency * 0, (total, next) => total + next.Value); + } + + internal static IWorld Add( + IWorld states, + Address validatorAddress, + Currency currency, + long blockHeight, + FungibleAssetValue reward) + { + ValidatorRewards validatorRewards; + (states, validatorRewards) = FetchValidatorRewards(states, validatorAddress, currency); + validatorRewards.Add(blockHeight, reward); + states = states.SetDPoSState(validatorRewards.Address, validatorRewards.Serialize()); + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/ValidatorSetCtrl.cs b/Lib9c.DPoS/Control/ValidatorSetCtrl.cs new file mode 100644 index 0000000000..8230662c8f --- /dev/null +++ b/Lib9c.DPoS/Control/ValidatorSetCtrl.cs @@ -0,0 +1,127 @@ +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class ValidatorSetCtrl + { + internal static (IWorld, ValidatorSet) FetchValidatorSet(IWorld states, Address address) + { + ValidatorSet validatorSet; + if (states.GetDPoSState(address) is { } value) + { + validatorSet = new ValidatorSet(value); + } + else + { + validatorSet = new ValidatorSet(); + states = states.SetDPoSState( + address, validatorSet.Serialize()); + } + + return (states, validatorSet); + } + + internal static (IWorld, ValidatorSet) FetchBondedValidatorSet(IWorld states) + => FetchValidatorSet(states, ReservedAddress.BondedValidatorSet); + + // Have to be called on tip changed + internal static IWorld Update(IWorld states, IActionContext ctx) + { + states = UpdateSets(states); + states = UpdateBondedSetElements(states, ctx); + states = UpdateUnbondedSetElements(states, ctx); + states = UnbondingSetCtrl.Complete(states, ctx); + + return states; + } + + internal static IWorld UpdateSets(IWorld states) + { + ValidatorSet previousBondedSet; + (states, previousBondedSet) = FetchValidatorSet( + states, ReservedAddress.BondedValidatorSet); + ValidatorSet bondedSet = new ValidatorSet(); + ValidatorSet unbondedSet = new ValidatorSet(); + ValidatorPowerIndex validatorPowerIndex; + (states, validatorPowerIndex) + = ValidatorPowerIndexCtrl.FetchValidatorPowerIndex(states); + + foreach (var item in validatorPowerIndex.Index.Select((value, index) => (value, index))) + { + if (!(ValidatorCtrl.GetValidator( + states, item.value.ValidatorAddress) is { } validator)) + { + throw new NullValidatorException(item.value.ValidatorAddress); + } + + if (validator.Jailed) + { + throw new JailedValidatorException(validator.Address); + } + + if (item.index >= ValidatorSet.MaxBondedSetSize || + states.GetBalance(item.value.ValidatorAddress, Asset.ConsensusToken) + <= Asset.ConsensusToken * 0) + { + unbondedSet.Add(item.value); + } + else + { + bondedSet.Add(item.value); + } + } + + states = states.SetDPoSState( + ReservedAddress.PreviousBondedValidatorSet, previousBondedSet.Serialize()); + states = states.SetDPoSState( + ReservedAddress.BondedValidatorSet, bondedSet.Serialize()); + states = states.SetDPoSState( + ReservedAddress.UnbondedValidatorSet, unbondedSet.Serialize()); + + return states; + } + + internal static IWorld UpdateBondedSetElements(IWorld states, IActionContext ctx) + { + ValidatorSet bondedSet; + (states, bondedSet) = FetchBondedValidatorSet(states); + foreach (ValidatorPower validatorPower in bondedSet.Set) + { + if (!(ValidatorCtrl.GetValidator( + states, validatorPower.ValidatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorPower.ValidatorAddress); + } + + states = ValidatorCtrl.Bond(states, ctx, validatorPower.ValidatorAddress); + } + + return states; + } + + internal static IWorld UpdateUnbondedSetElements(IWorld states, IActionContext ctx) + { + ValidatorSet unbondedSet; + (states, unbondedSet) = FetchValidatorSet(states, ReservedAddress.UnbondedValidatorSet); + foreach (ValidatorPower validatorPower in unbondedSet.Set) + { + if (!(ValidatorCtrl.GetValidator( + states, validatorPower.ValidatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorPower.ValidatorAddress); + } + + states = ValidatorCtrl.Unbond(states, ctx, validatorPower.ValidatorAddress); + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Exception/DuplicatedValidatorException.cs b/Lib9c.DPoS/Exception/DuplicatedValidatorException.cs new file mode 100644 index 0000000000..4ece4e6807 --- /dev/null +++ b/Lib9c.DPoS/Exception/DuplicatedValidatorException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class DuplicatedValidatorException : System.Exception + { + public DuplicatedValidatorException(Address address) + : base($"Validator {address} is duplicated") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/InsufficientFungibleAssetValueException.cs b/Lib9c.DPoS/Exception/InsufficientFungibleAssetValueException.cs new file mode 100644 index 0000000000..e5f717dde8 --- /dev/null +++ b/Lib9c.DPoS/Exception/InsufficientFungibleAssetValueException.cs @@ -0,0 +1,13 @@ +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Exception +{ + public class InsufficientFungibleAssetValueException : System.Exception + { + public InsufficientFungibleAssetValueException( + FungibleAssetValue required, FungibleAssetValue actual, string message) + : base($"{message}, required : {required} > actual : {actual}") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/InvalidCurrencyException.cs b/Lib9c.DPoS/Exception/InvalidCurrencyException.cs new file mode 100644 index 0000000000..47a667ceed --- /dev/null +++ b/Lib9c.DPoS/Exception/InvalidCurrencyException.cs @@ -0,0 +1,12 @@ +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Exception +{ + public class InvalidCurrencyException : System.Exception + { + public InvalidCurrencyException(Currency expected, Currency actual) + : base($"Expected {expected}, found {actual}") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/InvalidExchangeRateException.cs b/Lib9c.DPoS/Exception/InvalidExchangeRateException.cs new file mode 100644 index 0000000000..bd460b7aea --- /dev/null +++ b/Lib9c.DPoS/Exception/InvalidExchangeRateException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class InvalidExchangeRateException : System.Exception + { + public InvalidExchangeRateException(Address address) + : base($"Exchange of Validator {address} is invalid") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/JailedValidatorException.cs b/Lib9c.DPoS/Exception/JailedValidatorException.cs new file mode 100644 index 0000000000..5e520580f7 --- /dev/null +++ b/Lib9c.DPoS/Exception/JailedValidatorException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class JailedValidatorException : System.Exception + { + public JailedValidatorException(Address address) + : base($"Validator {address} is jailed") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/MaximumRedelegationEntriesException.cs b/Lib9c.DPoS/Exception/MaximumRedelegationEntriesException.cs new file mode 100644 index 0000000000..3d71822f31 --- /dev/null +++ b/Lib9c.DPoS/Exception/MaximumRedelegationEntriesException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class MaximumRedelegationEntriesException : System.Exception + { + public MaximumRedelegationEntriesException(Address address, long count) + : base($"Redelegation {address} reached maximum entry size : {count}") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/MaximumUndelegationEntriesException.cs b/Lib9c.DPoS/Exception/MaximumUndelegationEntriesException.cs new file mode 100644 index 0000000000..99599eaa17 --- /dev/null +++ b/Lib9c.DPoS/Exception/MaximumUndelegationEntriesException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class MaximumUndelegationEntriesException : System.Exception + { + public MaximumUndelegationEntriesException(Address address, long count) + : base($"Undelegation {address} reached maximum entry size : {count}") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/NullDelegationException.cs b/Lib9c.DPoS/Exception/NullDelegationException.cs new file mode 100644 index 0000000000..f076dfa044 --- /dev/null +++ b/Lib9c.DPoS/Exception/NullDelegationException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class NullDelegationException : System.Exception + { + public NullDelegationException(Address address) + : base($"Delegation {address} not found") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/NullNativeTokensException.cs b/Lib9c.DPoS/Exception/NullNativeTokensException.cs new file mode 100644 index 0000000000..e7b42cb56e --- /dev/null +++ b/Lib9c.DPoS/Exception/NullNativeTokensException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Libplanet.PoS +{ + public class NullNativeTokensException : Exception + { + public NullNativeTokensException() + : base($"At least one native token have to be set on block policy") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/NullRedelegationException.cs b/Lib9c.DPoS/Exception/NullRedelegationException.cs new file mode 100644 index 0000000000..cc531abaa9 --- /dev/null +++ b/Lib9c.DPoS/Exception/NullRedelegationException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class NullRedelegationException : System.Exception + { + public NullRedelegationException(Address address) + : base($"Redelegation {address} not found") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/NullUndelegationException.cs b/Lib9c.DPoS/Exception/NullUndelegationException.cs new file mode 100644 index 0000000000..28f9d86a21 --- /dev/null +++ b/Lib9c.DPoS/Exception/NullUndelegationException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class NullUndelegationException : System.Exception + { + public NullUndelegationException(Address address) + : base($"Undelegation {address} not found") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/NullValidatorException.cs b/Lib9c.DPoS/Exception/NullValidatorException.cs new file mode 100644 index 0000000000..3d75440413 --- /dev/null +++ b/Lib9c.DPoS/Exception/NullValidatorException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class NullValidatorException : System.Exception + { + public NullValidatorException(Address address) + : base($"Validator {address} not found") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/PostmatureUndelegationEntryException.cs b/Lib9c.DPoS/Exception/PostmatureUndelegationEntryException.cs new file mode 100644 index 0000000000..eed386cbef --- /dev/null +++ b/Lib9c.DPoS/Exception/PostmatureUndelegationEntryException.cs @@ -0,0 +1,14 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class PostmatureUndelegationEntryException : System.Exception + { + public PostmatureUndelegationEntryException( + long blockHeight, long completionBlockHeight, Address address) + : base($"UndelegationEntry {address} is postmatured, " + + $"blockHeight : {blockHeight} > completionBlockHeight : {completionBlockHeight}") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/PublicKeyAddressMatchingException.cs b/Lib9c.DPoS/Exception/PublicKeyAddressMatchingException.cs new file mode 100644 index 0000000000..ece95fd0a2 --- /dev/null +++ b/Lib9c.DPoS/Exception/PublicKeyAddressMatchingException.cs @@ -0,0 +1,13 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class PublicKeyAddressMatchingException : System.Exception + { + public PublicKeyAddressMatchingException(Address expected, PublicKey publicKey) + : base($"publicKey {publicKey} does not match to address " + + $": Expected {expected}, found {publicKey.Address}") + { + } + } +} diff --git a/Lib9c.DPoS/Lib9c.DPoS.csproj b/Lib9c.DPoS/Lib9c.DPoS.csproj new file mode 100644 index 0000000000..3a4eb76f25 --- /dev/null +++ b/Lib9c.DPoS/Lib9c.DPoS.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/Lib9c.DPoS/Misc/Asset.cs b/Lib9c.DPoS/Misc/Asset.cs new file mode 100644 index 0000000000..bc26e822ec --- /dev/null +++ b/Lib9c.DPoS/Misc/Asset.cs @@ -0,0 +1,28 @@ +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Misc +{ + public struct Asset + { + public static readonly Currency GovernanceToken = + Currency.Legacy("NCG", 2, null); + + public static readonly Currency ConsensusToken = + Currency.Uncapped("ConsensusToken", 18, minters: null); + + public static readonly Currency Share = + Currency.Uncapped("Share", 18, minters: null); + + public static FungibleAssetValue ConsensusFromGovernance(FungibleAssetValue governanceToken) + { + return new FungibleAssetValue( + ConsensusToken, governanceToken.MajorUnit, governanceToken.MinorUnit); + } + + public static FungibleAssetValue GovernanceFromConsensus(FungibleAssetValue consensusToken) + { + return new FungibleAssetValue( + GovernanceToken, consensusToken.MajorUnit, consensusToken.MinorUnit); + } + } +} diff --git a/Lib9c.DPoS/Misc/ReservedAddress.cs b/Lib9c.DPoS/Misc/ReservedAddress.cs new file mode 100644 index 0000000000..69b0032e68 --- /dev/null +++ b/Lib9c.DPoS/Misc/ReservedAddress.cs @@ -0,0 +1,40 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Misc +{ + public static class ReservedAddress + { + public static readonly Address DPoSAccountAddress + = new Address("0000000000000000000000000000000000100000"); + + public static readonly Address BondedPool + = new Address("0000000000000000000000000000000000100001"); + + public static readonly Address UnbondedPool + = new Address("0000000000000000000000000000000000100002"); + + public static readonly Address RewardPool + = new Address("0000000000000000000000000000000000100003"); + + public static readonly Address ValidatorPowerIndex + = new Address("0000000000000000000000000000000000100004"); + + public static readonly Address PreviousBondedValidatorSet + = new Address("0000000000000000000000000000000000100005"); + + public static readonly Address BondedValidatorSet + = new Address("0000000000000000000000000000000000100006"); + + public static readonly Address UnbondedValidatorSet + = new Address("0000000000000000000000000000000000100007"); + + public static readonly Address UnbondingSet + = new Address("0000000000000000000000000000000000100008"); + + public static readonly Address BlockRewardHistory + = new Address("0000000000000000000000000000000000100009"); + + public static readonly Address CommunityPool + = new Address("0000000000000000000000000000000000100010"); + } +} diff --git a/Lib9c.DPoS/Model/BondingStatus.cs b/Lib9c.DPoS/Model/BondingStatus.cs new file mode 100644 index 0000000000..3980e5eef6 --- /dev/null +++ b/Lib9c.DPoS/Model/BondingStatus.cs @@ -0,0 +1,21 @@ +namespace Lib9c.DPoS.Model +{ + public enum BondingStatus : byte + { + /// + /// For delegation : Current delegation is bonded. + /// For validator : Current validator has enough consensus power to vote. + /// + Bonded = 0, + + /// + /// . + /// + Unbonding = 1, + + /// + /// . + /// + Unbonded = 2, + } +} diff --git a/Lib9c.DPoS/Model/Delegation.cs b/Lib9c.DPoS/Model/Delegation.cs new file mode 100644 index 0000000000..d5b938eb42 --- /dev/null +++ b/Lib9c.DPoS/Model/Delegation.cs @@ -0,0 +1,88 @@ +using Bencodex.Types; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Model +{ + public class Delegation : IEquatable + { + public Delegation(Address delegatorAddress, Address validatorAddress) + { + Address = DeriveAddress(delegatorAddress, validatorAddress); + DelegatorAddress = delegatorAddress; + ValidatorAddress = validatorAddress; + LatestDistributeHeight = 0; + } + + public Delegation(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + DelegatorAddress = serializedList[1].ToAddress(); + ValidatorAddress = serializedList[2].ToAddress(); + LatestDistributeHeight = serializedList[3].ToLong(); + } + + public Delegation(Delegation delegation) + { + Address = delegation.Address; + DelegatorAddress = delegation.DelegatorAddress; + ValidatorAddress = delegation.ValidatorAddress; + LatestDistributeHeight = delegation.LatestDistributeHeight; + } + + public Address Address { get; } + + public Address DelegatorAddress { get; } + + public Address ValidatorAddress { get; } + + public long LatestDistributeHeight { get; set; } + + public static bool operator ==(Delegation obj, Delegation other) + { + return obj.Equals(other); + } + + public static bool operator !=(Delegation obj, Delegation other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address delegatorAddress, Address validatorAddress) + { + return delegatorAddress + .Derive(validatorAddress.ToByteArray()) + .Derive("Delegation"); + } + + public IValue Serialize() + { + return List.Empty + .Add(Address.Serialize()) + .Add(DelegatorAddress.Serialize()) + .Add(ValidatorAddress.Serialize()) + .Add(LatestDistributeHeight.Serialize()); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Delegation); + } + + public bool Equals(Delegation? other) + { + return !(other is null) && + Address.Equals(other.Address) && + DelegatorAddress.Equals(other.DelegatorAddress) && + ValidatorAddress.Equals(other.ValidatorAddress) && + LatestDistributeHeight.Equals(other.LatestDistributeHeight); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c.DPoS/Model/Redelegation.cs b/Lib9c.DPoS/Model/Redelegation.cs new file mode 100644 index 0000000000..7c03d75e5d --- /dev/null +++ b/Lib9c.DPoS/Model/Redelegation.cs @@ -0,0 +1,137 @@ +using Bencodex.Types; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Model +{ + public class Redelegation : IEquatable + { + public Redelegation( + Address delegatorAddress, Address srcValidatorAddress, Address dstValidatorAddress) + { + Address = DeriveAddress(delegatorAddress, srcValidatorAddress, dstValidatorAddress); + DelegatorAddress = delegatorAddress; + SrcValidatorAddress = srcValidatorAddress; + DstValidatorAddress = dstValidatorAddress; + RedelegationEntryIndex = 0; + RedelegationEntryAddresses = new SortedList(); + } + + public Redelegation(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + DelegatorAddress = serializedList[1].ToAddress(); + SrcValidatorAddress = serializedList[2].ToAddress(); + DstValidatorAddress = serializedList[3].ToAddress(); + RedelegationEntryIndex = serializedList[4].ToLong(); + RedelegationEntryAddresses = new SortedList(); + foreach ( + IValue serializedRedelegationEntryAddress + in (List)serializedList[5]) + { + List items = (List)serializedRedelegationEntryAddress; + RedelegationEntryAddresses.Add(items[0].ToLong(), items[1].ToAddress()); + } + } + + public Redelegation(Redelegation redelegation) + { + Address = redelegation.Address; + DelegatorAddress = redelegation.DelegatorAddress; + SrcValidatorAddress = redelegation.SrcValidatorAddress; + DstValidatorAddress = redelegation.DstValidatorAddress; + RedelegationEntryIndex = redelegation.RedelegationEntryIndex; + RedelegationEntryAddresses = redelegation.RedelegationEntryAddresses; + } + + // TODO: Better structure + // This hard coding will cause some problems when it's modified + // May be it would be better to be serialized + public static int MaximumRedelegationEntries { get => 10; } + + public Address Address { get; } + + public Address DelegatorAddress { get; } + + public Address SrcValidatorAddress { get; } + + public Address DstValidatorAddress { get; } + + public Address SrcDelegationAddress + => Delegation.DeriveAddress(DelegatorAddress, SrcValidatorAddress); + + public Address DstDelegationAddress + => Delegation.DeriveAddress(DelegatorAddress, DstValidatorAddress); + + public long RedelegationEntryIndex { get; set; } + + public SortedList RedelegationEntryAddresses { get; set; } + + public static bool operator ==(Redelegation obj, Redelegation other) + { + return obj.Equals(other); + } + + public static bool operator !=(Redelegation obj, Redelegation other) + { + return !(obj == other); + } + + public static Address DeriveAddress( + Address delegatorAddress, Address srcValidatorAddress, Address dstValidatorAddress) + { + return delegatorAddress + .Derive(srcValidatorAddress.ToByteArray()) + .Derive(dstValidatorAddress.ToByteArray()) + .Derive("Redelegation"); + } + + public IValue Serialize() + { + List serializedRedelegationEntryAddresses = List.Empty; + foreach ( + KeyValuePair redelegationEntryAddressKV + in RedelegationEntryAddresses) + { + serializedRedelegationEntryAddresses = + serializedRedelegationEntryAddresses.Add( + List.Empty + .Add(redelegationEntryAddressKV.Key.Serialize()) + .Add(redelegationEntryAddressKV.Value.Serialize())); + } + + return List.Empty + .Add(Address.Serialize()) + .Add(DelegatorAddress.Serialize()) + .Add(SrcValidatorAddress.Serialize()) + .Add(DstValidatorAddress.Serialize()) + .Add(RedelegationEntryIndex.Serialize()) + .Add(serializedRedelegationEntryAddresses); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Redelegation); + } + + public bool Equals(Redelegation? other) + { + return !(other is null) && + Address.Equals(other.Address) && + DelegatorAddress.Equals(other.DelegatorAddress) && + SrcValidatorAddress.Equals(other.SrcValidatorAddress) && + DstValidatorAddress.Equals(other.DstValidatorAddress) && + SrcDelegationAddress.Equals(other.SrcDelegationAddress) && + DstDelegationAddress.Equals(other.DstDelegationAddress) && + RedelegationEntryIndex == other.RedelegationEntryIndex && + RedelegationEntryAddresses.SequenceEqual(other.RedelegationEntryAddresses); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c.DPoS/Model/RedelegationEntry.cs b/Lib9c.DPoS/Model/RedelegationEntry.cs new file mode 100644 index 0000000000..256f9d979c --- /dev/null +++ b/Lib9c.DPoS/Model/RedelegationEntry.cs @@ -0,0 +1,147 @@ +using Bencodex.Types; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Model +{ + public class RedelegationEntry : IEquatable + { + private FungibleAssetValue _redelegatingShare; + private FungibleAssetValue _unbondingConsensusToken; + private FungibleAssetValue _issuedShare; + + public RedelegationEntry( + Address redelegationAddress, + FungibleAssetValue redelegatingShare, + FungibleAssetValue unbondingConsensusToken, + FungibleAssetValue issuedShare, + long index, + long blockHeight) + { + Address = DeriveAddress(redelegationAddress, index); + RedelegationAddress = redelegationAddress; + RedelegatingShare = redelegatingShare; + UnbondingConsensusToken = unbondingConsensusToken; + IssuedShare = issuedShare; + Index = index; + CompletionBlockHeight = blockHeight + UnbondingSet.Period; + } + + public RedelegationEntry(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + RedelegationAddress = serializedList[1].ToAddress(); + RedelegatingShare = serializedList[2].ToFungibleAssetValue(); + UnbondingConsensusToken = serializedList[3].ToFungibleAssetValue(); + IssuedShare = serializedList[4].ToFungibleAssetValue(); + Index = serializedList[5].ToLong(); + CompletionBlockHeight = serializedList[6].ToLong(); + } + + public Address Address { get; set; } + + public Address RedelegationAddress { get; set; } + + public FungibleAssetValue RedelegatingShare + { + get => _redelegatingShare; + set + { + if (!value.Currency.Equals(Asset.Share)) + { + throw new InvalidCurrencyException(Asset.Share, value.Currency); + } + + _redelegatingShare = value; + } + } + + public FungibleAssetValue UnbondingConsensusToken + { + get => _unbondingConsensusToken; + set + { + if (!value.Currency.Equals(Asset.ConsensusToken)) + { + throw new InvalidCurrencyException(Asset.ConsensusToken, value.Currency); + } + + _unbondingConsensusToken = value; + } + } + + public FungibleAssetValue IssuedShare + { + get => _issuedShare; + set + { + if (!value.Currency.Equals(Asset.Share)) + { + throw new InvalidCurrencyException(Asset.Share, value.Currency); + } + + _issuedShare = value; + } + } + + public long Index { get; set; } + + public long CompletionBlockHeight { get; set; } + + public static bool operator ==(RedelegationEntry obj, RedelegationEntry other) + { + return obj.Equals(other); + } + + public static bool operator !=(RedelegationEntry obj, RedelegationEntry other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address redelegationAddress, long index) + { + return redelegationAddress.Derive($"RedelegationEntry{index}"); + } + + public bool IsMatured(long blockHeight) => blockHeight >= CompletionBlockHeight; + + public IValue Serialize() + { + return List.Empty + .Add(Address.Serialize()) + .Add(RedelegationAddress.Serialize()) + .Add(RedelegatingShare.Serialize()) + .Add(UnbondingConsensusToken.Serialize()) + .Add(IssuedShare.Serialize()) + .Add(Index.Serialize()) + .Add(CompletionBlockHeight.Serialize()); + } + + public override bool Equals(object? obj) + { + return Equals(obj as RedelegationEntry); + } + + public bool Equals(RedelegationEntry? other) + { + return !(other is null) && + Address.Equals(other.Address) && + RedelegationAddress.Equals(other.RedelegationAddress) && + RedelegatingShare.Equals(other.RedelegatingShare) && + UnbondingConsensusToken.Equals(other.UnbondingConsensusToken) && + IssuedShare.Equals(other.IssuedShare) && + Index == other.Index && + CompletionBlockHeight == other.CompletionBlockHeight; + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c.DPoS/Model/UnbondingSet.cs b/Lib9c.DPoS/Model/UnbondingSet.cs new file mode 100644 index 0000000000..41d7d01a9a --- /dev/null +++ b/Lib9c.DPoS/Model/UnbondingSet.cs @@ -0,0 +1,59 @@ +using Bencodex.Types; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Model +{ + public class UnbondingSet + { + public UnbondingSet() + { + ValidatorAddressSet = new SortedSet
(); + UndelegationAddressSet = new SortedSet
(); + RedelegationAddressSet = new SortedSet
(); + } + + public UnbondingSet(IValue serialized) + { + List serializedList = (List)serialized; + ValidatorAddressSet + = new SortedSet
( + ((List)serializedList[0]).Select(x => x.ToAddress())); + UndelegationAddressSet + = new SortedSet
( + ((List)serializedList[1]).Select(x => x.ToAddress())); + RedelegationAddressSet + = new SortedSet
( + ((List)serializedList[2]).Select(x => x.ToAddress())); + } + + public UnbondingSet(UnbondingSet unbondingSet) + { + ValidatorAddressSet = unbondingSet.ValidatorAddressSet; + UndelegationAddressSet = unbondingSet.UndelegationAddressSet; + RedelegationAddressSet = unbondingSet.RedelegationAddressSet; + } + + public static long Period => 50400 * 4; + + public SortedSet
ValidatorAddressSet { get; set; } + + public SortedSet
UndelegationAddressSet { get; set; } + + public SortedSet
RedelegationAddressSet { get; set; } + + public Address Address => ReservedAddress.UnbondingSet; + + public IValue Serialize() + { + return List.Empty + .Add(new List(ValidatorAddressSet.Select( + address => address.Serialize()))) + .Add(new List(UndelegationAddressSet.Select( + address => address.Serialize()))) + .Add(new List(RedelegationAddressSet.Select( + address => address.Serialize()))); + } + } +} diff --git a/Lib9c.DPoS/Model/Undelegation.cs b/Lib9c.DPoS/Model/Undelegation.cs new file mode 100644 index 0000000000..678658557c --- /dev/null +++ b/Lib9c.DPoS/Model/Undelegation.cs @@ -0,0 +1,125 @@ +using Bencodex.Types; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Model +{ + public class Undelegation : IEquatable + { + public Undelegation(Address delegatorAddress, Address validatorAddress) + { + Address = DeriveAddress(delegatorAddress, validatorAddress); + DelegatorAddress = delegatorAddress; + ValidatorAddress = validatorAddress; + UndelegationEntryIndex = 0; + UndelegationEntryAddresses = new SortedList(); + } + + public Undelegation(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + DelegatorAddress = serializedList[1].ToAddress(); + ValidatorAddress = serializedList[2].ToAddress(); + UndelegationEntryIndex = serializedList[3].ToLong(); + UndelegationEntryAddresses = new SortedList(); + foreach ( + IValue serializedUndelegationEntryAddress + in (List)serializedList[4]) + { + List items = (List)serializedUndelegationEntryAddress; + UndelegationEntryAddresses.Add(items[0].ToLong(), items[1].ToAddress()); + } + } + + public Undelegation(Undelegation undelegation) + { + Address = undelegation.Address; + DelegatorAddress = undelegation.DelegatorAddress; + ValidatorAddress = undelegation.ValidatorAddress; + UndelegationEntryIndex = undelegation.UndelegationEntryIndex; + UndelegationEntryAddresses = undelegation.UndelegationEntryAddresses; + } + + // TODO: Better structure + // This hard coding will cause some problems when it's modified + // May be it would be better to be serialized + public static int MaximumUndelegationEntries { get => 10; } + + public Address Address { get; } + + public Address DelegatorAddress { get; } + + public Address ValidatorAddress { get; } + + public Address DelegationAddress + { + get => Delegation.DeriveAddress(DelegatorAddress, ValidatorAddress); + } + + public long UndelegationEntryIndex { get; set; } + + public SortedList UndelegationEntryAddresses { get; set; } + + public static bool operator ==(Undelegation obj, Undelegation other) + { + return obj.Equals(other); + } + + public static bool operator !=(Undelegation obj, Undelegation other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address delegatorAddress, Address validatorAddress) + { + return delegatorAddress + .Derive(validatorAddress.ToByteArray()) + .Derive("Undelegation"); + } + + public IValue Serialize() + { + List serializedUndelegationEntryAddresses = List.Empty; + foreach ( + KeyValuePair undelegationEntryAddressKV + in UndelegationEntryAddresses) + { + serializedUndelegationEntryAddresses = + serializedUndelegationEntryAddresses.Add( + List.Empty + .Add(undelegationEntryAddressKV.Key.Serialize()) + .Add(undelegationEntryAddressKV.Value.Serialize())); + } + + return List.Empty + .Add(Address.Serialize()) + .Add(DelegatorAddress.Serialize()) + .Add(ValidatorAddress.Serialize()) + .Add(UndelegationEntryIndex.Serialize()) + .Add(serializedUndelegationEntryAddresses); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Undelegation); + } + + public bool Equals(Undelegation? other) + { + return !(other is null) && + Address.Equals(other.Address) && + DelegatorAddress.Equals(other.DelegatorAddress) && + ValidatorAddress.Equals(other.ValidatorAddress) && + DelegationAddress.Equals(other.DelegationAddress) && + UndelegationEntryIndex == other.UndelegationEntryIndex && + UndelegationEntryAddresses.SequenceEqual(other.UndelegationEntryAddresses); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c.DPoS/Model/UndelegationEntry.cs b/Lib9c.DPoS/Model/UndelegationEntry.cs new file mode 100644 index 0000000000..34b13f0867 --- /dev/null +++ b/Lib9c.DPoS/Model/UndelegationEntry.cs @@ -0,0 +1,107 @@ +using Bencodex.Types; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Model +{ + public class UndelegationEntry : IEquatable + { + private FungibleAssetValue _unbondingConsensusToken; + + public UndelegationEntry( + Address undelegationAddress, + FungibleAssetValue unbondingConsensusToken, + long index, + long blockHeight) + { + Address = DeriveAddress(undelegationAddress, index); + UndelegationAddress = undelegationAddress; + UnbondingConsensusToken = unbondingConsensusToken; + Index = index; + CompletionBlockHeight = blockHeight + UnbondingSet.Period; + } + + public UndelegationEntry(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + UndelegationAddress = serializedList[1].ToAddress(); + UnbondingConsensusToken = serializedList[2].ToFungibleAssetValue(); + Index = serializedList[3].ToLong(); + CompletionBlockHeight = serializedList[4].ToLong(); + } + + public Address Address { get; set; } + + public Address UndelegationAddress { get; set; } + + public FungibleAssetValue UnbondingConsensusToken + { + get => _unbondingConsensusToken; + set + { + if (!value.Currency.Equals(Asset.ConsensusToken)) + { + throw new InvalidCurrencyException(Asset.ConsensusToken, value.Currency); + } + + _unbondingConsensusToken = value; + } + } + + public long Index { get; set; } + + public long CompletionBlockHeight { get; set; } + + public static bool operator ==(UndelegationEntry obj, UndelegationEntry other) + { + return obj.Equals(other); + } + + public static bool operator !=(UndelegationEntry obj, UndelegationEntry other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address undelegationAddress, long index) + { + return undelegationAddress.Derive($"UndelegationEntry{index}"); + } + + public bool IsMatured(long blockHeight) => blockHeight >= CompletionBlockHeight; + + public IValue Serialize() + { + return List.Empty + .Add(Address.Serialize()) + .Add(UndelegationAddress.Serialize()) + .Add(UnbondingConsensusToken.Serialize()) + .Add(Index.Serialize()) + .Add(CompletionBlockHeight.Serialize()); + } + + public override bool Equals(object? obj) + { + return Equals(obj as UndelegationEntry); + } + + public bool Equals(UndelegationEntry? other) + { + return !(other is null) && + Address.Equals(other.Address) && + UndelegationAddress.Equals(other.UndelegationAddress) && + UnbondingConsensusToken.Equals(other.UnbondingConsensusToken) && + Index == other.Index && + CompletionBlockHeight == other.CompletionBlockHeight; + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c.DPoS/Model/Validator.cs b/Lib9c.DPoS/Model/Validator.cs new file mode 100644 index 0000000000..da84e75b83 --- /dev/null +++ b/Lib9c.DPoS/Model/Validator.cs @@ -0,0 +1,134 @@ +using System.Numerics; +using Bencodex.Types; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Model +{ + public class Validator : IEquatable + { + private FungibleAssetValue _delegatorShares; + + public Validator( + Address operatorAddress, + PublicKey operatorPublicKey) + { + Address = DeriveAddress(operatorAddress); + OperatorAddress = operatorAddress; + OperatorPublicKey = operatorPublicKey; + Jailed = false; + Status = BondingStatus.Unbonded; + UnbondingCompletionBlockHeight = -1; + DelegatorShares = Asset.Share * 0; + } + + public Validator(IValue serialized) + { + var dict = (Bencodex.Types.Dictionary)serialized; + Address = dict["addr"].ToAddress(); + OperatorAddress = dict["op_addr"].ToAddress(); + OperatorPublicKey = dict["op_pub"].ToPublicKey(); + Jailed = dict["jailed"].ToBoolean(); + Status = dict["status"].ToEnum(); + UnbondingCompletionBlockHeight = dict["unbonding"].ToLong(); + DelegatorShares = dict["shares"].ToFungibleAssetValue(); + } + + // TODO: Better structure + // This hard coding will cause some problems when it's modified + // May be it would be better to be serialized + public static FungibleAssetValue MinSelfDelegation => Asset.ConsensusToken * 1; + + public static BigInteger CommissionNumer => 1; + + public static BigInteger CommissionDenom => 10; + + public static double CommissionMaxRate => 0.2; + + public static double CommissionMaxChangeRate => 0.01; + + public Address Address { get; set; } + + public Address OperatorAddress { get; set; } + + public PublicKey OperatorPublicKey { get; set; } + + public bool Jailed { get; set; } + + public BondingStatus Status { get; set; } + + public long UnbondingCompletionBlockHeight { get; set; } + + public FungibleAssetValue DelegatorShares + { + get => _delegatorShares; + set + { + if (!value.Currency.Equals(Asset.Share)) + { + throw new InvalidCurrencyException(Asset.Share, value.Currency); + } + + _delegatorShares = value; + } + } + + public static bool operator ==(Validator obj, Validator other) + { + return obj.Equals(other); + } + + public static bool operator !=(Validator obj, Validator other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address operatorAddress) + { + return operatorAddress.Derive("ValidatorAddress"); + } + + public bool IsMatured(long blockHeight) + => UnbondingCompletionBlockHeight > 0 + && Status != BondingStatus.Bonded + && blockHeight >= UnbondingCompletionBlockHeight; + + public IValue Serialize() + { + return Dictionary.Empty + .Add("addr", Address.Serialize()) + .Add("op_addr", OperatorAddress.Serialize()) + .Add("op_pub", OperatorPublicKey.Serialize()) + .Add("jailed", Jailed.Serialize()) + .Add("status", Status.Serialize()) + .Add("unbonding", UnbondingCompletionBlockHeight.Serialize()) + .Add("shares", DelegatorShares.Serialize()); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Validator); + } + + public bool Equals(Validator? other) + { + return !(other is null) && + Address.Equals(other.Address) && + OperatorAddress.Equals(other.OperatorAddress) && + OperatorPublicKey.Equals(other.OperatorPublicKey) && + Jailed == other.Jailed && + Status == other.Status && + UnbondingCompletionBlockHeight == other.UnbondingCompletionBlockHeight && + DelegatorShares.Equals(other.DelegatorShares); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c.DPoS/Model/ValidatorDelegationSet.cs b/Lib9c.DPoS/Model/ValidatorDelegationSet.cs new file mode 100644 index 0000000000..f24c77654a --- /dev/null +++ b/Lib9c.DPoS/Model/ValidatorDelegationSet.cs @@ -0,0 +1,63 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Util; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Model +{ + public class ValidatorDelegationSet + { + private readonly SortedSet
_set; + + public ValidatorDelegationSet(Address validatorAddress) + { + Address = DeriveAddress(validatorAddress); + ValidatorAddress = validatorAddress; + _set = new SortedSet
(); + } + + public ValidatorDelegationSet(IValue serialized) + { + var dict = (Dictionary)serialized; + Address = dict["addr"].ToAddress(); + ValidatorAddress = dict["val_addr"].ToAddress(); + _set = new SortedSet
(((List)dict["set"]).Select(x => x.ToAddress())); + } + + public ValidatorDelegationSet(ValidatorDelegationSet bondedValidatorSet) + { + Address = bondedValidatorSet.Address; + ValidatorAddress = bondedValidatorSet.ValidatorAddress; + _set = bondedValidatorSet._set; + } + + public Address Address { get; } + + public Address ValidatorAddress { get; } + + public ImmutableSortedSet
Set => _set.ToImmutableSortedSet(); + + public static Address DeriveAddress(Address validatorAddress) + { + return validatorAddress.Derive("ValidatorDelegationSetAddress"); + } + + public void Add(Address delegationAddress) + { + _set.Add(delegationAddress); + } + + public void Remove(Address delegationAddress) + { + _set.Remove(delegationAddress); + } + + public IValue Serialize() + { + return Dictionary.Empty + .Add("addr", Address.Serialize()) + .Add("val_addr", ValidatorAddress.Serialize()) + .Add("set", new List(Set.Select(x => x.Serialize()))); + } + } +} diff --git a/Lib9c.DPoS/Model/ValidatorPower.cs b/Lib9c.DPoS/Model/ValidatorPower.cs new file mode 100644 index 0000000000..d1b06da2f3 --- /dev/null +++ b/Lib9c.DPoS/Model/ValidatorPower.cs @@ -0,0 +1,111 @@ +using Bencodex.Types; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Model +{ + public class ValidatorPower + : IEquatable, IComparable, IComparable + { + private FungibleAssetValue _consensusToken; + + public ValidatorPower( + Address validatorAddress, + PublicKey operatorPublicKey, + FungibleAssetValue consensusToken) + { + ValidatorAddress = validatorAddress; + OperatorPublicKey = operatorPublicKey; + ConsensusToken = consensusToken; + } + + public ValidatorPower(IValue serialized) + { + List serializedList = (List)serialized; + ValidatorAddress = serializedList[0].ToAddress(); + OperatorPublicKey = serializedList[1].ToPublicKey(); + ConsensusToken = serializedList[2].ToFungibleAssetValue(); + } + + public Address ValidatorAddress { get; set; } + + public PublicKey OperatorPublicKey { get; set; } + + public FungibleAssetValue ConsensusToken + { + get => _consensusToken; + set + { + if (!value.Currency.Equals(Asset.ConsensusToken)) + { + throw new InvalidCurrencyException(Asset.ConsensusToken, value.Currency); + } + + _consensusToken = value; + } + } + + public static bool operator ==(ValidatorPower obj, ValidatorPower other) + { + return obj.Equals(other); + } + + public static bool operator !=(ValidatorPower obj, ValidatorPower other) + { + return !(obj == other); + } + + public IValue Serialize() => List.Empty + .Add(ValidatorAddress.Serialize()) + .Add(OperatorPublicKey.Serialize()) + .Add(ConsensusToken.Serialize()); + + public override bool Equals(object? obj) + { + return Equals(obj as ValidatorPower); + } + + public bool Equals(ValidatorPower? other) + { + return !(other is null) && + _consensusToken.Equals(other._consensusToken) && + ValidatorAddress.Equals(other.ValidatorAddress) && + OperatorPublicKey.Equals(other.OperatorPublicKey) && + ConsensusToken.Equals(other.ConsensusToken); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(ValidatorAddress.ToByteArray()); + } + + int IComparable.CompareTo(ValidatorPower? other) + { + int result + = ConsensusToken.Equals(other?.ConsensusToken) + ? ((IComparable
)ValidatorAddress).CompareTo(other.ValidatorAddress) + : ConsensusToken.CompareTo(other?.ConsensusToken); + + return -result; + } + + int IComparable.CompareTo(object? obj) + { + if (obj is ValidatorPower other) + { + return ((IComparable)this).CompareTo(other); + } + + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + throw new ArgumentException(nameof(obj)); + } + } +} diff --git a/Lib9c.DPoS/Model/ValidatorPowerIndex.cs b/Lib9c.DPoS/Model/ValidatorPowerIndex.cs new file mode 100644 index 0000000000..7c646ef1fd --- /dev/null +++ b/Lib9c.DPoS/Model/ValidatorPowerIndex.cs @@ -0,0 +1,36 @@ +using Bencodex.Types; +using Lib9c.DPoS.Misc; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Model +{ + public class ValidatorPowerIndex + { + public ValidatorPowerIndex() + { + Index = new SortedSet(); + } + + public ValidatorPowerIndex(IValue serialized) + { + IEnumerable items + = ((List)serialized).Select(item => new ValidatorPower(item)); + Index = new SortedSet(items); + } + + public ValidatorPowerIndex(ValidatorPowerIndex consensusPowerIndexInfo) + { + Index = consensusPowerIndexInfo.Index; + } + + public SortedSet Index { get; set; } + + public Address Address => ReservedAddress.ValidatorPowerIndex; + + public List
ValidatorAddresses + => Index.Select(key => key.ValidatorAddress).ToList(); + + public IValue Serialize() + => new List(Index.Select(consensusPowerKey => consensusPowerKey.Serialize())); + } +} diff --git a/Lib9c.DPoS/Model/ValidatorRewards.cs b/Lib9c.DPoS/Model/ValidatorRewards.cs new file mode 100644 index 0000000000..8707f5c852 --- /dev/null +++ b/Lib9c.DPoS/Model/ValidatorRewards.cs @@ -0,0 +1,87 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Util; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Model +{ + public class ValidatorRewards + { + private readonly SortedList _rewards; + + public ValidatorRewards(Address validatorAddress, Currency currency) + { + Address = DeriveAddress(validatorAddress, currency); + ValidatorAddress = validatorAddress; + Currency = currency; + _rewards = new SortedList(); + } + + public ValidatorRewards(IValue serialized) + { + var dict = (Dictionary)serialized; + Address = dict["addr"].ToAddress(); + ValidatorAddress = dict["val_addr"].ToAddress(); + Currency = dict["currency"].ToCurrency(); + _rewards = new SortedList(); + foreach ( + KeyValuePair kv + in (Dictionary)dict["rewards"]) + { + _rewards.Add(kv.Key.ToLong(), kv.Value.ToFungibleAssetValue()); + } + } + + public ValidatorRewards(ValidatorRewards validatorRewards) + { + Address = validatorRewards.Address; + ValidatorAddress = validatorRewards.ValidatorAddress; + Currency = validatorRewards.Currency; + _rewards = validatorRewards._rewards; + } + + public Address Address { get; } + + public Address ValidatorAddress { get; } + + public Currency Currency { get; } + + public ImmutableSortedDictionary Rewards + => _rewards.ToImmutableSortedDictionary(); + + public static Address DeriveAddress(Address validatorAddress, Currency currency) + { + return validatorAddress.Derive("ValidatorRewardsAddress").Derive(currency.Ticker); + } + + public void Add(long blockHeight, FungibleAssetValue reward) + { + if (!reward.Currency.Equals(Currency)) + { + throw new InvalidCurrencyException(Currency, reward.Currency); + } + + _rewards.Add(blockHeight, reward); + } + + public IValue Serialize() + { + Dictionary serializedRewards = Dictionary.Empty; + foreach ( + KeyValuePair rewards in Rewards) + { + serializedRewards + = (Dictionary)serializedRewards + .Add((IKey)rewards.Key.Serialize(), rewards.Value.Serialize()); + } + + return Dictionary.Empty + .Add("addr", Address.Serialize()) + .Add("val_addr", ValidatorAddress.Serialize()) + .Add("currency", Currency.Serialize()) + .Add("rewards", serializedRewards); + } + } +} diff --git a/Lib9c.DPoS/Model/ValidatorSet.cs b/Lib9c.DPoS/Model/ValidatorSet.cs new file mode 100644 index 0000000000..dd211f039d --- /dev/null +++ b/Lib9c.DPoS/Model/ValidatorSet.cs @@ -0,0 +1,58 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Misc; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Model +{ + public class ValidatorSet + { + private readonly SortedSet _set; + + public ValidatorSet() + { + _set = new SortedSet(); + } + + public ValidatorSet(IValue serialized) + { + IEnumerable validatorPowerEnum + = ((List)serialized).Select(x => new ValidatorPower(x)); + _set = new SortedSet(validatorPowerEnum); + } + + public ValidatorSet(ValidatorSet bondedValidatorSet) + { + _set = bondedValidatorSet._set; + } + + public static int MaxBondedSetSize => 100; + + public static Address PreviousBondedAddress => ReservedAddress.PreviousBondedValidatorSet; + + public static Address BondedAddress => ReservedAddress.BondedValidatorSet; + + public static Address UnbondedAddress => ReservedAddress.UnbondedValidatorSet; + + public long Count => _set.Count; + + public ImmutableSortedSet Set => _set.ToImmutableSortedSet(); + + public FungibleAssetValue TotalConsensusToken + => Set.Aggregate( + Asset.ConsensusToken * 0, (total, next) => total + next.ConsensusToken); + + public ValidatorPower this[int index] => _set.ElementAt(index); + + public void Add(ValidatorPower validatorPower) + { + _set.Add(validatorPower); + } + + public IValue Serialize() + { + return new List(_set.Select(x => x.Serialize())); + } + } +} diff --git a/Lib9c.DPoS/Util/AddressHelper.cs b/Lib9c.DPoS/Util/AddressHelper.cs new file mode 100644 index 0000000000..210e27144b --- /dev/null +++ b/Lib9c.DPoS/Util/AddressHelper.cs @@ -0,0 +1,25 @@ +using System.Security.Cryptography; +using System.Text; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Util +{ + internal static class AddressHelper + { + public static Address Derive(this Address address, byte[] key) + { + var bytes = address.ToByteArray(); + byte[] hashed; + + using (var hmac = new HMACSHA1(key)) + { + hashed = hmac.ComputeHash(bytes); + } + + return new Address(hashed); + } + + public static Address Derive(this Address address, string key) + => address.Derive(Encoding.UTF8.GetBytes(key)); + } +} diff --git a/Lib9c.DPoS/Util/MarshalHelper.cs b/Lib9c.DPoS/Util/MarshalHelper.cs new file mode 100644 index 0000000000..f084ab3d0d --- /dev/null +++ b/Lib9c.DPoS/Util/MarshalHelper.cs @@ -0,0 +1,82 @@ +using System.Collections.Immutable; +using System.Globalization; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Util +{ + internal static class MarshalHelper + { + public static IValue Serialize(this Address address) => + new Binary(address.ByteArray); + + public static IValue Serialize(this PublicKey publicKey) => + new Binary(publicKey.Format(false)); + + public static IValue Serialize(this bool boolean) => + new Bencodex.Types.Boolean(boolean); + + public static IValue Serialize(this int number) => + (Text)number.ToString(CultureInfo.InvariantCulture); + + public static IValue Serialize(this long number) => + (Text)number.ToString(CultureInfo.InvariantCulture); + + public static IValue Serialize(this double number) => + (Text)number.ToString(CultureInfo.InvariantCulture); + + public static IValue Serialize(this BigInteger number) => + (Bencodex.Types.Integer)number; + + public static IValue Serialize(this Enum type) => (Text)type.ToString(); + + public static IValue Serialize(this Guid number) => + new Binary(number.ToByteArray()); + + public static IValue Serialize(this FungibleAssetValue fungibleAssetValue) + { + return Dictionary.Empty + .Add("currency", fungibleAssetValue.Currency.Serialize()) + .Add("majorUnit", fungibleAssetValue.MajorUnit.Serialize()) + .Add("minorUnit", fungibleAssetValue.MinorUnit.Serialize()); + } + + public static Address ToAddress(this IValue serialized) => + new Address((Binary)serialized); + + public static PublicKey ToPublicKey(this IValue serialized) => + new PublicKey(((Binary)serialized).ToImmutableArray()); + + public static bool ToBoolean(this IValue serialized) => + ((Bencodex.Types.Boolean)serialized).Value; + + public static int ToInteger(this IValue serialized) => + int.Parse(((Text)serialized).Value, CultureInfo.InvariantCulture); + + public static long ToLong(this IValue serialized) => + long.Parse(((Text)serialized).Value, CultureInfo.InvariantCulture); + + public static double ToDouble(this IValue serialized) => + double.Parse(((Text)serialized).Value, CultureInfo.InvariantCulture); + + public static BigInteger ToBigInteger(this IValue serialized) => + ((Integer)serialized).Value; + + public static T ToEnum(this IValue serialized) + where T : struct + { + return (T)Enum.Parse(typeof(T), (Text)serialized); + } + + public static Guid ToGuid(this IValue serialized) => + new Guid(((Binary)serialized).ToByteArray()); + + public static Currency ToCurrency(this IValue serialized) => + new Currency(serialized); + + public static FungibleAssetValue ToFungibleAssetValue(this IValue serialized) => + new FungibleAssetValue(serialized); + } +} diff --git a/Lib9c.sln b/Lib9c.sln index 79f6d8b418..c19d8af968 100644 --- a/Lib9c.sln +++ b/Lib9c.sln @@ -70,6 +70,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib9c.Plugin", ".Lib9c.Plug EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib9c.Plugin.Shared", ".Lib9c.Plugin.Shared\Lib9c.Plugin.Shared.csproj", "{76F6C25E-94D2-4EA9-B88D-0249F44D1D16}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib9c.DPoS", "Lib9c.DPoS\Lib9c.DPoS.csproj", "{23848A39-37DD-4B73-A108-1915E10FEE50}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -196,6 +198,10 @@ Global {76F6C25E-94D2-4EA9-B88D-0249F44D1D16}.Debug|Any CPU.Build.0 = Debug|Any CPU {76F6C25E-94D2-4EA9-B88D-0249F44D1D16}.Release|Any CPU.ActiveCfg = Release|Any CPU {76F6C25E-94D2-4EA9-B88D-0249F44D1D16}.Release|Any CPU.Build.0 = Release|Any CPU + {23848A39-37DD-4B73-A108-1915E10FEE50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23848A39-37DD-4B73-A108-1915E10FEE50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23848A39-37DD-4B73-A108-1915E10FEE50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23848A39-37DD-4B73-A108-1915E10FEE50}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 11ec77efc92c545790d8767ea078f4854bd30c77 Mon Sep 17 00:00:00 2001 From: Chanhyuck Ko Date: Wed, 20 Mar 2024 15:17:16 +0900 Subject: [PATCH 16/16] test: add unit tests --- .Lib9c.Tests/Action/ActionContext.cs | 2 + Lib9c.DPoS.Tests/ActionContext.cs | 60 +++ Lib9c.DPoS.Tests/Control/DelegateCtrlTest.cs | 193 +++++++++ .../Control/RedelegateCtrlTest.cs | 375 +++++++++++++++++ .../Control/UndelegateCtrlTest.cs | 393 ++++++++++++++++++ Lib9c.DPoS.Tests/Control/ValidatorCtrlTest.cs | 124 ++++++ .../Control/ValidatorPowerIndexCtrlTest.cs | 186 +++++++++ .../Control/ValidatorSetCtrlTest.cs | 221 ++++++++++ Lib9c.DPoS.Tests/DistributeTest.cs | 259 ++++++++++++ Lib9c.DPoS.Tests/Lib9c.DPoS.Tests.csproj | 34 ++ Lib9c.DPoS.Tests/Model/DelegationTest.cs | 23 + .../Model/RedelegationEntryTest.cs | 48 +++ Lib9c.DPoS.Tests/Model/RedelegationTest.cs | 24 ++ .../Model/UndelegationEntryTest.cs | 35 ++ Lib9c.DPoS.Tests/Model/UndelegationTest.cs | 23 + .../Model/ValidatorPowerIndexTest.cs | 23 + Lib9c.DPoS.Tests/Model/ValidatorPowerTest.cs | 38 ++ Lib9c.DPoS.Tests/Model/ValidatorSetTest.cs | 25 ++ Lib9c.DPoS.Tests/Model/ValidatorTest.cs | 34 ++ Lib9c.DPoS.Tests/PoSTest.cs | 20 + Lib9c.DPoS.Tests/TestRandom.cs | 41 ++ .../ValidatorPowerComparerTest.cs | 39 ++ Lib9c.DPoS/AssemblyInfo.cs | 3 + Lib9c.sln | 6 + 24 files changed, 2229 insertions(+) create mode 100644 Lib9c.DPoS.Tests/ActionContext.cs create mode 100644 Lib9c.DPoS.Tests/Control/DelegateCtrlTest.cs create mode 100644 Lib9c.DPoS.Tests/Control/RedelegateCtrlTest.cs create mode 100644 Lib9c.DPoS.Tests/Control/UndelegateCtrlTest.cs create mode 100644 Lib9c.DPoS.Tests/Control/ValidatorCtrlTest.cs create mode 100644 Lib9c.DPoS.Tests/Control/ValidatorPowerIndexCtrlTest.cs create mode 100644 Lib9c.DPoS.Tests/Control/ValidatorSetCtrlTest.cs create mode 100644 Lib9c.DPoS.Tests/DistributeTest.cs create mode 100644 Lib9c.DPoS.Tests/Lib9c.DPoS.Tests.csproj create mode 100644 Lib9c.DPoS.Tests/Model/DelegationTest.cs create mode 100644 Lib9c.DPoS.Tests/Model/RedelegationEntryTest.cs create mode 100644 Lib9c.DPoS.Tests/Model/RedelegationTest.cs create mode 100644 Lib9c.DPoS.Tests/Model/UndelegationEntryTest.cs create mode 100644 Lib9c.DPoS.Tests/Model/UndelegationTest.cs create mode 100644 Lib9c.DPoS.Tests/Model/ValidatorPowerIndexTest.cs create mode 100644 Lib9c.DPoS.Tests/Model/ValidatorPowerTest.cs create mode 100644 Lib9c.DPoS.Tests/Model/ValidatorSetTest.cs create mode 100644 Lib9c.DPoS.Tests/Model/ValidatorTest.cs create mode 100644 Lib9c.DPoS.Tests/PoSTest.cs create mode 100644 Lib9c.DPoS.Tests/TestRandom.cs create mode 100644 Lib9c.DPoS.Tests/ValidatorPowerComparerTest.cs create mode 100644 Lib9c.DPoS/AssemblyInfo.cs diff --git a/.Lib9c.Tests/Action/ActionContext.cs b/.Lib9c.Tests/Action/ActionContext.cs index 9c157693b4..1afdbc9f7e 100644 --- a/.Lib9c.Tests/Action/ActionContext.cs +++ b/.Lib9c.Tests/Action/ActionContext.cs @@ -28,6 +28,8 @@ public class ActionContext : IActionContext public int BlockProtocolVersion { get; set; } + public BlockCommit LastCommit { get; set; } + public IWorld PreviousState { get; set; } public int RandomSeed { get; set; } diff --git a/Lib9c.DPoS.Tests/ActionContext.cs b/Lib9c.DPoS.Tests/ActionContext.cs new file mode 100644 index 0000000000..7bb511d6f9 --- /dev/null +++ b/Lib9c.DPoS.Tests/ActionContext.cs @@ -0,0 +1,60 @@ +#nullable disable + +using System.Security.Cryptography; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Blocks; +using Libplanet.Types.Tx; + +namespace Lib9c.DPoS.Tests +{ + public class ActionContext : IActionContext + { + private long _gasUsed; + + private IRandom _random = null; + + public BlockHash? GenesisHash { get; set; } + + public Address Signer { get; set; } + + public TxId? TxId { get; set; } + + public Address Miner { get; set; } + + public BlockHash BlockHash { get; set; } + + public long BlockIndex { get; set; } + + public int BlockProtocolVersion { get; set; } + + public BlockCommit LastCommit { get; set; } + + public IWorld PreviousState { get; set; } + + public int RandomSeed { get; set; } + + public HashDigest? PreviousStateRootHash { get; set; } + + public bool BlockAction { get; } + + public void UseGas(long gas) + { + _gasUsed += gas; + } + + public IRandom GetRandom() => _random ?? new TestRandom(RandomSeed); + + public long GasUsed() => _gasUsed; + + public long GasLimit() => 0; + + // FIXME: Temporary measure to allow inheriting already mutated IRandom. + public void SetRandom(IRandom random) + { + _random = random; + } + } +} diff --git a/Lib9c.DPoS.Tests/Control/DelegateCtrlTest.cs b/Lib9c.DPoS.Tests/Control/DelegateCtrlTest.cs new file mode 100644 index 0000000000..d0fff28aaf --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/DelegateCtrlTest.cs @@ -0,0 +1,193 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + public class DelegateCtrlTest : PoSTest + { + private readonly PublicKey _operatorPublicKey; + private readonly Address _operatorAddress; + private readonly Address _delegatorAddress; + private readonly Address _validatorAddress; + private ImmutableHashSet _nativeTokens; + private IWorld _states; + + public DelegateCtrlTest() + { + _operatorPublicKey = new PrivateKey().PublicKey; + _operatorAddress = _operatorPublicKey.Address; + _delegatorAddress = CreateAddress(); + _validatorAddress = Validator.DeriveAddress(_operatorAddress); + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + } + + [Fact] + public void InvalidCurrencyTest() + { + Initialize(500, 500, 100); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.ConsensusToken * 50); + Assert.Throws( + () => _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.ConsensusToken * 30, + _nativeTokens)); + } + + [Fact] + public void InvalidValidatorTest() + { + Initialize(500, 500, 100); + Assert.Throws( + () => _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + CreateAddress(), + Asset.GovernanceToken * 10, + _nativeTokens)); + } + + [Fact] + public void InvalidShareTest() + { + Initialize(500, 500, 100); + _states = _states.BurnAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _validatorAddress, + Asset.ConsensusToken * 100); + Assert.Throws( + () => _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.GovernanceToken * 10, + _nativeTokens)); + } + + [Theory] + [InlineData(500, 500, 100, 10)] + [InlineData(500, 500, 100, 20)] + public void BalanceTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount); + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.GovernanceToken * delegateAmount, + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * 0, + _states.GetBalance(_validatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_operatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_delegatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_operatorAddress, Asset.Share)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_delegatorAddress, Asset.Share)); + Assert.Equal( + Asset.ConsensusToken * (selfDelegateAmount + delegateAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.GovernanceToken * (operatorMintAmount - selfDelegateAmount), + _states.GetBalance(_operatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (selfDelegateAmount + delegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _validatorAddress)!.DelegatorShares, + _states.GetBalance( + Delegation.DeriveAddress(_operatorAddress, _validatorAddress), Asset.Share) + + _states.GetBalance( + Delegation.DeriveAddress(_delegatorAddress, _validatorAddress), Asset.Share)); + } + + private void Initialize( + int operatorMintAmount, int delegatorMintAmount, int selfDelegateAmount) + { + _states = InitializeStates(); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + Asset.GovernanceToken * operatorMintAmount); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.GovernanceToken * delegatorMintAmount); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens); + } + } +} diff --git a/Lib9c.DPoS.Tests/Control/RedelegateCtrlTest.cs b/Lib9c.DPoS.Tests/Control/RedelegateCtrlTest.cs new file mode 100644 index 0000000000..862f2c0575 --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/RedelegateCtrlTest.cs @@ -0,0 +1,375 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + public class RedelegateCtrlTest : PoSTest + { + private readonly PublicKey _srcOperatorPublicKey; + private readonly PublicKey _dstOperatorPublicKey; + private readonly Address _srcOperatorAddress; + private readonly Address _dstOperatorAddress; + private readonly Address _delegatorAddress; + private readonly Address _srcValidatorAddress; + private readonly Address _dstValidatorAddress; + private readonly Address _redelegationAddress; + private ImmutableHashSet _nativeTokens; + private IWorld _states; + + public RedelegateCtrlTest() + { + _srcOperatorPublicKey = new PrivateKey().PublicKey; + _dstOperatorPublicKey = new PrivateKey().PublicKey; + _srcOperatorAddress = _srcOperatorPublicKey.Address; + _dstOperatorAddress = _dstOperatorPublicKey.Address; + _delegatorAddress = CreateAddress(); + _srcValidatorAddress = Validator.DeriveAddress(_srcOperatorAddress); + _dstValidatorAddress = Validator.DeriveAddress(_dstOperatorAddress); + _redelegationAddress = Redelegation.DeriveAddress( + _delegatorAddress, _srcValidatorAddress, _dstValidatorAddress); + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + } + + [Fact] + public void InvalidCurrencyTest() + { + Initialize(500, 500, 100, 100); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.ConsensusToken * 50); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.ConsensusToken * 30, + _nativeTokens)); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.GovernanceToken * 30, + _nativeTokens)); + } + + [Fact] + public void InvalidValidatorTest() + { + Initialize(500, 500, 100, 100); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + CreateAddress(), + _dstValidatorAddress, + Asset.Share * 10, + _nativeTokens)); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + CreateAddress(), + Asset.Share * 10, + _nativeTokens)); + } + + [Fact] + public void MaxEntriesTest() + { + Initialize(500, 500, 100, 100); + for (long i = 0; i < 10; i++) + { + _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = i, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.Share * 1, + _nativeTokens); + } + + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.Share * 1, + _nativeTokens)); + } + + [Fact] + public void ExceedRedelegateTest() + { + Initialize(500, 500, 100, 100); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.Share * 101, + _nativeTokens)); + } + + [Theory] + [InlineData(500, 500, 100, 100, 100)] + [InlineData(500, 500, 100, 100, 50)] + public void CompleteRedelegationTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount, + int redelegateAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount, delegateAmount); + _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.Share * redelegateAmount, + _nativeTokens); + Assert.Single( + RedelegateCtrl.GetRedelegation(_states, _redelegationAddress)! + .RedelegationEntryAddresses); + _states = RedelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1000, + }, + _redelegationAddress); + Assert.Single( + RedelegateCtrl.GetRedelegation(_states, _redelegationAddress)! + .RedelegationEntryAddresses); + _states = RedelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 50400 * 5, + }, + _redelegationAddress); + Assert.Empty( + RedelegateCtrl.GetRedelegation(_states, _redelegationAddress)! + .RedelegationEntryAddresses); + } + + [Theory] + [InlineData(500, 500, 100, 100, 100)] + [InlineData(500, 500, 100, 100, 50)] + public void BalanceTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount, + int redelegateAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount, delegateAmount); + _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.Share * redelegateAmount, + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * 0, + _states.GetBalance(_srcValidatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * 0, + _states.GetBalance(_dstValidatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_srcOperatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_dstOperatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_delegatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_srcOperatorAddress, Asset.Share)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_dstOperatorAddress, Asset.Share)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_delegatorAddress, Asset.Share)); + Assert.Equal( + Asset.GovernanceToken * (operatorMintAmount - selfDelegateAmount), + _states.GetBalance(_srcOperatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (operatorMintAmount - selfDelegateAmount), + _states.GetBalance(_dstOperatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (2 * selfDelegateAmount + delegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _srcValidatorAddress)!.DelegatorShares, + _states.GetBalance( + Delegation.DeriveAddress( + _srcOperatorAddress, _srcValidatorAddress), Asset.Share) + + _states.GetBalance( + Delegation.DeriveAddress( + _delegatorAddress, _srcValidatorAddress), Asset.Share)); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _dstValidatorAddress)!.DelegatorShares, + _states.GetBalance( + Delegation.DeriveAddress( + _dstOperatorAddress, _dstValidatorAddress), Asset.Share) + + _states.GetBalance( + Delegation.DeriveAddress( + _delegatorAddress, _dstValidatorAddress), Asset.Share)); + RedelegationEntry entry = new RedelegationEntry( + _states.GetDPoSState( + RedelegateCtrl.GetRedelegation(_states, _redelegationAddress)! + .RedelegationEntryAddresses[0])!); + Assert.Equal( + Asset.ConsensusToken * (selfDelegateAmount + delegateAmount) + - entry.UnbondingConsensusToken, + _states.GetBalance(_srcValidatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusToken * selfDelegateAmount + + entry.UnbondingConsensusToken, + _states.GetBalance(_dstValidatorAddress, Asset.ConsensusToken)); + } + + private void Initialize( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount) + { + _states = InitializeStates(); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _srcOperatorAddress, + Asset.GovernanceToken * operatorMintAmount); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _dstOperatorAddress, + Asset.GovernanceToken * operatorMintAmount); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.GovernanceToken * delegatorMintAmount); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _srcOperatorAddress, + _srcOperatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _dstOperatorAddress, + _dstOperatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens); + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + Asset.GovernanceToken * delegateAmount, + _nativeTokens); + } + } +} diff --git a/Lib9c.DPoS.Tests/Control/UndelegateCtrlTest.cs b/Lib9c.DPoS.Tests/Control/UndelegateCtrlTest.cs new file mode 100644 index 0000000000..4bfb53041a --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/UndelegateCtrlTest.cs @@ -0,0 +1,393 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + public class UndelegateCtrlTest : PoSTest + { + private readonly PublicKey _operatorPublicKey; + private readonly Address _operatorAddress; + private readonly Address _delegatorAddress; + private readonly Address _validatorAddress; + private readonly Address _undelegationAddress; + private ImmutableHashSet _nativeTokens; + private IWorld _states; + + public UndelegateCtrlTest() + { + _operatorPublicKey = new PrivateKey().PublicKey; + _operatorAddress = _operatorPublicKey.Address; + _delegatorAddress = CreateAddress(); + _validatorAddress = Validator.DeriveAddress(_operatorAddress); + _undelegationAddress = Undelegation.DeriveAddress(_delegatorAddress, _validatorAddress); + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + } + + [Fact] + public void InvalidCurrencyTest() + { + Initialize(500, 500, 100, 100); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.ConsensusToken * 50); + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.ConsensusToken * 30, + _nativeTokens)); + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.GovernanceToken * 30, + _nativeTokens)); + } + + [Fact] + public void InvalidValidatorTest() + { + Initialize(500, 500, 100, 100); + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + CreateAddress(), + Asset.Share * 10, + _nativeTokens)); + } + + [Fact] + public void MaxEntriesTest() + { + Initialize(500, 500, 100, 100); + for (long i = 0; i < 10; i++) + { + _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = i, + }, + _delegatorAddress, + _validatorAddress, + Asset.Share * 1, + _nativeTokens); + } + + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.Share * 1, + _nativeTokens)); + } + + [Fact] + public void ExceedUndelegateTest() + { + Initialize(500, 500, 100, 100); + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.Share * 101, + _nativeTokens)); + } + + [Theory] + [InlineData(500, 500, 100, 100, 100)] + [InlineData(500, 500, 100, 100, 50)] + public void CompleteUnbondingTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount, + int undelegateAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount, delegateAmount); + _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.Share * undelegateAmount, + _nativeTokens); + Assert.Single( + UndelegateCtrl.GetUndelegation(_states, _undelegationAddress)! + .UndelegationEntryAddresses); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (selfDelegateAmount + delegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + _states = UndelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1000, + }, + _undelegationAddress); + Assert.Single(UndelegateCtrl.GetUndelegation(_states, _undelegationAddress)! + .UndelegationEntryAddresses); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (selfDelegateAmount + delegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + _states = UndelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 50400 * 5, + }, + _undelegationAddress); + Assert.Empty(UndelegateCtrl.GetUndelegation(_states, _undelegationAddress)! + .UndelegationEntryAddresses); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount + undelegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (selfDelegateAmount + delegateAmount - undelegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + } + + [Theory] + [InlineData(500, 500, 100, 100, 100, 30)] + [InlineData(500, 500, 100, 100, 50, 30)] + public void CancelUndelegateTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount, + int undelegateAmount, + int cancelAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount, delegateAmount); + _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.Share * undelegateAmount, + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusToken * (selfDelegateAmount + delegateAmount - undelegateAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + _states = UndelegateCtrl.Cancel( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 2, + }, + _undelegationAddress, + Asset.ConsensusToken * cancelAmount, + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusToken + * (selfDelegateAmount + delegateAmount - undelegateAmount + cancelAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + _states = UndelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1000, + }, + _undelegationAddress); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusToken + * (selfDelegateAmount + delegateAmount - undelegateAmount + cancelAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + _states = UndelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 50400 * 5, + }, + _undelegationAddress); + Assert.Equal( + Asset.GovernanceToken + * (delegatorMintAmount - delegateAmount + undelegateAmount - cancelAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusToken + * (selfDelegateAmount + delegateAmount - undelegateAmount + cancelAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + } + + [Theory] + [InlineData(500, 500, 100, 100, 100)] + [InlineData(500, 500, 100, 100, 50)] + public void BalanceTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount, + int undelegateAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount, delegateAmount); + _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.Share * undelegateAmount, + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * 0, + _states.GetBalance(_validatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_operatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_delegatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_operatorAddress, Asset.Share)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_delegatorAddress, Asset.Share)); + Assert.Equal( + Asset.GovernanceToken * (operatorMintAmount - selfDelegateAmount), + _states.GetBalance(_operatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (selfDelegateAmount + delegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _validatorAddress)!.DelegatorShares, + _states.GetBalance( + Delegation.DeriveAddress( + _operatorAddress, _validatorAddress), Asset.Share) + + _states.GetBalance( + Delegation.DeriveAddress( + _delegatorAddress, _validatorAddress), Asset.Share)); + Assert.Equal( + Asset.ConsensusToken * (selfDelegateAmount + delegateAmount - undelegateAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + } + + private void Initialize( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount) + { + _states = InitializeStates(); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + Asset.GovernanceToken * operatorMintAmount); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.GovernanceToken * delegatorMintAmount); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens); + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.GovernanceToken * delegateAmount, + _nativeTokens); + } + } +} diff --git a/Lib9c.DPoS.Tests/Control/ValidatorCtrlTest.cs b/Lib9c.DPoS.Tests/Control/ValidatorCtrlTest.cs new file mode 100644 index 0000000000..6a5a548024 --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/ValidatorCtrlTest.cs @@ -0,0 +1,124 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + public class ValidatorCtrlTest : PoSTest + { + private readonly PublicKey _operatorPublicKey; + private readonly Address _operatorAddress; + private readonly Address _validatorAddress; + private readonly ImmutableHashSet _nativeTokens; + private IWorld _states; + + public ValidatorCtrlTest() + : base() + { + _operatorPublicKey = new PrivateKey().PublicKey; + _operatorAddress = _operatorPublicKey.Address; + _validatorAddress = Validator.DeriveAddress(_operatorAddress); + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + } + + [Fact] + public void InvalidCurrencyTest() + { + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + Asset.ConsensusToken * 50); + Assert.Throws( + () => _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.ConsensusToken * 30, + _nativeTokens)); + } + + [Theory] + [InlineData(500, 0)] + [InlineData(500, 1000)] + public void InvalidSelfDelegateTest(int mintAmount, int selfDelegateAmount) + { + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + Asset.GovernanceToken * mintAmount); + Assert.Throws( + () => _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens)); + } + + [Theory] + [InlineData(500, 10)] + [InlineData(500, 100)] + public void BalanceTest(int mintAmount, int selfDelegateAmount) + { + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + Asset.GovernanceToken * mintAmount); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens); + Assert.Equal( + Asset.ConsensusToken * selfDelegateAmount, + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.GovernanceToken * (mintAmount - selfDelegateAmount), + _states.GetBalance(_operatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.Share * selfDelegateAmount, + _states.GetBalance( + Delegation.DeriveAddress(_operatorAddress, _validatorAddress), Asset.Share)); + Assert.Equal( + Asset.Share * selfDelegateAmount, + ValidatorCtrl.GetValidator(_states, _validatorAddress)!.DelegatorShares); + } + } +} diff --git a/Lib9c.DPoS.Tests/Control/ValidatorPowerIndexCtrlTest.cs b/Lib9c.DPoS.Tests/Control/ValidatorPowerIndexCtrlTest.cs new file mode 100644 index 0000000000..8929a66683 --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/ValidatorPowerIndexCtrlTest.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + public class ValidatorPowerIndexCtrlTest : PoSTest + { + private readonly ImmutableHashSet _nativeTokens; + private IWorld _states; + + public ValidatorPowerIndexCtrlTest() + { + List operatorPublicKeys = new List() + { + new PrivateKey().PublicKey, + new PrivateKey().PublicKey, + new PrivateKey().PublicKey, + new PrivateKey().PublicKey, + new PrivateKey().PublicKey, + }; + + List
operatorAddresses = operatorPublicKeys.Select( + pubKey => pubKey.Address).ToList(); + + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + _states = InitializeStates(); + ValidatorAddresses = new List
(); + + var pairs = operatorAddresses.Zip(operatorPublicKeys, (addr, key) => (addr, key)); + foreach (var (addr, key) in pairs) + { + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + addr, + Asset.GovernanceToken * 100); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + addr, + key, + Asset.GovernanceToken * 10, _nativeTokens); + ValidatorAddresses.Add(Validator.DeriveAddress(addr)); + } + } + + private List
ValidatorAddresses { get; set; } + + [Fact] + public void SortingTestDifferentToken() + { + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[0], + Asset.ConsensusToken * 10); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[1], + Asset.ConsensusToken * 30); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[2], + Asset.ConsensusToken * 50); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[3], + Asset.ConsensusToken * 40); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[4], + Asset.ConsensusToken * 20); + _states = ValidatorPowerIndexCtrl.Update(_states, ValidatorAddresses); + ValidatorPowerIndex validatorPowerIndex; + (_states, validatorPowerIndex) + = ValidatorPowerIndexCtrl.FetchValidatorPowerIndex(_states); + List index = validatorPowerIndex.Index.ToList(); + Assert.Equal(5, index.Count); + Assert.Equal(ValidatorAddresses[2], index[0].ValidatorAddress); + Assert.Equal(Asset.ConsensusToken * 60, index[0].ConsensusToken); + Assert.Equal(ValidatorAddresses[3], index[1].ValidatorAddress); + Assert.Equal(Asset.ConsensusToken * 50, index[1].ConsensusToken); + Assert.Equal(ValidatorAddresses[1], index[2].ValidatorAddress); + Assert.Equal(Asset.ConsensusToken * 40, index[2].ConsensusToken); + Assert.Equal(ValidatorAddresses[4], index[3].ValidatorAddress); + Assert.Equal(Asset.ConsensusToken * 30, index[3].ConsensusToken); + Assert.Equal(ValidatorAddresses[0], index[4].ValidatorAddress); + Assert.Equal(Asset.ConsensusToken * 20, index[4].ConsensusToken); + } + + [Fact] + public void SortingTestSameToken() + { + (_states, _) = ValidatorPowerIndexCtrl.FetchValidatorPowerIndex(_states); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[0], + Asset.ConsensusToken * 10); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[1], + Asset.ConsensusToken * 10); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[2], + Asset.ConsensusToken * 10); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[3], + Asset.ConsensusToken * 10); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[4], + Asset.ConsensusToken * 10); + _states = ValidatorPowerIndexCtrl.Update(_states, ValidatorAddresses); + ValidatorPowerIndex validatorPowerIndex; + (_states, validatorPowerIndex) + = ValidatorPowerIndexCtrl.FetchValidatorPowerIndex(_states); + List index = validatorPowerIndex.Index.ToList(); + Assert.Equal(5, index.Count); + for (int i = 0; i < index.Count - 1; i++) + { + Assert.True(((IComparable
)index[i].ValidatorAddress) + .CompareTo(index[i + 1].ValidatorAddress) > 0); + } + } + } +} diff --git a/Lib9c.DPoS.Tests/Control/ValidatorSetCtrlTest.cs b/Lib9c.DPoS.Tests/Control/ValidatorSetCtrlTest.cs new file mode 100644 index 0000000000..e8a9055f80 --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/ValidatorSetCtrlTest.cs @@ -0,0 +1,221 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + public class ValidatorSetCtrlTest : PoSTest + { + private ImmutableHashSet _nativeTokens; + private IWorld _states; + + public ValidatorSetCtrlTest() + : base() + { + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + OperatorAddresses = new List
(); + ValidatorAddresses = new List
(); + DelegatorAddress = CreateAddress(); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + }, + DelegatorAddress, + Asset.GovernanceToken * 100000); + for (int i = 0; i < 200; i++) + { + PublicKey operatorPublicKey = new PrivateKey().PublicKey; + Address operatorAddress = operatorPublicKey.Address; + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + }, + operatorAddress, + Asset.GovernanceToken * 1000); + OperatorAddresses.Add(operatorAddress); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + operatorAddress, + operatorPublicKey, + Asset.GovernanceToken * 1, + _nativeTokens); + ValidatorAddresses.Add(Validator.DeriveAddress(operatorAddress)); + } + } + + private List
OperatorAddresses { get; set; } + + private List
ValidatorAddresses { get; set; } + + private Address DelegatorAddress { get; set; } + + [Fact] + public void ValidatorSetTest() + { + for (int i = 0; i < 200; i++) + { + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + ValidatorAddresses[i], + Asset.GovernanceToken * (i + 1), + _nativeTokens); + } + + Address validatorAddressA = ValidatorAddresses[3]; + Address validatorAddressB = ValidatorAddresses[5]; + Address delegationAddressB = Delegation.DeriveAddress( + DelegatorAddress, validatorAddressB); + + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + validatorAddressA, + Asset.GovernanceToken * 200, + _nativeTokens); + + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + validatorAddressB, + Asset.GovernanceToken * 300, + _nativeTokens); + + _states = ValidatorSetCtrl.Update( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }); + + ValidatorSet bondedSet; + (_states, bondedSet) = ValidatorSetCtrl.FetchBondedValidatorSet(_states); + Assert.Equal( + validatorAddressB, bondedSet.Set.ToList()[0].ValidatorAddress); + Assert.Equal( + validatorAddressA, bondedSet.Set.ToList()[1].ValidatorAddress); + Assert.Equal( + Asset.Share * (5 + 1 + 300), + _states.GetBalance(delegationAddressB, Asset.Share)); + Assert.Equal( + Asset.ConsensusToken * (1 + 5 + 1 + 300), + _states.GetBalance(ValidatorAddresses[5], Asset.ConsensusToken)); + Assert.Equal( + Asset.GovernanceToken + * (100 + (101 + 200) * 50 - 101 - 102 + 204 + 306), + _states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100 + (1 + 100) * 50 - 4 - 6 + 101 + 102), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100000 - (1 + 200) * 100 - 200 - 300), + _states.GetBalance(DelegatorAddress, Asset.GovernanceToken)); + + _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 2, + }, + DelegatorAddress, + validatorAddressB, + _states.GetBalance(delegationAddressB, Asset.Share), + _nativeTokens); + + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(delegationAddressB, Asset.Share)); + Assert.Equal( + Asset.ConsensusToken * 1, + _states.GetBalance(validatorAddressB, Asset.ConsensusToken)); + Assert.Equal( + Asset.GovernanceToken + * (100 + (101 + 200) * 50 - 101 - 102 + 204 + 306 - 306), + _states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100 + (1 + 100) * 50 - 4 - 6 + 101 + 102 + 306), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100000 - (1 + 200) * 100 - 200 - 300), + _states.GetBalance(DelegatorAddress, Asset.GovernanceToken)); + + _states = ValidatorSetCtrl.Update( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }); + (_states, bondedSet) = ValidatorSetCtrl.FetchBondedValidatorSet(_states); + Assert.Equal( + validatorAddressA, bondedSet.Set.ToList()[0].ValidatorAddress); + Assert.Equal( + Asset.GovernanceToken + * (100 + (101 + 200) * 50 - 101 - 102 + 204 + 306 - 306 + 102), + _states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100 + (1 + 100) * 50 - 4 - 6 + 101 + 102 + 306 - 102), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100000 - (1 + 200) * 100 - 200 - 300), + _states.GetBalance(DelegatorAddress, Asset.GovernanceToken)); + + _states = ValidatorSetCtrl.Update( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 50400 * 5, + }); + + Assert.Equal( + Asset.GovernanceToken + * (100 + (1 + 100) * 50 - 4 - 6 + 101 + 102 + 306 - 102 - 306), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100000 - (1 + 200) * 100 - 200 - 300 + 306), + _states.GetBalance(DelegatorAddress, Asset.GovernanceToken)); + } + } +} diff --git a/Lib9c.DPoS.Tests/DistributeTest.cs b/Lib9c.DPoS.Tests/DistributeTest.cs new file mode 100644 index 0000000000..9c50484e87 --- /dev/null +++ b/Lib9c.DPoS.Tests/DistributeTest.cs @@ -0,0 +1,259 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Consensus; +using Nekoyume.Module; +using Xunit; +using Validator = Lib9c.DPoS.Model.Validator; + +namespace Lib9c.DPoS.Tests +{ + public class DistributeTest : PoSTest + { + private readonly ImmutableHashSet _nativeTokens; + private IWorld _states; + + public DistributeTest() + : base() + { + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + OperatorPrivateKeys = new List(); + OperatorPublicKeys = new List(); + OperatorAddresses = new List
(); + ValidatorAddresses = new List
(); + DelegatorAddress = CreateAddress(); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + Asset.GovernanceToken * 100000); + for (int i = 0; i < 200; i++) + { + PrivateKey operatorPrivateKey = new PrivateKey(); + PublicKey operatorPublicKey = operatorPrivateKey.PublicKey; + Address operatorAddress = operatorPublicKey.Address; + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + operatorAddress, + Asset.GovernanceToken * 1000); + + OperatorPrivateKeys.Add(operatorPrivateKey); + OperatorPublicKeys.Add(operatorPublicKey); + OperatorAddresses.Add(operatorAddress); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + operatorAddress, + operatorPublicKey, + Asset.GovernanceToken * 1, + _nativeTokens); + ValidatorAddresses.Add(Validator.DeriveAddress(operatorAddress)); + } + } + + private List OperatorPrivateKeys { get; set; } + + private List OperatorPublicKeys { get; set; } + + private List
OperatorAddresses { get; set; } + + private List
ValidatorAddresses { get; set; } + + private Address DelegatorAddress { get; set; } + + [Fact] + public void ValidatorSetTest() + { + for (int i = 0; i < 200; i++) + { + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + ValidatorAddresses[i], + Asset.GovernanceToken * (i + 1), + _nativeTokens); + } + + Address validatorAddressA = ValidatorAddresses[3]; + Address validatorAddressB = ValidatorAddresses[5]; + + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + validatorAddressA, + Asset.GovernanceToken * 200, + _nativeTokens); + + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + validatorAddressB, + Asset.GovernanceToken * 300, + _nativeTokens); + + _states = ValidatorSetCtrl.Update( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }); + + (_states, _) = ValidatorSetCtrl.FetchBondedValidatorSet(_states); + + List votes = new List() + { + new VoteMetadata( + default, + default, + default, + default, + OperatorPrivateKeys[3].PublicKey, + VoteFlag.PreCommit).Sign(OperatorPrivateKeys[3]), + new VoteMetadata( + default, + default, + default, + default, + OperatorPrivateKeys[5].PublicKey, + VoteFlag.PreCommit).Sign(OperatorPrivateKeys[5]), + }; + FungibleAssetValue blockReward = Asset.ConsensusToken * 50; + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ReservedAddress.RewardPool, + blockReward); + _states = AllocateReward.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _nativeTokens, + votes, + OperatorAddresses[3]); + + var (baseProposerReward, _) + = (blockReward * AllocateReward.BaseProposerRewardNumer) + .DivRem(AllocateReward.BaseProposerRewardDenom); + var (bonusProposerReward, _) + = (blockReward * (205 + 307) + * AllocateReward.BonusProposerRewardNumer) + .DivRem((100 + (101 + 200) * 50 - 101 - 102 + 204 + 306) + * AllocateReward.BonusProposerRewardDenom); + FungibleAssetValue proposerReward = baseProposerReward + bonusProposerReward; + FungibleAssetValue validatorRewardSum = blockReward - proposerReward; + + var (validatorRewardA, _) + = (validatorRewardSum * 205) + .DivRem(100 + (101 + 200) * 50 - 101 - 102 + 204 + 306); + var (commissionA, _) + = (validatorRewardA * Validator.CommissionNumer) + .DivRem(Validator.CommissionDenom); + var (validatorRewardB, _) + = (validatorRewardSum * 307) + .DivRem(100 + (101 + 200) * 50 - 101 - 102 + 204 + 306); + var (commissionB, _) + = (validatorRewardB * Validator.CommissionNumer) + .DivRem(Validator.CommissionDenom); + + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(ReservedAddress.RewardPool, Asset.ConsensusToken)); + + Assert.Equal( + Asset.GovernanceToken * (100 + (101 + 200) * 50 - 101 - 102 + 204 + 306), + _states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken)); + + Assert.Equal( + Asset.ConsensusToken * 205, + _states.GetBalance(validatorAddressA, Asset.ConsensusToken)); + + Assert.Equal( + Asset.ConsensusToken * 307, + _states.GetBalance(validatorAddressB, Asset.ConsensusToken)); + + Assert.Equal( + proposerReward + commissionA, + _states.GetBalance( + AllocateReward.RewardAddress(OperatorAddresses[3]), Asset.ConsensusToken)); + + Assert.Equal( + commissionB, + _states.GetBalance( + AllocateReward.RewardAddress(OperatorAddresses[5]), Asset.ConsensusToken)); + + Address delegationAddressA + = Delegation.DeriveAddress(DelegatorAddress, validatorAddressA); + + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance( + AllocateReward.RewardAddress(DelegatorAddress), Asset.ConsensusToken)); + + var (delegatorToken, _) + = (_states.GetBalance( + ValidatorRewards.DeriveAddress(validatorAddressA, Asset.ConsensusToken), + Asset.ConsensusToken) + * _states.GetBalance( + Delegation.DeriveAddress(DelegatorAddress, validatorAddressA), + Asset.Share) + .RawValue) + .DivRem(ValidatorCtrl.GetValidator(_states, validatorAddressA)! + .DelegatorShares.RawValue); + + _states = DelegateCtrl.Distribute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 5, + }, + _nativeTokens, + delegationAddressA); + + Assert.Equal( + delegatorToken, + _states.GetBalance( + AllocateReward.RewardAddress(DelegatorAddress), Asset.ConsensusToken)); + } + } +} diff --git a/Lib9c.DPoS.Tests/Lib9c.DPoS.Tests.csproj b/Lib9c.DPoS.Tests/Lib9c.DPoS.Tests.csproj new file mode 100644 index 0000000000..db2468e974 --- /dev/null +++ b/Lib9c.DPoS.Tests/Lib9c.DPoS.Tests.csproj @@ -0,0 +1,34 @@ + + + + net6.0 + 9.0 + false + false + true + true + $(NoWarn);CS0162;CS8032;CS0618;CS0612;SYSLIB0011 + enable + .\Lib9c.Tests.ruleset + Debug;Release;DevEx + AnyCPU + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Lib9c.DPoS.Tests/Model/DelegationTest.cs b/Lib9c.DPoS.Tests/Model/DelegationTest.cs new file mode 100644 index 0000000000..5dea9496ab --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/DelegationTest.cs @@ -0,0 +1,23 @@ +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class DelegationTest : PoSTest + { + private readonly Delegation _delegation; + + public DelegationTest() + { + _delegation = new Delegation(CreateAddress(), CreateAddress()); + } + + [Fact] + public void MarshallingTest() + { + Delegation newDelegation + = new Delegation(_delegation.Serialize()); + Assert.Equal(_delegation, newDelegation); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/RedelegationEntryTest.cs b/Lib9c.DPoS.Tests/Model/RedelegationEntryTest.cs new file mode 100644 index 0000000000..cb6ae49b25 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/RedelegationEntryTest.cs @@ -0,0 +1,48 @@ +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class RedelegationEntryTest : PoSTest + { + private readonly RedelegationEntry _redelegationEntry; + + public RedelegationEntryTest() + { + _redelegationEntry = new RedelegationEntry( + CreateAddress(), + Asset.Share * 1, + Asset.ConsensusToken * 1, + Asset.Share * 1, + 1, + 1); + } + + [Fact] + public void InvalidUnbondingConsensusToken() + { + Assert.Throws( + () => _redelegationEntry.RedelegatingShare = Asset.GovernanceToken * 1); + Assert.Throws( + () => _redelegationEntry.RedelegatingShare = Asset.ConsensusToken * 1); + Assert.Throws( + () => _redelegationEntry.UnbondingConsensusToken = Asset.GovernanceToken * 1); + Assert.Throws( + () => _redelegationEntry.UnbondingConsensusToken = Asset.Share * 1); + Assert.Throws( + () => _redelegationEntry.IssuedShare = Asset.GovernanceToken * 1); + Assert.Throws( + () => _redelegationEntry.IssuedShare = Asset.ConsensusToken * 1); + } + + [Fact] + public void MarshallingTest() + { + RedelegationEntry newRedelegationEntry + = new RedelegationEntry(_redelegationEntry.Serialize()); + Assert.Equal(_redelegationEntry, newRedelegationEntry); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/RedelegationTest.cs b/Lib9c.DPoS.Tests/Model/RedelegationTest.cs new file mode 100644 index 0000000000..21cee77b7c --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/RedelegationTest.cs @@ -0,0 +1,24 @@ +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class RedelegationTest : PoSTest + { + private readonly Redelegation _redelegation; + + public RedelegationTest() + { + _redelegation = new Redelegation( + CreateAddress(), CreateAddress(), CreateAddress()); + } + + [Fact] + public void MarshallingTest() + { + Redelegation newRedelegationInfo + = new Redelegation(_redelegation.Serialize()); + Assert.Equal(_redelegation, newRedelegationInfo); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/UndelegationEntryTest.cs b/Lib9c.DPoS.Tests/Model/UndelegationEntryTest.cs new file mode 100644 index 0000000000..16d5124510 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/UndelegationEntryTest.cs @@ -0,0 +1,35 @@ +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class UndelegationEntryTest : PoSTest + { + private readonly UndelegationEntry _undelegationEntry; + + public UndelegationEntryTest() + { + _undelegationEntry = new UndelegationEntry( + CreateAddress(), Asset.ConsensusToken * 1, 1, 1); + } + + [Fact] + public void InvalidUnbondingConsensusToken() + { + Assert.Throws( + () => _undelegationEntry.UnbondingConsensusToken = Asset.GovernanceToken * 1); + Assert.Throws( + () => _undelegationEntry.UnbondingConsensusToken = Asset.Share * 1); + } + + [Fact] + public void MarshallingTest() + { + UndelegationEntry newUndelegationEntry + = new UndelegationEntry(_undelegationEntry.Serialize()); + Assert.Equal(_undelegationEntry, newUndelegationEntry); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/UndelegationTest.cs b/Lib9c.DPoS.Tests/Model/UndelegationTest.cs new file mode 100644 index 0000000000..0806c58cf4 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/UndelegationTest.cs @@ -0,0 +1,23 @@ +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class UndelegationTest : PoSTest + { + private readonly Undelegation _undelegation; + + public UndelegationTest() + { + _undelegation = new Undelegation(CreateAddress(), CreateAddress()); + } + + [Fact] + public void MarshallingTest() + { + Undelegation newUndelegationInfo + = new Undelegation(_undelegation.Serialize()); + Assert.Equal(_undelegation, newUndelegationInfo); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/ValidatorPowerIndexTest.cs b/Lib9c.DPoS.Tests/Model/ValidatorPowerIndexTest.cs new file mode 100644 index 0000000000..acdd7a4983 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/ValidatorPowerIndexTest.cs @@ -0,0 +1,23 @@ +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class ValidatorPowerIndexTest : PoSTest + { + private readonly ValidatorPowerIndex _validatorPowerIndex; + + public ValidatorPowerIndexTest() + { + _validatorPowerIndex = new ValidatorPowerIndex(); + } + + [Fact] + public void MarshallingTest() + { + ValidatorPowerIndex newValidatorPowerIndex = new ValidatorPowerIndex( + _validatorPowerIndex.Serialize()); + Assert.Equal(_validatorPowerIndex.Index, newValidatorPowerIndex.Index); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/ValidatorPowerTest.cs b/Lib9c.DPoS.Tests/Model/ValidatorPowerTest.cs new file mode 100644 index 0000000000..0149f97279 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/ValidatorPowerTest.cs @@ -0,0 +1,38 @@ +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Crypto; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class ValidatorPowerTest : PoSTest + { + private readonly ValidatorPower _validatorPower; + + public ValidatorPowerTest() + { + _validatorPower = new ValidatorPower( + CreateAddress(), + new PrivateKey().PublicKey, + Asset.ConsensusToken * 10); + } + + [Fact] + public void InvalidUnbondingConsensusToken() + { + Assert.Throws( + () => _validatorPower.ConsensusToken = Asset.GovernanceToken * 1); + Assert.Throws( + () => _validatorPower.ConsensusToken = Asset.Share * 1); + } + + [Fact] + public void MarshallingTest() + { + ValidatorPower newValidatorPower = new ValidatorPower( + _validatorPower.Serialize()); + Assert.Equal(_validatorPower, newValidatorPower); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/ValidatorSetTest.cs b/Lib9c.DPoS.Tests/Model/ValidatorSetTest.cs new file mode 100644 index 0000000000..189a89996f --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/ValidatorSetTest.cs @@ -0,0 +1,25 @@ +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class ValidatorSetTest : PoSTest + { + private readonly ValidatorSet _validatorSet; + + public ValidatorSetTest() + { + _validatorSet = new ValidatorSet(); + } + + [Fact] + public void MarshallingTest() + { + ValidatorSet newValidatorSet = new ValidatorSet( + _validatorSet.Serialize()); + Assert.Equal( + _validatorSet.Set, + newValidatorSet.Set); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/ValidatorTest.cs b/Lib9c.DPoS.Tests/Model/ValidatorTest.cs new file mode 100644 index 0000000000..d700513315 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/ValidatorTest.cs @@ -0,0 +1,34 @@ +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Crypto; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class ValidatorTest : PoSTest + { + private readonly Validator _validator; + + public ValidatorTest() + { + _validator = new Validator(CreateAddress(), new PrivateKey().PublicKey); + } + + [Fact] + public void InvalidShareTypeTest() + { + Assert.Throws( + () => _validator.DelegatorShares = Asset.ConsensusToken * 1); + Assert.Throws( + () => _validator.DelegatorShares = Asset.GovernanceToken * 1); + } + + [Fact] + public void MarshallingTest() + { + Validator newValidator = new Validator(_validator.Serialize()); + Assert.Equal(_validator, newValidator); + } + } +} diff --git a/Lib9c.DPoS.Tests/PoSTest.cs b/Lib9c.DPoS.Tests/PoSTest.cs new file mode 100644 index 0000000000..1c1c1f1387 --- /dev/null +++ b/Lib9c.DPoS.Tests/PoSTest.cs @@ -0,0 +1,20 @@ +using Lib9c.Tests.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Tests +{ + public class PoSTest + { + protected static IWorld InitializeStates() + { + return new World(new MockWorldState()); + } + + protected static Address CreateAddress() + { + PrivateKey privateKey = new PrivateKey(); + return privateKey.Address; + } + } +} diff --git a/Lib9c.DPoS.Tests/TestRandom.cs b/Lib9c.DPoS.Tests/TestRandom.cs new file mode 100644 index 0000000000..361c45c03b --- /dev/null +++ b/Lib9c.DPoS.Tests/TestRandom.cs @@ -0,0 +1,41 @@ +using Libplanet.Action; + +namespace Lib9c.DPoS.Tests +{ + public class TestRandom : IRandom + { + private readonly System.Random _random; + + public TestRandom(int seed = default) + { + _random = new System.Random(seed); + } + + public int Seed => 0; + + public int Next() + { + return _random.Next(); + } + + public int Next(int maxValue) + { + return _random.Next(maxValue); + } + + public int Next(int minValue, int maxValue) + { + return _random.Next(minValue, maxValue); + } + + public void NextBytes(byte[] buffer) + { + _random.NextBytes(buffer); + } + + public double NextDouble() + { + return _random.NextDouble(); + } + } +} diff --git a/Lib9c.DPoS.Tests/ValidatorPowerComparerTest.cs b/Lib9c.DPoS.Tests/ValidatorPowerComparerTest.cs new file mode 100644 index 0000000000..e38e666f3b --- /dev/null +++ b/Lib9c.DPoS.Tests/ValidatorPowerComparerTest.cs @@ -0,0 +1,39 @@ +using System; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Crypto; +using Xunit; + +namespace Lib9c.DPoS.Tests +{ + public class ValidatorPowerComparerTest : PoSTest + { + [Fact] + public void CompareDifferentTokenTest() + { + PublicKey publicKeyA = new PrivateKey().PublicKey; + PublicKey publicKeyB = new PrivateKey().PublicKey; + ValidatorPower validatorPowerA = new ValidatorPower( + publicKeyA.Address, publicKeyA, Asset.ConsensusToken * 10); + ValidatorPower validatorPowerB = new ValidatorPower( + publicKeyB.Address, publicKeyB, Asset.ConsensusToken * 11); + Assert.True(((IComparable)validatorPowerA) + .CompareTo(validatorPowerB) > 0); + } + + [Fact] + public void CompareSameTokenTest() + { + PublicKey publicKeyA = new PrivateKey().PublicKey; + PublicKey publicKeyB = new PrivateKey().PublicKey; + ValidatorPower validatorPowerA = new ValidatorPower( + publicKeyA.Address, publicKeyA, Asset.ConsensusToken * 10); + ValidatorPower validatorPowerB = new ValidatorPower( + publicKeyB.Address, publicKeyB, Asset.ConsensusToken * 10); + int sign = -((IComparable
)publicKeyA.Address) + .CompareTo(publicKeyB.Address); + Assert.True(((IComparable)validatorPowerA) + .CompareTo(validatorPowerB) == sign); + } + } +} diff --git a/Lib9c.DPoS/AssemblyInfo.cs b/Lib9c.DPoS/AssemblyInfo.cs new file mode 100644 index 0000000000..bb400a3aee --- /dev/null +++ b/Lib9c.DPoS/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Lib9c.DPoS.Tests")] diff --git a/Lib9c.sln b/Lib9c.sln index c19d8af968..35a141ef77 100644 --- a/Lib9c.sln +++ b/Lib9c.sln @@ -72,6 +72,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib9c.Plugin.Shared", ".Lib EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib9c.DPoS", "Lib9c.DPoS\Lib9c.DPoS.csproj", "{23848A39-37DD-4B73-A108-1915E10FEE50}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib9c.DPoS.Tests", "Lib9c.DPoS.Tests\Lib9c.DPoS.Tests.csproj", "{447DEF1C-01B0-4FC7-8444-B9DB5C26AFB7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -202,6 +204,10 @@ Global {23848A39-37DD-4B73-A108-1915E10FEE50}.Debug|Any CPU.Build.0 = Debug|Any CPU {23848A39-37DD-4B73-A108-1915E10FEE50}.Release|Any CPU.ActiveCfg = Release|Any CPU {23848A39-37DD-4B73-A108-1915E10FEE50}.Release|Any CPU.Build.0 = Release|Any CPU + {447DEF1C-01B0-4FC7-8444-B9DB5C26AFB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {447DEF1C-01B0-4FC7-8444-B9DB5C26AFB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {447DEF1C-01B0-4FC7-8444-B9DB5C26AFB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {447DEF1C-01B0-4FC7-8444-B9DB5C26AFB7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE