diff --git a/.Lib9c.Benchmarks/Actions/AutoJoinGuild.cs b/.Lib9c.Benchmarks/Actions/AutoJoinGuild.cs deleted file mode 100644 index 426ceeeb89..0000000000 --- a/.Lib9c.Benchmarks/Actions/AutoJoinGuild.cs +++ /dev/null @@ -1,88 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Bencodex.Types; -using Lib9c.Tests.Action; -using Lib9c.Tests.Util; -using Libplanet.Action.State; -using Libplanet.Mocks; -using Nekoyume; -using Nekoyume.Action.Guild; -using Nekoyume.Extensions; -using Nekoyume.Module; -using Nekoyume.Module.Guild; -using Nekoyume.TypedAddress; - -namespace Lib9c.Benchmarks.Actions; - -public class AutoJoinGuild -{ - private AgentAddress signer = AddressUtil.CreateAgentAddress(); - private IWorld worldEmpty; - private IWorld worldWithPledge; - private IWorld worldWithPledgeAndGuild; - private IWorld worldAfterMigration; - - [GlobalSetup] - public void Setup() - { - worldEmpty = new World(MockUtil.MockModernWorldState); - worldWithPledge = worldEmpty - .SetLegacyState( - signer.GetPledgeAddress(), - new List(MeadConfig.PatronAddress.Bencoded, (Boolean)true, (Integer)4)); - - var guildMasterAddress = GuildConfig.PlanetariumGuildOwner; - var guildAddress = AddressUtil.CreateGuildAddress(); - worldWithPledgeAndGuild = worldWithPledge - .MakeGuild(guildAddress, guildMasterAddress); - worldAfterMigration = worldWithPledgeAndGuild - .JoinGuild(guildAddress, signer); - } - - [Benchmark] - public void Execute_WithoutPledge() - { - var action = new Nekoyume.PolicyAction.Tx.Begin.AutoJoinGuild(); - action.Execute(new ActionContext - { - IsPolicyAction = true, - PreviousState = worldEmpty, - Signer = signer, - }); - } - - [Benchmark] - public void Execute_WithPledge_WithoutGuild() - { - var action = new Nekoyume.PolicyAction.Tx.Begin.AutoJoinGuild(); - action.Execute(new ActionContext - { - IsPolicyAction = true, - PreviousState = worldWithPledge, - Signer = signer, - }); - } - - [Benchmark] - public void Execute_WithPledge_WithGuild() - { - var action = new Nekoyume.PolicyAction.Tx.Begin.AutoJoinGuild(); - action.Execute(new ActionContext - { - IsPolicyAction = true, - PreviousState = worldWithPledgeAndGuild, - Signer = signer, - }); - } - - [Benchmark] - public void Execute_AfterMigration() - { - var action = new Nekoyume.PolicyAction.Tx.Begin.AutoJoinGuild(); - action.Execute(new ActionContext - { - IsPolicyAction = true, - PreviousState = worldAfterMigration, - Signer = signer, - }); - } -} diff --git a/.Lib9c.Benchmarks/Actions/MigrateDelegation.cs b/.Lib9c.Benchmarks/Actions/MigrateDelegation.cs new file mode 100644 index 0000000000..01610e9d14 --- /dev/null +++ b/.Lib9c.Benchmarks/Actions/MigrateDelegation.cs @@ -0,0 +1,128 @@ +using BenchmarkDotNet.Attributes; +using Bencodex.Types; +using Lib9c.Tests.Action; +using Lib9c.Tests.Util; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Mocks; +using Nekoyume; +using Nekoyume.Action.Guild; +using Nekoyume.Action.Guild.Migration; +using Nekoyume.Action.Guild.Migration.LegacyModels; +using Nekoyume.Extensions; +using Nekoyume.TypedAddress; + +namespace Lib9c.Benchmarks.Actions; + +public class MigrateDelegation +{ + private GuildAddress planetariumGuild = AddressUtil.CreateGuildAddress(); + private AgentAddress target = AddressUtil.CreateAgentAddress(); + private AgentAddress signer = AddressUtil.CreateAgentAddress(); + private IWorld worldEmpty; + private IWorld worldBeforeGuildMigration; + private IWorld worldBeforeParticipantMigration; + private IWorld worldAfterMigration; + + [GlobalSetup] + public void Setup() + { + worldEmpty = new World(MockUtil.MockModernWorldState); + var legacyPlanetariumGuild = new LegacyGuild(GuildConfig.PlanetariumGuildOwner); + var legacyPlanetariumGuildParticipant = new LegacyGuildParticipant(planetariumGuild); + worldBeforeGuildMigration = worldEmpty + .MutateAccount( + Addresses.Guild, + account => account.SetState(planetariumGuild, legacyPlanetariumGuild.Bencoded)) + .MutateAccount( + Addresses.GuildParticipant, + account => account.SetState(GuildConfig.PlanetariumGuildOwner, legacyPlanetariumGuildParticipant.Bencoded)) + .MutateAccount( + Addresses.GuildParticipant, + account => account.SetState(target, legacyPlanetariumGuildParticipant.Bencoded)) + .MutateAccount( + Addresses.GuildMemberCounter, + account => account.SetState(planetariumGuild, (Integer)2)); + worldBeforeParticipantMigration = new MigratePlanetariumGuild().Execute(new ActionContext + { + PreviousState = worldBeforeGuildMigration, + Signer = new PrivateKey().Address, + }); + } + + [Benchmark] + public void Execute_Empty() + { + var action = new Nekoyume.Action.Guild.Migration.MigrateDelegation(target); + try + { + action.Execute(new ActionContext + { + IsPolicyAction = false, + PreviousState = worldEmpty, + Signer = signer, + }); + } + catch + { + // Do nothing. + } + } + + [Benchmark] + public void Execute_Before_Guild_Migration() + { + var action = new Nekoyume.Action.Guild.Migration.MigrateDelegation(target); + try + { + action.Execute(new ActionContext + { + IsPolicyAction = false, + PreviousState = worldBeforeGuildMigration, + Signer = signer, + }); + } + catch + { + // Do nothing. + } + } + + [Benchmark] + public void Execute_Before_Participant_Migration() + { + var action = new Nekoyume.Action.Guild.Migration.MigrateDelegation(target); + try + { + action.Execute(new ActionContext + { + IsPolicyAction = false, + PreviousState = worldBeforeParticipantMigration, + Signer = signer, + }); + } + catch + { + // Do nothing. + } + } + + [Benchmark] + public void Execute_After_Migration() + { + var action = new Nekoyume.Action.Guild.Migration.MigrateDelegation(target); + try + { + action.Execute(new ActionContext + { + IsPolicyAction = false, + PreviousState = worldAfterMigration, + Signer = signer, + }); + } + catch + { + // Do nothing. + } + } +} diff --git a/.Lib9c.Benchmarks/Actions/MigratePledgeToGuild.cs b/.Lib9c.Benchmarks/Actions/MigratePledgeToGuild.cs deleted file mode 100644 index 2edf048b8e..0000000000 --- a/.Lib9c.Benchmarks/Actions/MigratePledgeToGuild.cs +++ /dev/null @@ -1,110 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Bencodex.Types; -using Lib9c.Tests.Action; -using Lib9c.Tests.Util; -using Libplanet.Action.State; -using Libplanet.Mocks; -using Nekoyume; -using Nekoyume.Action.Guild; -using Nekoyume.Extensions; -using Nekoyume.Module; -using Nekoyume.Module.Guild; -using Nekoyume.TypedAddress; - -namespace Lib9c.Benchmarks.Actions; - -public class MigratePledgeToGuild -{ - private AgentAddress signer = AddressUtil.CreateAgentAddress(); - private AgentAddress target = AddressUtil.CreateAgentAddress(); - private IWorld worldEmpty; - private IWorld worldWithPledge; - private IWorld worldWithPledgeAndGuild; - private IWorld worldAfterMigration; - - [GlobalSetup] - public void Setup() - { - worldEmpty = new World(MockUtil.MockModernWorldState); - worldWithPledge = worldEmpty - .SetLegacyState( - target.GetPledgeAddress(), - new List(MeadConfig.PatronAddress.Bencoded, (Boolean)true, (Integer)4)); - - var guildMasterAddress = GuildConfig.PlanetariumGuildOwner; - var guildAddress = AddressUtil.CreateGuildAddress(); - worldWithPledgeAndGuild = worldWithPledge - .MakeGuild(guildAddress, guildMasterAddress); - worldAfterMigration = worldWithPledgeAndGuild - .JoinGuild(guildAddress, signer); - } - - [Benchmark] - public void Execute_WithoutPledge() - { - var action = new Nekoyume.Action.Guild.Migration.MigratePledgeToGuild(target); - try - { - action.Execute(new ActionContext - { - IsPolicyAction = false, - PreviousState = worldEmpty, - Signer = signer, - }); - } - catch - { - // Do nothing. - } - } - - [Benchmark] - public void Execute_WithPledge_WithoutGuild() - { - var action = new Nekoyume.Action.Guild.Migration.MigratePledgeToGuild(target); - try - { - action.Execute(new ActionContext - { - IsPolicyAction = false, - PreviousState = worldWithPledge, - Signer = signer, - }); - } - catch - { - // Do nothing. - } - } - - [Benchmark] - public void Execute_WithPledge_WithGuild() - { - var action = new Nekoyume.Action.Guild.Migration.MigratePledgeToGuild(target); - action.Execute(new ActionContext - { - IsPolicyAction = false, - PreviousState = worldWithPledgeAndGuild, - Signer = signer, - }); - } - - [Benchmark] - public void Execute_AfterMigration() - { - var action = new Nekoyume.Action.Guild.Migration.MigratePledgeToGuild(target); - try - { - action.Execute(new ActionContext - { - IsPolicyAction = false, - PreviousState = worldAfterMigration, - Signer = signer, - }); - } - catch - { - // Do nothing. - } - } -} diff --git a/.Lib9c.Miner.Tests/CustomActionsDeserializableValidatorTest.cs b/.Lib9c.Miner.Tests/CustomActionsDeserializableValidatorTest.cs index 104a8c1f5d..cf58ed01d1 100644 --- a/.Lib9c.Miner.Tests/CustomActionsDeserializableValidatorTest.cs +++ b/.Lib9c.Miner.Tests/CustomActionsDeserializableValidatorTest.cs @@ -49,7 +49,7 @@ public void LoadPlainValue(IValue plainValue) public IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); return context.PreviousState; } } diff --git a/.Lib9c.Plugin/PluginActionEvaluator.cs b/.Lib9c.Plugin/PluginActionEvaluator.cs index a44ace7658..961d1cab36 100644 --- a/.Lib9c.Plugin/PluginActionEvaluator.cs +++ b/.Lib9c.Plugin/PluginActionEvaluator.cs @@ -7,8 +7,6 @@ using Libplanet.Store; using Nekoyume.Action; using Nekoyume.Action.Loader; -using Nekoyume.PolicyAction.Tx.Begin; - namespace Lib9c.Plugin { diff --git a/.Lib9c.Tests/Action/ActionContext.cs b/.Lib9c.Tests/Action/ActionContext.cs index c366d29099..0323a0c918 100644 --- a/.Lib9c.Tests/Action/ActionContext.cs +++ b/.Lib9c.Tests/Action/ActionContext.cs @@ -7,14 +7,13 @@ namespace Lib9c.Tests.Action using Libplanet.Action.State; using Libplanet.Common; using Libplanet.Crypto; + using Libplanet.Types.Assets; using Libplanet.Types.Blocks; using Libplanet.Types.Evidence; using Libplanet.Types.Tx; public class ActionContext : IActionContext { - private long _gasUsed; - private IRandom _random = null; private IReadOnlyList _txs = null; @@ -35,6 +34,8 @@ public class ActionContext : IActionContext public int BlockProtocolVersion { get; set; } = BlockMetadata.CurrentProtocolVersion; + public BlockCommit LastCommit { get; set; } + public IWorld PreviousState { get; set; } public int RandomSeed { get; set; } @@ -43,6 +44,8 @@ public class ActionContext : IActionContext public bool IsPolicyAction { get; set; } + public FungibleAssetValue? MaxGasPrice { get; set; } + public IReadOnlyList Txs { get => _txs ?? ImmutableList.Empty; @@ -55,17 +58,8 @@ public IReadOnlyList Evidence set => _evs = value; } - 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) { diff --git a/.Lib9c.Tests/Action/AdventureBoss/ClaimAdventureBossRewardTest.cs b/.Lib9c.Tests/Action/AdventureBoss/ClaimAdventureBossRewardTest.cs index 598b36b047..c72d70bb43 100644 --- a/.Lib9c.Tests/Action/AdventureBoss/ClaimAdventureBossRewardTest.cs +++ b/.Lib9c.Tests/Action/AdventureBoss/ClaimAdventureBossRewardTest.cs @@ -4,6 +4,7 @@ namespace Lib9c.Tests.Action.AdventureBoss using System.Collections.Generic; using System.Linq; using System.Numerics; + using Lib9c.Tests.Util; using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Mocks; @@ -386,6 +387,10 @@ AdventureBossGameData.ClaimableReward expectedReward state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } + var validatorKey = new PrivateKey().PublicKey; + state = DelegationUtil.EnsureValidatorPromotionReady(state, validatorKey, 0L); + + state = DelegationUtil.MakeGuild(state, TesterAddress, validatorKey.Address, 0L); state = Stake(state, TesterAddress); // Wanted @@ -404,6 +409,7 @@ AdventureBossGameData.ClaimableReward expectedReward if (anotherWanted) { + state = DelegationUtil.MakeGuild(state, WantedAddress, validatorKey.Address, 0L); state = Stake(state, WantedAddress); state = new Wanted { @@ -471,6 +477,11 @@ public void WantedMultipleSeason() state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } + var validatorKey = new PrivateKey().PublicKey; + state = DelegationUtil.EnsureValidatorPromotionReady(state, validatorKey, 0L); + state = DelegationUtil.MakeGuild(state, TesterAddress, validatorKey.Address, 0L); + state = DelegationUtil.MakeGuild(state, WantedAddress, validatorKey.Address, 0L); + state = Stake(state, TesterAddress); state = Stake(state, WantedAddress); @@ -564,6 +575,10 @@ FungibleAssetValue expectedRemainingNcg state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } + var validatorKey = new PrivateKey().PublicKey; + state = DelegationUtil.EnsureValidatorPromotionReady(state, validatorKey, 0L); + state = DelegationUtil.MakeGuild(state, WantedAddress, validatorKey.Address, 0L); + state = Stake(state, WantedAddress); // Wanted @@ -737,6 +752,12 @@ public void ExploreMultipleSeason() state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } + var validatorKey = new PrivateKey().PublicKey; + state = DelegationUtil.EnsureValidatorPromotionReady(state, validatorKey, 0L); + state = DelegationUtil.MakeGuild(state, TesterAddress, validatorKey.Address, 0L); + state = DelegationUtil.MakeGuild(state, WantedAddress, validatorKey.Address, 0L); + state = DelegationUtil.MakeGuild(state, ExplorerAddress, validatorKey.Address, 0L); + state = Stake(state, TesterAddress); state = Stake(state, WantedAddress); state = Stake(state, ExplorerAddress); @@ -865,6 +886,10 @@ public void AllReward() state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } + var validatorKey = new PrivateKey().PublicKey; + state = DelegationUtil.EnsureValidatorPromotionReady(state, validatorKey, 0L); + state = DelegationUtil.MakeGuild(state, TesterAddress, validatorKey.Address, 0L); + state = Stake(state, TesterAddress); // Wanted @@ -934,6 +959,12 @@ Type exc state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } + var validatorKey = new PrivateKey().PublicKey; + state = DelegationUtil.EnsureValidatorPromotionReady(state, validatorKey, 0L); + state = DelegationUtil.MakeGuild(state, TesterAddress, validatorKey.Address, 0L); + state = DelegationUtil.MakeGuild(state, WantedAddress, validatorKey.Address, 0L); + state = DelegationUtil.MakeGuild(state, ExplorerAddress, validatorKey.Address, 0L); + state = Stake(state, TesterAddress); state = Stake(state, WantedAddress); state = Stake(state, ExplorerAddress); diff --git a/.Lib9c.Tests/Action/AdventureBoss/SweepAdventureBossTest.cs b/.Lib9c.Tests/Action/AdventureBoss/SweepAdventureBossTest.cs index 93bf24913d..0f6c24f74d 100644 --- a/.Lib9c.Tests/Action/AdventureBoss/SweepAdventureBossTest.cs +++ b/.Lib9c.Tests/Action/AdventureBoss/SweepAdventureBossTest.cs @@ -4,6 +4,7 @@ namespace Lib9c.Tests.Action.AdventureBoss using System.Collections.Generic; using System.Linq; using System.Numerics; + using Lib9c.Tests.Util; using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Mocks; @@ -130,6 +131,10 @@ public void Execute( state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } + var validatorKey = new PrivateKey().PublicKey; + state = DelegationUtil.EnsureValidatorPromotionReady(state, validatorKey, 0L); + state = DelegationUtil.MakeGuild(state, WantedAddress, validatorKey.Address, 0L); + state = Stake(state, WantedAddress); var sheets = state.GetSheets(sheetTypes: new[] { diff --git a/.Lib9c.Tests/Action/AdventureBoss/UnlockFloorTest.cs b/.Lib9c.Tests/Action/AdventureBoss/UnlockFloorTest.cs index 80b2f201b1..1a74798c0a 100644 --- a/.Lib9c.Tests/Action/AdventureBoss/UnlockFloorTest.cs +++ b/.Lib9c.Tests/Action/AdventureBoss/UnlockFloorTest.cs @@ -4,6 +4,7 @@ namespace Lib9c.Tests.Action.AdventureBoss using System.Collections.Generic; using System.Linq; using System.Numerics; + using Lib9c.Tests.Util; using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Mocks; @@ -108,6 +109,10 @@ public void Execute( state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); } + var validatorKey = new PrivateKey().PublicKey; + state = DelegationUtil.EnsureValidatorPromotionReady(state, validatorKey, 0L); + state = DelegationUtil.MakeGuild(state, WantedAddress, validatorKey.Address, 0L); + state = Stake(state, WantedAddress); var materialSheet = state.GetSheet(); var goldenDust = diff --git a/.Lib9c.Tests/Action/AdventureBoss/WantedTest.cs b/.Lib9c.Tests/Action/AdventureBoss/WantedTest.cs index 60528d50c6..29fc64af70 100644 --- a/.Lib9c.Tests/Action/AdventureBoss/WantedTest.cs +++ b/.Lib9c.Tests/Action/AdventureBoss/WantedTest.cs @@ -3,6 +3,7 @@ namespace Lib9c.Tests.Action.AdventureBoss using System.Collections.Generic; using System.Linq; using System.Numerics; + using Lib9c.Tests.Util; using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Mocks; @@ -14,6 +15,7 @@ namespace Lib9c.Tests.Action.AdventureBoss using Nekoyume.Exceptions; using Nekoyume.Helper; using Nekoyume.Model.AdventureBoss; + using Nekoyume.Model.Guild; using Nekoyume.Model.State; using Nekoyume.Module; using Nekoyume.TableData; @@ -574,6 +576,10 @@ private IWorld Stake(IWorld world, long amount = 0) amount = stakeRegularRewardSheet[requiredStakingLevel].RequiredGold; } + var validatorKey = new PrivateKey().PublicKey; + world = DelegationUtil.EnsureValidatorPromotionReady(world, validatorKey, 0L); + world = DelegationUtil.MakeGuild(world, AgentAddress, validatorKey.Address, 0L); + var action = new Stake(new BigInteger(amount)); return action.Execute(new ActionContext { diff --git a/.Lib9c.Tests/Action/ApprovePledgeTest.cs b/.Lib9c.Tests/Action/ApprovePledgeTest.cs index a2b9179a29..7f93df5d73 100644 --- a/.Lib9c.Tests/Action/ApprovePledgeTest.cs +++ b/.Lib9c.Tests/Action/ApprovePledgeTest.cs @@ -6,9 +6,12 @@ namespace Lib9c.Tests.Action using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Mocks; + using Libplanet.Types.Assets; using Nekoyume; using Nekoyume.Action; using Nekoyume.Action.Guild; + using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.Model.Guild; using Nekoyume.Model.State; using Nekoyume.Module; using Nekoyume.Module.Guild; @@ -45,7 +48,8 @@ public void Execute(int mead) Assert.Equal(patron, contract[0].ToAddress()); Assert.True(contract[1].ToBoolean()); Assert.Equal(mead, contract[2].ToInteger()); - Assert.Null(nextState.GetJoinedGuild(new AgentAddress(address))); + Assert.Null(new GuildRepository(nextState, new ActionContext()) + .GetJoinedGuild(new AgentAddress(address))); } [Theory] @@ -58,11 +62,24 @@ public void Execute_JoinGuild(int mead) var contractAddress = address.Derive(nameof(RequestPledge)); var guildAddress = AddressUtil.CreateGuildAddress(); IWorld states = new World(MockUtil.MockModernWorldState) + .SetLegacyState( + Addresses.GoldCurrency, + new GoldCurrencyState(Currency.Legacy("NCG", 2, null)).Serialize()) .SetLegacyState( contractAddress, List.Empty.Add(patron.Serialize()).Add(false.Serialize()).Add(mead.Serialize()) - ) - .MakeGuild(guildAddress, GuildConfig.PlanetariumGuildOwner); + ); + + states = DelegationUtil.EnsureValidatorPromotionReady( + states, ValidatorConfig.PlanetariumValidatorPublicKey, 0L); + + states = new GuildRepository( + states, + new ActionContext + { + Signer = GuildConfig.PlanetariumGuildOwner, + }) + .MakeGuild(guildAddress, ValidatorConfig.PlanetariumValidatorAddress).World; var action = new ApprovePledge { @@ -78,7 +95,8 @@ public void Execute_JoinGuild(int mead) Assert.Equal(patron, contract[0].ToAddress()); Assert.True(contract[1].ToBoolean()); Assert.Equal(mead, contract[2].ToInteger()); - var joinedGuildAddress = nextState.GetJoinedGuild(new AgentAddress(address)); + var joinedGuildAddress = new GuildRepository(nextState, new ActionContext()) + .GetJoinedGuild(new AgentAddress(address)); Assert.NotNull(joinedGuildAddress); Assert.Equal(guildAddress, joinedGuildAddress); } diff --git a/.Lib9c.Tests/Action/BuyTest.cs b/.Lib9c.Tests/Action/BuyTest.cs index 446209e98d..4ae96846b2 100644 --- a/.Lib9c.Tests/Action/BuyTest.cs +++ b/.Lib9c.Tests/Action/BuyTest.cs @@ -15,6 +15,7 @@ namespace Lib9c.Tests.Action using Libplanet.Types.Assets; using Nekoyume; using Nekoyume.Action; + using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Model; using Nekoyume.Model.Item; @@ -104,7 +105,8 @@ public BuyTest(ITestOutputHelper outputHelper) .SetAgentState(_buyerAgentAddress, buyerAgentState) .SetAvatarState(_buyerAvatarAddress, _buyerAvatarState) .SetLegacyState(Addresses.Shop, new ShopState().Serialize()) - .MintAsset(context, _buyerAgentAddress, _goldCurrencyState.Currency * 100); + .MintAsset(context, _buyerAgentAddress, _goldCurrencyState.Currency * 100) + .SetDelegationMigrationHeight(0); } public static IEnumerable GetExecuteMemberData() @@ -439,10 +441,7 @@ public void Execute(params OrderData[] orderDataList) Assert.Equal(30, nextBuyerAvatarState.mailBox.Count); - var arenaSheet = _tableSheets.ArenaSheet; - var arenaData = arenaSheet.GetRoundByBlockIndex(100); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); - var goldCurrencyGold = nextState.GetBalance(feeStoreAddress, goldCurrencyState); + var goldCurrencyGold = nextState.GetBalance(Addresses.RewardPool, goldCurrencyState); Assert.Equal(totalTax, goldCurrencyGold); var buyerGold = nextState.GetBalance(_buyerAgentAddress, goldCurrencyState); var prevBuyerGold = _initialState.GetBalance(_buyerAgentAddress, goldCurrencyState); @@ -944,10 +943,7 @@ public void Execute_With_Testbed() var buyerGold = nextState.GetBalance(result.GetAgentState().address, goldCurrencyState); Assert.Equal(prevBuyerGold - totalPrice, buyerGold); - var arenaSheet = _tableSheets.ArenaSheet; - var arenaData = arenaSheet.GetRoundByBlockIndex(100); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); - var goldCurrencyGold = nextState.GetBalance(feeStoreAddress, goldCurrencyState); + var goldCurrencyGold = nextState.GetBalance(Addresses.RewardPool, goldCurrencyState); Assert.Equal(totalTax, goldCurrencyGold); foreach (var (agentAddress, expectedGold) in agentRevenue) diff --git a/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs b/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs index 82ae98e4eb..77f831b50d 100644 --- a/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs +++ b/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs @@ -361,7 +361,7 @@ public void Execute_Throw_FailedLoadStateException_When_Staking_State_Null() AvatarAddr, 0)); - var stakeAddr = StakeStateV2.DeriveAddress(AgentAddr); + var stakeAddr = StakeState.DeriveAddress(AgentAddr); var previousState = _initialState.RemoveLegacyState(stakeAddr); Assert.Throws(() => Execute( @@ -373,19 +373,19 @@ public void Execute_Throw_FailedLoadStateException_When_Staking_State_Null() [Theory] [InlineData(0, null, 0)] - [InlineData(0, null, StakeState.RewardInterval - 1)] - [InlineData(0, StakeState.RewardInterval - 2, StakeState.RewardInterval - 1)] - [InlineData(0, StakeState.RewardInterval, StakeState.RewardInterval + 1)] - [InlineData(0, StakeState.RewardInterval, StakeState.RewardInterval * 2 - 1)] - [InlineData(0, StakeState.RewardInterval * 2 - 2, StakeState.RewardInterval * 2 - 1)] - [InlineData(0, StakeState.RewardInterval * 2, StakeState.RewardInterval * 2 + 1)] + [InlineData(0, null, LegacyStakeState.RewardInterval - 1)] + [InlineData(0, LegacyStakeState.RewardInterval - 2, LegacyStakeState.RewardInterval - 1)] + [InlineData(0, LegacyStakeState.RewardInterval, LegacyStakeState.RewardInterval + 1)] + [InlineData(0, LegacyStakeState.RewardInterval, LegacyStakeState.RewardInterval * 2 - 1)] + [InlineData(0, LegacyStakeState.RewardInterval * 2 - 2, LegacyStakeState.RewardInterval * 2 - 1)] + [InlineData(0, LegacyStakeState.RewardInterval * 2, LegacyStakeState.RewardInterval * 2 + 1)] public void Execute_Throw_RequiredBlockIndexException_With_StakeState( long startedBlockIndex, long? receivedBlockIndex, long blockIndex) { - var stakeAddr = StakeState.DeriveAddress(AgentAddr); - var stakeState = new StakeState(stakeAddr, startedBlockIndex); + var stakeAddr = LegacyStakeState.DeriveAddress(AgentAddr); + var stakeState = new LegacyStakeState(stakeAddr, startedBlockIndex); if (receivedBlockIndex is not null) { stakeState.Claim((long)receivedBlockIndex); @@ -417,7 +417,7 @@ public void Execute_Throw_RequiredBlockIndexException_With_StakeStateV2( long? receivedBlockIndex, long blockIndex) { - var stakeAddr = StakeStateV2.DeriveAddress(AgentAddr); + var stakeAddr = StakeState.DeriveAddress(AgentAddr); var stakeStateV2 = PrepareStakeStateV2( _stakePolicySheet, startedBlockIndex, @@ -437,7 +437,7 @@ public void Execute_Throw_RequiredBlockIndexException_With_StakeStateV2( [Fact] public void Execute_Throw_FailedLoadStateException_When_Sheet_Null() { - var stakeAddr = StakeStateV2.DeriveAddress(AgentAddr); + var stakeAddr = StakeState.DeriveAddress(AgentAddr); var stakeStateV2 = PrepareStakeStateV2(_stakePolicySheet, 0, null); var blockIndex = stakeStateV2.StartedBlockIndex + stakeStateV2.Contract.RewardInterval; var prevState = _initialState @@ -477,7 +477,7 @@ public void Execute_Throw_FailedLoadStateException_When_Sheet_Null() [InlineData(0)] public void Execute_Throw_InsufficientBalanceException(long stakedBalance) { - var stakeAddr = StakeStateV2.DeriveAddress(AgentAddr); + var stakeAddr = StakeState.DeriveAddress(AgentAddr); var stakeStateV2 = PrepareStakeStateV2(_stakePolicySheet, 0, null); var blockIndex = stakeStateV2.StartedBlockIndex + stakeStateV2.Contract.RewardInterval; var previousState = _initialState.SetLegacyState(stakeAddr, stakeStateV2.Serialize()); @@ -498,7 +498,7 @@ public void Execute_Throw_InsufficientBalanceException(long stakedBalance) [Fact] public void Execute_Throw_ArgumentNullException_When_Reward_CurrencyTicker_Null() { - var stakeAddr = StakeStateV2.DeriveAddress(AgentAddr); + var stakeAddr = StakeState.DeriveAddress(AgentAddr); var stakeStateV2 = PrepareStakeStateV2(_stakePolicySheet, 0, null); var blockIndex = stakeStateV2.StartedBlockIndex + stakeStateV2.Contract.RewardInterval; var prevState = _initialState @@ -528,7 +528,7 @@ public void Execute_Throw_ArgumentNullException_When_Reward_CurrencyTicker_Null( public void Execute_Throw_ArgumentNullException_When_Reward_CurrencyTicker_New_CurrencyDecimalPlaces_Null() { - var stakeAddr = StakeStateV2.DeriveAddress(AgentAddr); + var stakeAddr = StakeState.DeriveAddress(AgentAddr); var stakeStateV2 = PrepareStakeStateV2(_stakePolicySheet, 0, null); var blockIndex = stakeStateV2.StartedBlockIndex + stakeStateV2.Contract.RewardInterval; var prevState = _initialState @@ -570,8 +570,8 @@ public void Execute_Success_With_StakeState( (Address balanceAddr, FungibleAssetValue fav)[] expectedBalances, (int itemSheetId, int count)[] expectedItems) { - var stakeAddr = StakeState.DeriveAddress(AgentAddr); - var stakeState = new StakeState(stakeAddr, startedBlockIndex); + var stakeAddr = LegacyStakeState.DeriveAddress(AgentAddr); + var stakeState = new LegacyStakeState(stakeAddr, startedBlockIndex); if (receivedBlockIndex is not null) { stakeState.Claim((long)receivedBlockIndex); @@ -606,7 +606,7 @@ public void Execute_Success_With_StakeStateV2( (Address balanceAddr, FungibleAssetValue fav)[] expectedBalances, (int itemSheetId, int count)[] expectedItems) { - var stakeAddr = StakeStateV2.DeriveAddress(AgentAddr); + var stakeAddr = StakeState.DeriveAddress(AgentAddr); var stakeStateV2 = PrepareStakeStateV2( _stakePolicySheet, startedBlockIndex, @@ -634,7 +634,7 @@ public void Execute_Success_With_StakeStateV2( public void Execute_V6() { var prevState = _initialState; - var stakeAddr = StakeStateV2.DeriveAddress(AgentAddr); + var stakeAddr = StakeState.DeriveAddress(AgentAddr); var stakePolicySheet = new StakePolicySheet(); stakePolicySheet.Set(StakePolicySheetFixtures.V6); var stakeStateV2 = PrepareStakeStateV2( @@ -685,15 +685,15 @@ public void Execute_V6() } } - private static StakeStateV2 PrepareStakeStateV2( + private static StakeState PrepareStakeStateV2( StakePolicySheet stakePolicySheet, long startedBlockIndex, long? receivedBlockIndex) { var contract = new Contract(stakePolicySheet); return receivedBlockIndex is null - ? new StakeStateV2(contract, startedBlockIndex) - : new StakeStateV2(contract, startedBlockIndex, receivedBlockIndex.Value); + ? new StakeState(contract, startedBlockIndex) + : new StakeState(contract, startedBlockIndex, receivedBlockIndex.Value); } private static IWorld Execute( @@ -702,7 +702,7 @@ private static IWorld Execute( Address avatarAddr, long blockIndex) { - var stakeAddr = StakeStateV2.DeriveAddress(agentAddr); + var stakeAddr = StakeState.DeriveAddress(agentAddr); var ncg = prevState.GetGoldCurrency(); var prevBalance = prevState.GetBalance(agentAddr, ncg); var prevStakedBalance = prevState.GetBalance(stakeAddr, ncg); @@ -717,7 +717,7 @@ private static IWorld Execute( Assert.Equal(prevBalance, nextBalance); var nextStakedBalance = nextState.GetBalance(stakeAddr, ncg); Assert.Equal(prevStakedBalance, nextStakedBalance); - Assert.True(nextState.TryGetStakeStateV2(agentAddr, out var stakeStateV2)); + Assert.True(nextState.TryGetStakeState(agentAddr, out var stakeStateV2)); Assert.Equal(blockIndex, stakeStateV2.ReceivedBlockIndex); Assert.True(stakeStateV2.ClaimedBlockIndex <= blockIndex); Assert.True(stakeStateV2.ClaimableBlockIndex > blockIndex); diff --git a/.Lib9c.Tests/Action/GrindingTest.cs b/.Lib9c.Tests/Action/GrindingTest.cs index 11a941d080..6e0fb6d6b9 100644 --- a/.Lib9c.Tests/Action/GrindingTest.cs +++ b/.Lib9c.Tests/Action/GrindingTest.cs @@ -13,6 +13,7 @@ namespace Lib9c.Tests.Action using Nekoyume.Action; using Nekoyume.Model.Item; using Nekoyume.Model.Mail; + using Nekoyume.Model.Stake; using Nekoyume.Model.State; using Nekoyume.Module; using Nekoyume.TableData; @@ -138,8 +139,8 @@ public void Execute_Success_With_StakeState( Assert.Equal(0 * _crystalCurrency, state.GetBalance(_avatarAddress, _crystalCurrency)); // StakeState; - var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); - var stakeState = new StakeState(stakeStateAddress, 1); + var stakeStateAddress = LegacyStakeState.DeriveAddress(_agentAddress); + var stakeState = new LegacyStakeState(stakeStateAddress, 1); var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows .FirstOrDefault(r => r.Level == monsterCollectLevel)?.RequiredGold ?? 0; diff --git a/.Lib9c.Tests/Action/Guild/AcceptGuildApplicationTest.cs b/.Lib9c.Tests/Action/Guild/AcceptGuildApplicationTest.cs deleted file mode 100644 index 8b6215d070..0000000000 --- a/.Lib9c.Tests/Action/Guild/AcceptGuildApplicationTest.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace Lib9c.Tests.Action.Guild -{ - using System; - using Lib9c.Tests.Util; - using Libplanet.Action.State; - using Libplanet.Mocks; - using Nekoyume.Action.Guild; - using Nekoyume.Module.Guild; - using Xunit; - - public class AcceptGuildApplicationTest - { - [Fact] - public void Serialization() - { - var agentAddress = AddressUtil.CreateAgentAddress(); - var action = new AcceptGuildApplication(agentAddress); - var plainValue = action.PlainValue; - - var deserialized = new AcceptGuildApplication(); - deserialized.LoadPlainValue(plainValue); - Assert.Equal(agentAddress, deserialized.Target); - } - - [Fact] - public void Execute() - { - var appliedMemberAddress = AddressUtil.CreateAgentAddress(); - var nonAppliedMemberAddress = AddressUtil.CreateAgentAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var guildAddress = AddressUtil.CreateGuildAddress(); - - IWorld world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .ApplyGuild(appliedMemberAddress, guildAddress); - - // These cases should fail because the member didn't apply the guild and - // non-guild-master-addresses cannot accept the guild application. - Assert.Throws( - () => new AcceptGuildApplication(nonAppliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = guildMasterAddress, - })); - Assert.Throws( - () => new AcceptGuildApplication(nonAppliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = appliedMemberAddress, - })); - Assert.Throws( - () => new AcceptGuildApplication(nonAppliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = nonAppliedMemberAddress, - })); - - // These cases should fail because non-guild-master-addresses cannot accept the guild application. - Assert.Throws( - () => new AcceptGuildApplication(appliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = appliedMemberAddress, - })); - Assert.Throws( - () => new AcceptGuildApplication(appliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = nonAppliedMemberAddress, - })); - - world = new AcceptGuildApplication(appliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = guildMasterAddress, - }); - - Assert.False(world.TryGetGuildApplication(appliedMemberAddress, out _)); - Assert.Equal(guildAddress, world.GetJoinedGuild(appliedMemberAddress)); - } - } -} diff --git a/.Lib9c.Tests/Action/Guild/ApplyGuildTest.cs b/.Lib9c.Tests/Action/Guild/ApplyGuildTest.cs deleted file mode 100644 index e978cf4a68..0000000000 --- a/.Lib9c.Tests/Action/Guild/ApplyGuildTest.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace Lib9c.Tests.Action.Guild -{ - using System; - using Lib9c.Tests.Util; - using Libplanet.Action.State; - using Libplanet.Mocks; - using Nekoyume.Action.Guild; - using Nekoyume.Action.Loader; - using Nekoyume.Module.Guild; - using Xunit; - - public class ApplyGuildTest - { - [Fact] - public void Serialization() - { - var guildAddress = AddressUtil.CreateGuildAddress(); - var action = new ApplyGuild(guildAddress); - var plainValue = action.PlainValue; - - var deserialized = new ApplyGuild(); - deserialized.LoadPlainValue(plainValue); - Assert.Equal(guildAddress, deserialized.GuildAddress); - } - - [Fact] - public void Execute() - { - var agentAddress = AddressUtil.CreateAgentAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var guildAddress = AddressUtil.CreateGuildAddress(); - var action = new ApplyGuild(guildAddress); - - IWorld world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMasterAddress); - IWorld bannedWorld = world.Ban(guildAddress, guildMasterAddress, agentAddress); - - // This case should fail because the agent is banned by the guild. - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = bannedWorld, - Signer = agentAddress, - })); - - world = action.Execute(new ActionContext - { - PreviousState = world, - Signer = agentAddress, - }); - - Assert.True(world.TryGetGuildApplication(agentAddress, out var application)); - Assert.Equal(guildAddress, application.GuildAddress); - } - } -} diff --git a/.Lib9c.Tests/Action/Guild/BanGuildMemberTest.cs b/.Lib9c.Tests/Action/Guild/BanGuildMemberTest.cs index 114d7ff350..16f8ed521b 100644 --- a/.Lib9c.Tests/Action/Guild/BanGuildMemberTest.cs +++ b/.Lib9c.Tests/Action/Guild/BanGuildMemberTest.cs @@ -3,12 +3,18 @@ namespace Lib9c.Tests.Action.Guild using System; using Lib9c.Tests.Util; using Libplanet.Action.State; + using Libplanet.Crypto; using Libplanet.Mocks; + using Libplanet.Types.Assets; + using Nekoyume; using Nekoyume.Action.Guild; + using Nekoyume.Model.Guild; + using Nekoyume.Model.State; + using Nekoyume.Module; using Nekoyume.Module.Guild; using Xunit; - public class BanGuildMemberTest + public class BanGuildMemberTest : GuildTestBase { [Fact] public void Serialization() @@ -22,10 +28,38 @@ public void Serialization() Assert.Equal(guildMemberAddress, deserialized.Target); } + [Fact] + public void Execute() + { + var validatorKey = new PrivateKey(); + var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); + var guildAddress = AddressUtil.CreateGuildAddress(); + + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, 1L); + + var banGuildMember = new BanGuildMember(targetGuildMemberAddress); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = guildMasterAddress, + }; + world = banGuildMember.Execute(actionContext); + + var repository = new GuildRepository(world, actionContext); + Assert.True(repository.IsBanned(guildAddress, targetGuildMemberAddress)); + Assert.Null(repository.GetJoinedGuild(targetGuildMemberAddress)); + } + // Expected use-case. [Fact] public void Ban_By_GuildMaster() { + var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); var otherGuildMasterAddress = AddressUtil.CreateAgentAddress(); var guildMemberAddress = AddressUtil.CreateAgentAddress(); @@ -33,82 +67,88 @@ public void Ban_By_GuildMaster() var guildAddress = AddressUtil.CreateGuildAddress(); var otherGuildAddress = AddressUtil.CreateGuildAddress(); - IWorld world = new World(MockWorldState.CreateModern()); - world = world.MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMemberAddress); - world = world.MakeGuild(otherGuildAddress, otherGuildMasterAddress) - .JoinGuild(otherGuildAddress, otherGuildMemberAddress); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, 1L); + world = EnsureToMakeGuild(world, otherGuildAddress, otherGuildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, otherGuildAddress, otherGuildMemberAddress, 1L); + var repository = new GuildRepository(world, new ActionContext()); // Guild - Assert.False(world.IsBanned(guildAddress, guildMasterAddress)); - Assert.Equal(guildAddress, world.GetJoinedGuild(guildMasterAddress)); - Assert.False(world.IsBanned(guildAddress, guildMemberAddress)); - Assert.Equal(guildAddress, world.GetJoinedGuild(guildMemberAddress)); + Assert.False(repository.IsBanned(guildAddress, guildMasterAddress)); + Assert.Equal(guildAddress, repository.GetJoinedGuild(guildMasterAddress)); + Assert.False(repository.IsBanned(guildAddress, guildMemberAddress)); + Assert.Equal(guildAddress, repository.GetJoinedGuild(guildMemberAddress)); // Other guild - Assert.False(world.IsBanned(guildAddress, otherGuildMasterAddress)); - Assert.Equal(otherGuildAddress, world.GetJoinedGuild(otherGuildMasterAddress)); - Assert.False(world.IsBanned(guildAddress, otherGuildMemberAddress)); - Assert.Equal(otherGuildAddress, world.GetJoinedGuild(otherGuildMemberAddress)); + Assert.False(repository.IsBanned(guildAddress, otherGuildMasterAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMasterAddress)); + Assert.False(repository.IsBanned(guildAddress, otherGuildMemberAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMemberAddress)); var action = new BanGuildMember(guildMemberAddress); world = action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = guildMasterAddress, }); // Guild - Assert.False(world.IsBanned(guildAddress, guildMasterAddress)); - Assert.Equal(guildAddress, world.GetJoinedGuild(guildMasterAddress)); - Assert.True(world.IsBanned(guildAddress, guildMemberAddress)); - Assert.Null(world.GetJoinedGuild(guildMemberAddress)); + repository.UpdateWorld(world); + Assert.False(repository.IsBanned(guildAddress, guildMasterAddress)); + Assert.Equal(guildAddress, repository.GetJoinedGuild(guildMasterAddress)); + Assert.True(repository.IsBanned(guildAddress, guildMemberAddress)); + Assert.Null(repository.GetJoinedGuild(guildMemberAddress)); // Other guild - Assert.False(world.IsBanned(guildAddress, otherGuildMasterAddress)); - Assert.Equal(otherGuildAddress, world.GetJoinedGuild(otherGuildMasterAddress)); - Assert.False(world.IsBanned(guildAddress, otherGuildMemberAddress)); - Assert.Equal(otherGuildAddress, world.GetJoinedGuild(otherGuildMemberAddress)); + Assert.False(repository.IsBanned(guildAddress, otherGuildMasterAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMasterAddress)); + Assert.False(repository.IsBanned(guildAddress, otherGuildMemberAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMemberAddress)); action = new BanGuildMember(otherGuildMasterAddress); world = action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = guildMasterAddress, }); // Guild - Assert.False(world.IsBanned(guildAddress, guildMasterAddress)); - Assert.Equal(guildAddress, world.GetJoinedGuild(guildMasterAddress)); - Assert.True(world.IsBanned(guildAddress, guildMemberAddress)); - Assert.Null(world.GetJoinedGuild(guildMemberAddress)); + repository.UpdateWorld(world); + Assert.False(repository.IsBanned(guildAddress, guildMasterAddress)); + Assert.Equal(guildAddress, repository.GetJoinedGuild(guildMasterAddress)); + Assert.True(repository.IsBanned(guildAddress, guildMemberAddress)); + Assert.Null(repository.GetJoinedGuild(guildMemberAddress)); // Other guild - Assert.True(world.IsBanned(guildAddress, otherGuildMasterAddress)); - Assert.Equal(otherGuildAddress, world.GetJoinedGuild(otherGuildMasterAddress)); - Assert.False(world.IsBanned(guildAddress, otherGuildMemberAddress)); - Assert.Equal(otherGuildAddress, world.GetJoinedGuild(otherGuildMemberAddress)); + Assert.True(repository.IsBanned(guildAddress, otherGuildMasterAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMasterAddress)); + Assert.False(repository.IsBanned(guildAddress, otherGuildMemberAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMemberAddress)); action = new BanGuildMember(otherGuildMemberAddress); world = action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = guildMasterAddress, }); // Guild - Assert.False(world.IsBanned(guildAddress, guildMasterAddress)); - Assert.Equal(guildAddress, world.GetJoinedGuild(guildMasterAddress)); - Assert.True(world.IsBanned(guildAddress, guildMemberAddress)); - Assert.Null(world.GetJoinedGuild(guildMemberAddress)); + repository.UpdateWorld(world); + Assert.False(repository.IsBanned(guildAddress, guildMasterAddress)); + Assert.Equal(guildAddress, repository.GetJoinedGuild(guildMasterAddress)); + Assert.True(repository.IsBanned(guildAddress, guildMemberAddress)); + Assert.Null(repository.GetJoinedGuild(guildMemberAddress)); // Other guild - Assert.True(world.IsBanned(guildAddress, otherGuildMasterAddress)); - Assert.Equal(otherGuildAddress, world.GetJoinedGuild(otherGuildMasterAddress)); - Assert.True(world.IsBanned(guildAddress, otherGuildMemberAddress)); - Assert.Equal(otherGuildAddress, world.GetJoinedGuild(otherGuildMemberAddress)); + Assert.True(repository.IsBanned(guildAddress, otherGuildMasterAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMasterAddress)); + Assert.True(repository.IsBanned(guildAddress, otherGuildMemberAddress)); + Assert.Equal(otherGuildAddress, repository.GetJoinedGuild(otherGuildMemberAddress)); action = new BanGuildMember(guildMasterAddress); // GuildMaster cannot ban itself. Assert.Throws(() => action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = guildMasterAddress, })); } @@ -116,6 +156,7 @@ public void Ban_By_GuildMaster() [Fact] public void Ban_By_GuildMember() { + var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); var guildMemberAddress = AddressUtil.CreateAgentAddress(); var otherAddress = AddressUtil.CreateAgentAddress(); @@ -124,15 +165,19 @@ public void Ban_By_GuildMember() var action = new BanGuildMember(targetGuildMemberAddress); - IWorld world = new World(MockWorldState.CreateModern()); - world = world.MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMemberAddress) - .JoinGuild(guildAddress, targetGuildMemberAddress); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, 1L); + world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, 1L); + + var repository = new GuildRepository(world, new ActionContext()); // GuildMember tries to ban other guild member. Assert.Throws(() => action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = guildMemberAddress, })); @@ -140,7 +185,7 @@ public void Ban_By_GuildMember() action = new BanGuildMember(guildMemberAddress); Assert.Throws(() => action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = guildMemberAddress, })); @@ -148,7 +193,7 @@ public void Ban_By_GuildMember() // GuildMember tries to ban other not joined to its guild. Assert.Throws(() => action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = guildMemberAddress, })); } @@ -158,20 +203,25 @@ public void Ban_By_Other() { // NOTE: It assumes 'other' hasn't any guild. If 'other' has its own guild, // it should be assumed as a guild master. + var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); var otherAddress = AddressUtil.CreateAgentAddress(); var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - IWorld world = new World(MockWorldState.CreateModern()); - world = world.MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, targetGuildMemberAddress); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, 1L); + + var repository = new GuildRepository(world, new ActionContext()); // Other tries to ban GuildMember. var action = new BanGuildMember(targetGuildMemberAddress); Assert.Throws(() => action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = otherAddress, })); @@ -183,28 +233,5 @@ public void Ban_By_Other() Signer = otherAddress, })); } - - [Fact] - public void RejectGuildApplication() - { - var guildAddress = AddressUtil.CreateGuildAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var agentAddress = AddressUtil.CreateAgentAddress(); - - IWorld world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .ApplyGuild(agentAddress, guildAddress); - Assert.True(world.TryGetGuildApplication(agentAddress, out _)); - - var action = new BanGuildMember(agentAddress); - world = action.Execute(new ActionContext - { - PreviousState = world, - Signer = guildMasterAddress, - }); - - Assert.True(world.IsBanned(guildAddress, agentAddress)); - Assert.False(world.TryGetGuildApplication(agentAddress, out _)); - } } } diff --git a/.Lib9c.Tests/Action/Guild/CancelGuildApplicationTest.cs b/.Lib9c.Tests/Action/Guild/CancelGuildApplicationTest.cs deleted file mode 100644 index d6ea8ee7b5..0000000000 --- a/.Lib9c.Tests/Action/Guild/CancelGuildApplicationTest.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace Lib9c.Tests.Action.Guild -{ - using System; - using Lib9c.Tests.Util; - using Libplanet.Action.State; - using Libplanet.Crypto; - using Libplanet.Mocks; - using Nekoyume.Action.Guild; - using Nekoyume.Module.Guild; - using Nekoyume.TypedAddress; - using Xunit; - - public class CancelGuildApplicationTest - { - [Fact] - public void Serialization() - { - var action = new CancelGuildApplication(); - var plainValue = action.PlainValue; - - var deserialized = new CancelGuildApplication(); - deserialized.LoadPlainValue(plainValue); - } - - [Fact] - public void Execute() - { - var privateKey = new PrivateKey(); - var signer = new AgentAddress(privateKey.Address); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var guildAddress = AddressUtil.CreateGuildAddress(); - - var action = new CancelGuildApplication(); - IWorld world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress); - Assert.Throws( - () => action.Execute(new ActionContext - { - PreviousState = world, - Signer = signer, - })); - - var otherAddress = AddressUtil.CreateAgentAddress(); - world = world.ApplyGuild(otherAddress, guildAddress); - - // It should fail because other agent applied the guild but the signer didn't apply. - Assert.Throws( - () => action.Execute(new ActionContext - { - PreviousState = world, - Signer = signer, - })); - - world = world.ApplyGuild(signer, guildAddress); - world = action.Execute(new ActionContext - { - PreviousState = world, - Signer = signer, - }); - - Assert.False(world.TryGetGuildApplication(signer, out _)); - } - } -} diff --git a/.Lib9c.Tests/Action/Guild/ClaimRewardTest.cs b/.Lib9c.Tests/Action/Guild/ClaimRewardTest.cs new file mode 100644 index 0000000000..ce71229d70 --- /dev/null +++ b/.Lib9c.Tests/Action/Guild/ClaimRewardTest.cs @@ -0,0 +1,490 @@ +#nullable enable +namespace Lib9c.Tests.Action.Guild; + +using System; +using System.Collections.Generic; +using System.Linq; +using Lib9c.Tests.Action.ValidatorDelegation; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.Guild; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.Model.Guild; +using Nekoyume.Model.Stake; +using Nekoyume.Module.Guild; +using Nekoyume.TypedAddress; +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class ClaimRewardTest : ValidatorDelegationTestBase +{ + private interface IClaimRewardFixture + { + FungibleAssetValue TotalGuildAllocateReward { get; } + + FungibleAssetValue TotalAllocateReward { get; } + + PrivateKey ValidatorKey { get; } + + FungibleAssetValue ValidatorBalance { get; } + + FungibleAssetValue ValidatorCash { get; } + + DelegatorInfo[] DelegatorInfos { get; } + + GuildParticipantInfo[] GuildParticipantInfos { get; } + + PrivateKey[] DelegatorKeys => DelegatorInfos.Select(i => i.Key).ToArray(); + + PrivateKey[] GuildParticipantKeys => GuildParticipantInfos.Select(i => i.Key).ToArray(); + + FungibleAssetValue[] DelegatorBalances => DelegatorInfos.Select(i => i.Balance).ToArray(); + + FungibleAssetValue[] GuildParticipantBalances => GuildParticipantInfos.Select(i => i.Balance).ToArray(); + } + + public static IEnumerable RandomSeeds => new List + { + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + }; + + [Fact] + public void Serialization() + { + var action = new ClaimGuildReward(); + var plainValue = action.PlainValue; + + var deserialized = new ClaimGuildReward(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var validatorGold = DelegationCurrency * 10; + var allocatedGuildRewards = GuildAllocateRewardCurrency * 100; + var allocatedReward = AllocateRewardCurrency * 100; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureRewardAllocatedValidator(world, validatorKey, allocatedGuildRewards, allocatedReward, ref height); + + // When + var expectedBalance = allocatedGuildRewards; + var lastCommit = CreateLastCommit(validatorKey, height - 1); + var claimRewardValidator = new ClaimValidatorRewardSelf(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height++, + Signer = validatorKey.Address, + LastCommit = lastCommit, + }; + world = claimRewardValidator.Execute(actionContext); + + // Then + var actualBalance = world.GetBalance(validatorKey.Address, GuildAllocateRewardCurrency); + + Assert.Equal(expectedBalance, actualBalance); + } + + [Theory] + [InlineData(33.33, 33.33)] + [InlineData(11.11, 11.11)] + [InlineData(10, 10)] + [InlineData(1, 1)] + public void Execute_Theory_OneDelegator( + decimal totalGuildReward, + decimal totalReward) + { + var delegatorInfos = new[] + { + new DelegatorInfo + { + Key = new PrivateKey(), + Balance = DelegationCurrency * 100, + }, + }; + + var guildParticipantInfos = new[] + { + new GuildParticipantInfo + { + Key = new PrivateKey(), + Balance = DelegationCurrency * 100, + GuildMasterAddress = delegatorInfos[0].Key.Address, + }, + }; + + var fixture = new StaticFixture + { + DelegatorLength = 1, + TotalGuildAllocateReward = FungibleAssetValue.Parse(GuildAllocateRewardCurrency, $"{totalGuildReward}"), + TotalAllocateReward = FungibleAssetValue.Parse(RewardCurrency, $"{totalReward}"), + ValidatorKey = new PrivateKey(), + ValidatorBalance = DelegationCurrency * 100, + ValidatorCash = DelegationCurrency * 10, + DelegatorInfos = delegatorInfos, + GuildParticipantInfos = guildParticipantInfos, + }; + ExecuteWithFixture(fixture); + } + + [Theory] + [InlineData(0.1, 0.1)] + [InlineData(1, 1)] + [InlineData(3, 3)] + [InlineData(5, 5)] + [InlineData(7, 7)] + [InlineData(9, 9)] + [InlineData(11.11, 11.11)] + [InlineData(11.12, 11.12)] + [InlineData(33.33, 33.33)] + [InlineData(33.34, 33.34)] + [InlineData(34.27, 34.27)] + [InlineData(34.28, 34.28)] + [InlineData(34.29, 34.29)] + public void Execute_Theory_TwoDelegators( + decimal totalGuildReward, + decimal totalReward) + { + var delegatorInfos = new[] + { + new DelegatorInfo + { + Key = new PrivateKey(), + Balance = DelegationCurrency * 100, + }, + new DelegatorInfo + { + Key = new PrivateKey(), + Balance = DelegationCurrency * 100, + }, + }; + + var guildParticipantInfos = new[] + { + new GuildParticipantInfo + { + Key = new PrivateKey(), + Balance = DelegationCurrency * 100, + GuildMasterAddress = delegatorInfos[0].Key.Address, + }, + new GuildParticipantInfo + { + Key = new PrivateKey(), + Balance = DelegationCurrency * 100, + GuildMasterAddress = delegatorInfos[1].Key.Address, + }, + }; + + var fixture = new StaticFixture + { + DelegatorLength = 2, + TotalGuildAllocateReward = FungibleAssetValue.Parse(GuildAllocateRewardCurrency, $"{totalGuildReward}"), + TotalAllocateReward = FungibleAssetValue.Parse(AllocateRewardCurrency, $"{totalReward}"), + ValidatorKey = new PrivateKey(), + ValidatorBalance = DelegationCurrency * 100, + ValidatorCash = DelegationCurrency * 10, + DelegatorInfos = delegatorInfos, + GuildParticipantInfos = guildParticipantInfos, + }; + ExecuteWithFixture(fixture); + } + + [Theory] + [InlineData(0)] + [InlineData(123)] + [InlineData(34352535)] + public void Execute_Theory_WithStaticSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Theory] + [MemberData(nameof(RandomSeeds))] + public void Execute_Theory_WithRandomSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + private void ExecuteWithFixture(IClaimRewardFixture fixture) + { + // Given + var length = fixture.DelegatorInfos.Length; + var world = World; + var validatorKey = fixture.ValidatorKey; + var delegatorKeys = fixture.DelegatorKeys; + var guildParticipantInfos = fixture.GuildParticipantInfos; + var guildParticipantKeys = fixture.GuildParticipantKeys; + var delegatorBalances = fixture.DelegatorBalances; + var height = 1L; + var actionContext = new ActionContext(); + var validatorBalance = fixture.ValidatorBalance; + var validatorCash = fixture.ValidatorCash; + var totalGuildReward = fixture.TotalGuildAllocateReward; + var totalReward = fixture.TotalAllocateReward; + int seed = 0; + world = EnsureToMintAsset(world, validatorKey, validatorBalance, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorCash, height++); + world = EnsureToMintAssets(world, delegatorKeys, delegatorBalances, height++); + world = delegatorKeys.Aggregate(world, (w, d) => EnsureMakeGuild( + w, d.Address, validatorKey.Address, height++, seed++)); + world = guildParticipantInfos.Aggregate(world, (w, i) => EnsureJoinGuild( + w, i.Key.Address, i.GuildMasterAddress, validatorKey.Address, height++)); + + world = EnsureRewardAllocatedValidator(world, validatorKey, totalGuildReward, totalReward, ref height); + + // Calculate expected values for comparison with actual values. + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedGuildRepository = new GuildRepository(expectedRepository); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee(validatorKey.Address); + var expectedTotalShares = expectedDelegatee.TotalShares; + var expectedValidatorShare + = expectedRepository.GetBond(expectedDelegatee, validatorKey.Address).Share; + var expectedDelegatorShares = delegatorKeys + .Select(item => expectedRepository.GetBond( + expectedDelegatee, + (Address)expectedGuildRepository.GetJoinedGuild(new AgentAddress(item.Address))!).Share) + .ToArray(); + var expectedProposerReward + = CalculatePropserReward(totalGuildReward) + CalculateBonusPropserReward(1, 1, totalGuildReward); + var expectedGuildReward = totalGuildReward - expectedProposerReward; + var expectedCommission = CalculateCommission( + expectedGuildReward, expectedDelegatee.CommissionPercentage); + var expectedGuildClaim = expectedGuildReward - expectedCommission; + var expectedValidatorGuildClaim = CalculateClaim( + expectedValidatorShare, expectedTotalShares, expectedGuildClaim); + var expectedDelegatorGuildClaims = CreateArray( + length, + i => CalculateClaim(expectedDelegatorShares[i], expectedTotalShares, expectedGuildClaim)); + var expectedValidatorBalance = validatorBalance; + expectedValidatorBalance -= validatorCash; + var expectedValidatorGuildReward = expectedProposerReward; + expectedValidatorGuildReward += expectedCommission; + expectedValidatorGuildReward += expectedValidatorGuildClaim; + var expectedDelegatorBalances = CreateArray(length, i => DelegationCurrency * 0); + var expectedRemainGuildReward = totalGuildReward; + expectedRemainGuildReward -= expectedProposerReward; + expectedRemainGuildReward -= expectedCommission; + expectedRemainGuildReward -= expectedValidatorGuildClaim; + for (var i = 0; i < length; i++) + { + expectedRemainGuildReward -= expectedDelegatorGuildClaims[i]; + } + + var expectedValidatorReward = totalReward.DivRem(10).Quotient; + var expectedValidatorClaim = expectedValidatorReward; + var expectedTotalGuildRewards = (totalReward - expectedValidatorReward).DivRem(10).Quotient; + var expectedTotalGuildParticipantRewards = totalReward - expectedValidatorReward - expectedTotalGuildRewards; + var expectedGuildClaims = CreateArray( + length, + i => CalculateClaim(expectedDelegatorShares[i], expectedTotalShares, expectedTotalGuildRewards)); + expectedValidatorClaim += CalculateClaim(expectedValidatorShare, expectedTotalShares, expectedTotalGuildRewards); + var expectedGuildParticipantClaims = CreateArray( + length, + i => CalculateClaim(expectedDelegatorShares[i], expectedTotalShares, expectedTotalGuildParticipantRewards)); + expectedValidatorClaim += CalculateClaim(expectedValidatorShare, expectedTotalShares, expectedTotalGuildParticipantRewards); + var expectedRemainReward = totalReward; + expectedRemainReward -= expectedValidatorClaim; + for (var i = 0; i < length; i++) + { + expectedRemainReward -= expectedGuildClaims[i]; + } + + for (var i = 0; i < length; i++) + { + expectedRemainReward -= expectedGuildParticipantClaims[i]; + } + + // When + var lastCommit = CreateLastCommit(validatorKey, height - 1); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height++, + Signer = validatorKey.Address, + LastCommit = lastCommit, + }; + world = new ClaimValidatorRewardSelf().Execute(actionContext); + + for (var i = 0; i < length; i++) + { + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height++, + Signer = delegatorKeys[i].Address, + LastCommit = lastCommit, + }; + world = new ClaimGuildReward().Execute(actionContext); + } + + for (var i = 0; i < length; i++) + { + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height++, + Signer = delegatorKeys[i].Address, + LastCommit = lastCommit, + }; + world = new ClaimReward().Execute(actionContext); + } + + // Then + var validatorRepository = new ValidatorRepository(world, actionContext); + var guildRepository = new GuildRepository(world, actionContext); + var delegatee = validatorRepository.GetValidatorDelegatee(validatorKey.Address); + var actualRemainGuildReward = world.GetBalance(delegatee.RewardRemainderPoolAddress, GuildAllocateRewardCurrency); + var actualValidatorBalance = world.GetBalance(StakeState.DeriveAddress(validatorKey.Address), DelegationCurrency); + var actualValidatorGuildReward = world.GetBalance(validatorKey.Address, GuildAllocateRewardCurrency); + var actualDelegatorBalances = delegatorKeys + .Select(item => world.GetBalance(item.Address, DelegationCurrency)) + .ToArray(); + var actualDelegatorGuildRewards = delegatorKeys + .Select(item => world.GetBalance( + guildRepository.GetJoinedGuild( + new AgentAddress(item.Address)) + ?? throw new Exception($"Delegator {item.Address} does not joind to guild."), + GuildAllocateRewardCurrency)) + .ToArray(); + + var actualRemainReward = world.GetBalance(delegatee.RewardRemainderPoolAddress, AllocateRewardCurrency); + var actualValidatorReward = world.GetBalance(validatorKey.Address, AllocateRewardCurrency); + var actualGuildRewards = delegatorKeys + .Select(item => world.GetBalance( + guildRepository.GetJoinedGuild( + new AgentAddress(item.Address)) + ?? throw new Exception($"Delegator {item.Address} does not joind to guild."), + AllocateRewardCurrency)) + .ToArray(); + var actualGuildParticipantRewards = delegatorKeys + .Select(item => world.GetBalance( + item.Address, + AllocateRewardCurrency)) + .ToArray(); + + Assert.Equal(expectedRemainGuildReward, actualRemainGuildReward); + Assert.Equal(expectedValidatorBalance, actualValidatorBalance); + Assert.Equal(expectedDelegatorBalances, actualDelegatorBalances); + Assert.Equal(expectedValidatorGuildReward, actualValidatorGuildReward); + Assert.Equal(expectedDelegatorGuildClaims, actualDelegatorGuildRewards); + Assert.Equal(expectedValidatorClaim, actualValidatorReward); + Assert.Equal(expectedGuildClaims, actualGuildRewards); + Assert.Equal(expectedGuildParticipantClaims, actualGuildParticipantRewards); + Assert.Equal(expectedRemainReward, actualRemainReward); + + foreach (var key in guildParticipantKeys) + { + Assert.Throws( + () => new ClaimGuildReward().Execute(new ActionContext + { + PreviousState = world, + BlockIndex = height++, + Signer = key.Address, + LastCommit = lastCommit, + })); + } + } + + private struct DelegatorInfo + { + public PrivateKey Key { get; set; } + + public FungibleAssetValue Balance { get; set; } + } + + private struct GuildParticipantInfo + { + public PrivateKey Key { get; set; } + + public FungibleAssetValue Balance { get; set; } + + public Address GuildMasterAddress { get; set; } + } + + private struct StaticFixture : IClaimRewardFixture + { + public int DelegatorLength { get; set; } + + public FungibleAssetValue TotalGuildAllocateReward { get; set; } + + public FungibleAssetValue TotalAllocateReward { get; set; } + + public PrivateKey ValidatorKey { get; set; } + + public FungibleAssetValue ValidatorBalance { get; set; } + + public FungibleAssetValue ValidatorCash { get; set; } + + public DelegatorInfo[] DelegatorInfos { get; set; } + + public GuildParticipantInfo[] GuildParticipantInfos { get; set; } + } + + private class RandomFixture : IClaimRewardFixture + { + private readonly Random _random; + + public RandomFixture(int randomSeed) + { + _random = new Random(randomSeed); + DelegatorLength = _random.Next(3, 100); + GuildParticipantLength = _random.Next(1, 50); + ValidatorKey = new PrivateKey(); + TotalGuildAllocateReward = GetRandomFAV(GuildAllocateRewardCurrency, _random); + TotalAllocateReward = GetRandomFAV(AllocateRewardCurrency, _random); + ValidatorBalance = GetRandomFAV(DelegationCurrency, _random); + ValidatorCash = GetRandomCash(_random, ValidatorBalance); + DelegatorInfos = CreateArray(DelegatorLength, _ => + { + var balance = GetRandomFAV(DelegationCurrency, _random); + return new DelegatorInfo + { + Key = new PrivateKey(), + Balance = balance, + }; + }); + GuildParticipantInfos = CreateArray(GuildParticipantLength, _ => + { + var balance = GetRandomFAV(DelegationCurrency, _random); + return new GuildParticipantInfo + { + Key = new PrivateKey(), + Balance = balance, + GuildMasterAddress = DelegatorInfos[_random.Next(DelegatorLength)].Key.Address, + }; + }); + } + + public int DelegatorLength { get; } + + public int GuildParticipantLength { get; } + + public FungibleAssetValue TotalGuildAllocateReward { get; } + + public FungibleAssetValue TotalAllocateReward { get; } + + public PrivateKey ValidatorKey { get; } + + public FungibleAssetValue ValidatorBalance { get; } + + public FungibleAssetValue ValidatorCash { get; } + + public DelegatorInfo[] DelegatorInfos { get; } + + public GuildParticipantInfo[] GuildParticipantInfos { get; } + } +} diff --git a/.Lib9c.Tests/Action/Guild/GuildTestBase.cs b/.Lib9c.Tests/Action/Guild/GuildTestBase.cs new file mode 100644 index 0000000000..291c6aed39 --- /dev/null +++ b/.Lib9c.Tests/Action/Guild/GuildTestBase.cs @@ -0,0 +1,111 @@ +namespace Lib9c.Tests.Action.Guild +{ + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Mocks; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Model.Guild; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.Module.Guild; + using Nekoyume.Module.ValidatorDelegation; + using Nekoyume.TypedAddress; + using Nekoyume.ValidatorDelegation; + + public abstract class GuildTestBase + { + protected static readonly Currency GG = Currencies.GuildGold; + protected static readonly Currency Mead = Currencies.Mead; + protected static readonly Currency NCG = Currency.Uncapped("NCG", 2, null); + + public GuildTestBase() + { + var world = new World(MockUtil.MockModernWorldState); + var goldCurrencyState = new GoldCurrencyState(NCG); + World = world + .SetLegacyState(Addresses.GoldCurrency, goldCurrencyState.Serialize()); + } + + protected IWorld World { get; } + + protected static IWorld EnsureToMintAsset( + IWorld world, Address address, FungibleAssetValue amount) + { + var actionContext = new ActionContext + { + PreviousState = world, + }; + return world.MintAsset(actionContext, address, amount); + } + + protected static IWorld EnsureToCreateValidator( + IWorld world, + PublicKey validatorPublicKey) + { + var validatorAddress = validatorPublicKey.Address; + var commissionPercentage = 10; + var actionContext = new ActionContext + { + Signer = validatorAddress, + }; + + var validatorRepository = new ValidatorRepository(world, actionContext); + validatorRepository.CreateValidatorDelegatee(validatorPublicKey, commissionPercentage); + + var guildRepository = new GuildRepository(validatorRepository); + guildRepository.CreateGuildDelegatee(validatorAddress); + + return guildRepository.World; + } + + protected static IWorld EnsureToMakeGuild( + IWorld world, + GuildAddress guildAddress, + AgentAddress guildMasterAddress, + Address validatorAddress) + { + var actionContext = new ActionContext + { + Signer = guildMasterAddress, + BlockIndex = 0L, + }; + var repository = new GuildRepository(world, actionContext); + repository.MakeGuild(guildAddress, validatorAddress); + return repository.World; + } + + protected static IWorld EnsureToJoinGuild( + IWorld world, + GuildAddress guildAddress, + AgentAddress guildParticipantAddress, + long blockHeight) + { + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = guildParticipantAddress, + }; + + var repository = new GuildRepository(world, actionContext); + repository.JoinGuild(guildAddress, guildParticipantAddress); + return repository.World; + } + + protected static IWorld EnsureToBanGuildMember( + IWorld world, + GuildAddress guildAddress, + AgentAddress guildMasterAddress, + AgentAddress agentAddress) + { + var actionContext = new ActionContext + { + Signer = agentAddress, + }; + var repository = new GuildRepository(world, actionContext); + repository.Ban(guildAddress, guildMasterAddress, agentAddress); + return repository.World; + } + } +} diff --git a/.Lib9c.Tests/Action/Guild/JoinGuildTest.cs b/.Lib9c.Tests/Action/Guild/JoinGuildTest.cs new file mode 100644 index 0000000000..1a1f5fe1c8 --- /dev/null +++ b/.Lib9c.Tests/Action/Guild/JoinGuildTest.cs @@ -0,0 +1,51 @@ +namespace Lib9c.Tests.Action.Guild +{ + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Nekoyume.Action.Guild; + using Nekoyume.Model.Guild; + using Nekoyume.Module.Guild; + using Nekoyume.TypedAddress; + using Xunit; + + public class JoinGuildTest : GuildTestBase + { + [Fact] + public void Serialization() + { + var guildAddress = AddressUtil.CreateGuildAddress(); + var action = new JoinGuild(guildAddress); + var plainValue = action.PlainValue; + + var deserialized = new JoinGuild(); + deserialized.LoadPlainValue(plainValue); + Assert.Equal(guildAddress, deserialized.GuildAddress); + } + + [Fact] + public void Execute() + { + var validatorKey = new PrivateKey(); + var agentAddress = AddressUtil.CreateAgentAddress(); + var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var guildAddress = AddressUtil.CreateGuildAddress(); + + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = DelegationUtil.MakeGuild(world, guildMasterAddress, validatorKey.Address, 0L); + + world = new JoinGuild(guildAddress).Execute(new ActionContext + { + PreviousState = world, + Signer = agentAddress, + }); + + var repository = new GuildRepository(world, new ActionContext { }); + var guildParticipant = repository.GetGuildParticipant(agentAddress); + + Assert.Equal(agentAddress, guildParticipant.Address); + } + } +} diff --git a/.Lib9c.Tests/Action/Guild/MakeGuildTest.cs b/.Lib9c.Tests/Action/Guild/MakeGuildTest.cs index 78c089f567..65e648a03e 100644 --- a/.Lib9c.Tests/Action/Guild/MakeGuildTest.cs +++ b/.Lib9c.Tests/Action/Guild/MakeGuildTest.cs @@ -4,14 +4,19 @@ namespace Lib9c.Tests.Action.Guild using System.Collections.Generic; using Lib9c.Tests.Util; using Libplanet.Action.State; + using Libplanet.Crypto; using Libplanet.Mocks; + using Libplanet.Types.Assets; using Nekoyume; using Nekoyume.Action.Guild; + using Nekoyume.Model.Guild; + using Nekoyume.Model.State; + using Nekoyume.Module; using Nekoyume.Module.Guild; using Nekoyume.TypedAddress; using Xunit; - public class MakeGuildTest + public class MakeGuildTest : GuildTestBase { public static IEnumerable TestCases => new[] { @@ -38,34 +43,27 @@ public void Serialization() deserialized.LoadPlainValue(plainValue); } - [Theory] - [MemberData(nameof(TestCases))] - public void Execute(AgentAddress guildMasterAddress, bool fail) + [Fact] + public void Execute() { - var action = new MakeGuild(); - IWorld world = new World(MockUtil.MockModernWorldState); + IWorld world = World; + var validatorPrivateKey = new PrivateKey(); + var guildMasterAddress = AddressUtil.CreateAgentAddress(); + world = EnsureToMintAsset(world, validatorPrivateKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorPrivateKey.PublicKey); + var action = new MakeGuild(validatorPrivateKey.Address); - if (fail) - { - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = world, - Signer = guildMasterAddress, - })); - } - else + world = action.ExecutePublic(new ActionContext { - world = action.Execute(new ActionContext - { - PreviousState = world, - Signer = guildMasterAddress, - }); + PreviousState = world, + Signer = guildMasterAddress, + }); - var guildAddress = world.GetJoinedGuild(guildMasterAddress); - Assert.NotNull(guildAddress); - Assert.True(world.TryGetGuild(guildAddress.Value, out var guild)); - Assert.Equal(guildMasterAddress, guild.GuildMasterAddress); - } + var repository = new GuildRepository(world, new ActionContext()); + var guildAddress = repository.GetJoinedGuild(guildMasterAddress); + Assert.NotNull(guildAddress); + var guild = repository.GetGuild(guildAddress.Value); + Assert.Equal(guildMasterAddress, guild.GuildMasterAddress); } } } diff --git a/.Lib9c.Tests/Action/Guild/Migration/GuildMigrationCtrlTest.cs b/.Lib9c.Tests/Action/Guild/Migration/GuildMigrationCtrlTest.cs deleted file mode 100644 index 46c937548d..0000000000 --- a/.Lib9c.Tests/Action/Guild/Migration/GuildMigrationCtrlTest.cs +++ /dev/null @@ -1,95 +0,0 @@ -namespace Lib9c.Tests.Action.Guild.Migration -{ - using Bencodex.Types; - using Lib9c.Tests.Util; - using Libplanet.Action.State; - using Libplanet.Mocks; - using Nekoyume; - using Nekoyume.Action; - using Nekoyume.Action.Guild; - using Nekoyume.Action.Guild.Migration; - using Nekoyume.Action.Guild.Migration.Controls; - using Nekoyume.Extensions; - using Nekoyume.Model.State; - using Nekoyume.Module; - using Nekoyume.Module.Guild; - using Nekoyume.TypedAddress; - using Xunit; - - public class GuildMigrationCtrlTest - { - [Fact] - public void MigratePlanetariumPledgeToGuild_When_WithUnapprovedPledgeContract() - { - var guildMasterAddress = GuildConfig.PlanetariumGuildOwner; - var guildAddress = AddressUtil.CreateGuildAddress(); - var target = AddressUtil.CreateAgentAddress(); - var pledgeAddress = target.GetPledgeAddress(); - var world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMasterAddress) - .SetLegacyState(pledgeAddress, new List( - MeadConfig.PatronAddress.Serialize(), - false.Serialize(), // Unapproved - RequestPledge.DefaultRefillMead.Serialize())); - - Assert.Null(world.GetJoinedGuild(target)); - Assert.Throws(() => - GuildMigrationCtrl.MigratePlanetariumPledgeToGuild(world, target)); - } - - [Fact] - public void MigratePlanetariumPledgeToGuild_When_WithPledgeContract() - { - var guildMasterAddress = GuildConfig.PlanetariumGuildOwner; - var guildAddress = AddressUtil.CreateGuildAddress(); - var target = AddressUtil.CreateAgentAddress(); - var pledgeAddress = target.GetPledgeAddress(); - var world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMasterAddress) - .SetLegacyState(pledgeAddress, new List( - MeadConfig.PatronAddress.Serialize(), - true.Serialize(), - RequestPledge.DefaultRefillMead.Serialize())); - - Assert.Null(world.GetJoinedGuild(target)); - world = GuildMigrationCtrl.MigratePlanetariumPledgeToGuild(world, target); - - var joinedGuildAddress = Assert.IsType(world.GetJoinedGuild(target)); - Assert.True(world.TryGetGuild(joinedGuildAddress, out var guild)); - Assert.Equal(GuildConfig.PlanetariumGuildOwner, guild.GuildMasterAddress); - } - - [Fact] - public void MigratePlanetariumPledgeToGuild_When_WithoutPledgeContract() - { - var guildMasterAddress = GuildConfig.PlanetariumGuildOwner; - var guildAddress = AddressUtil.CreateGuildAddress(); - var target = AddressUtil.CreateAgentAddress(); - var world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMasterAddress); - - Assert.Null(world.GetJoinedGuild(target)); - Assert.Throws(() => - GuildMigrationCtrl.MigratePlanetariumPledgeToGuild(world, target)); - } - - [Fact] - public void MigratePlanetariumPledgeToGuild_When_WithoutGuildYet() - { - var target = AddressUtil.CreateAgentAddress(); - var pledgeAddress = target.GetPledgeAddress(); - var world = new World(MockUtil.MockModernWorldState) - .SetLegacyState(pledgeAddress, new List( - MeadConfig.PatronAddress.Serialize(), - true.Serialize(), - RequestPledge.DefaultRefillMead.Serialize())); - - Assert.Null(world.GetJoinedGuild(target)); - Assert.Throws(() => - GuildMigrationCtrl.MigratePlanetariumPledgeToGuild(world, target)); - } - } -} diff --git a/.Lib9c.Tests/Action/Guild/Migration/MigrateDelegationTest.cs b/.Lib9c.Tests/Action/Guild/Migration/MigrateDelegationTest.cs new file mode 100644 index 0000000000..aa42c37100 --- /dev/null +++ b/.Lib9c.Tests/Action/Guild/Migration/MigrateDelegationTest.cs @@ -0,0 +1,122 @@ +namespace Lib9c.Tests.Action.Guild.Migration +{ + using System; + using System.Linq; + using System.Numerics; + using Bencodex.Types; + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Action.Guild; + using Nekoyume.Action.Guild.Migration; + using Nekoyume.Action.Guild.Migration.LegacyModels; + using Nekoyume.Extensions; + using Nekoyume.Model.Guild; + using Nekoyume.TypedAddress; + using Xunit; + + // TODO: Remove this test class after the migration is completed. + public class MigrateDelegationTest : GuildTestBase + { + [Fact] + public void Execute() + { + var guildAddress = AddressUtil.CreateGuildAddress(); + var world = EnsureLegacyPlanetariumGuild(World, guildAddress); + var guildMemberCount = 10; + var migratedGuildMemberCount = 5; + var guildMemberAddresses = Enumerable.Range(0, guildMemberCount).Select( + _ => AddressUtil.CreateAgentAddress()).ToList(); + for (var i = 0; i < guildMemberCount; i++) + { + world = EnsureJoinLegacyPlanetariumGuild(world, guildMemberAddresses[i]); + } + + world = EnsureMigratedPlanetariumGuild(world); + + for (var i = 0; i < migratedGuildMemberCount; i++) + { + var action = new MigrateDelegation(guildMemberAddresses[i]); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = new PrivateKey().Address, + }; + world = action.Execute(actionContext); + } + + var repo = new GuildRepository(world, new ActionContext()); + var guild = repo.GetGuild(guildAddress); + + for (var i = 0; i < migratedGuildMemberCount; i++) + { + repo.GetGuildParticipant(guildMemberAddresses[i]); + } + + for (var i = migratedGuildMemberCount; i < guildMemberCount; i++) + { + Assert.Throws(() => repo.GetGuildParticipant(guildMemberAddresses[i])); + } + } + + private static IWorld EnsureLegacyPlanetariumGuild(IWorld world, GuildAddress guildAddress) + { + var legacyPlanetariumGuild = new LegacyGuild(GuildConfig.PlanetariumGuildOwner); + var legacyPlanetariumGuildParticipant = new LegacyGuildParticipant(guildAddress); + + return world + .MutateAccount( + Addresses.Guild, + account => account.SetState(guildAddress, legacyPlanetariumGuild.Bencoded)) + .MutateAccount( + Addresses.GuildParticipant, + account => account.SetState(GuildConfig.PlanetariumGuildOwner, legacyPlanetariumGuildParticipant.Bencoded)) + .MutateAccount( + Addresses.GuildMemberCounter, + account => + { + BigInteger count = account.GetState(guildAddress) switch + { + Integer i => i.Value, + null => 0, + _ => throw new InvalidCastException(), + }; + + return account.SetState(guildAddress, (Integer)(count + 1)); + }); + } + + private static IWorld EnsureJoinLegacyPlanetariumGuild(IWorld world, AgentAddress guildParticipantAddress) + { + var planetariumGuildAddress + = new LegacyGuildParticipant( + world.GetAccount(Addresses.GuildParticipant).GetState(GuildConfig.PlanetariumGuildOwner) as List).GuildAddress; + var legacyParticipant = new LegacyGuildParticipant(planetariumGuildAddress); + + return world + .MutateAccount(Addresses.GuildParticipant, account => account.SetState(guildParticipantAddress, legacyParticipant.Bencoded)) + .MutateAccount( + Addresses.GuildMemberCounter, + account => + { + BigInteger count = account.GetState(planetariumGuildAddress) switch + { + Integer i => i.Value, + null => 0, + _ => throw new InvalidCastException(), + }; + + return account.SetState(planetariumGuildAddress, (Integer)(count + 1)); + }); + } + + private static IWorld EnsureMigratedPlanetariumGuild(IWorld world) + => new MigratePlanetariumGuild().Execute(new ActionContext + { + PreviousState = world, + Signer = new PrivateKey().Address, + }); + } +} diff --git a/.Lib9c.Tests/Action/Guild/Migration/MigratePlanetariumGuildTest.cs b/.Lib9c.Tests/Action/Guild/Migration/MigratePlanetariumGuildTest.cs new file mode 100644 index 0000000000..6f5686bd34 --- /dev/null +++ b/.Lib9c.Tests/Action/Guild/Migration/MigratePlanetariumGuildTest.cs @@ -0,0 +1,65 @@ +namespace Lib9c.Tests.Action.Guild.Migration +{ + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Action.Guild; + using Nekoyume.Action.Guild.Migration; + using Nekoyume.Action.Guild.Migration.LegacyModels; + using Nekoyume.Extensions; + using Nekoyume.Model.Guild; + using Nekoyume.TypedAddress; + using Xunit; + + // TODO: Remove this test class after the migration is completed. + public class MigratePlanetariumGuildTest : GuildTestBase + { + [Fact] + public void Execute() + { + var guildAddress = AddressUtil.CreateGuildAddress(); + var world = EnsureLegacyPlanetariumGuild(World, guildAddress); + var repo = new GuildRepository(world, new ActionContext()); + + Assert.Throws(() => repo.GetGuild(guildAddress)); + Assert.Throws(() => repo.GetGuildParticipant(GuildConfig.PlanetariumGuildOwner)); + + var action = new MigratePlanetariumGuild(); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = new PrivateKey().Address, + }; + world = action.Execute(actionContext); + + repo.UpdateWorld(world); + + var guild = repo.GetGuild(guildAddress); + var guildOwner = repo.GetGuildParticipant(GuildConfig.PlanetariumGuildOwner); + } + + private static IWorld EnsureLegacyPlanetariumGuild(IWorld world, GuildAddress guildAddress) + { + var legacyPlanetariumGuild = new LegacyGuild(GuildConfig.PlanetariumGuildOwner); + var legacyPlanetariumGuildParticipant = new LegacyGuildParticipant(guildAddress); + var guildAccount = world.GetAccount(Addresses.Guild); + var guildParticipantAccount = world.GetAccount(Addresses.GuildParticipant); + return world + .MutateAccount( + Addresses.Guild, + account => account.SetState(guildAddress, legacyPlanetariumGuild.Bencoded)) + .MutateAccount( + Addresses.GuildParticipant, + account => account.SetState(GuildConfig.PlanetariumGuildOwner, legacyPlanetariumGuildParticipant.Bencoded)); + } + + private static IWorld EnsureMigratedPlanetariumGuild(IWorld world) + => new MigratePlanetariumGuild().Execute(new ActionContext + { + PreviousState = world, + Signer = new PrivateKey().Address, + }); + } +} diff --git a/.Lib9c.Tests/Action/Guild/Migration/MigratePledgeToGuildTest.cs b/.Lib9c.Tests/Action/Guild/Migration/MigratePledgeToGuildTest.cs deleted file mode 100644 index 70c8850a2d..0000000000 --- a/.Lib9c.Tests/Action/Guild/Migration/MigratePledgeToGuildTest.cs +++ /dev/null @@ -1,172 +0,0 @@ -namespace Lib9c.Tests.Action.Guild.Migration -{ - using System; - using Bencodex.Types; - using Lib9c.Tests.Action; - using Lib9c.Tests.Util; - using Libplanet.Action.State; - using Libplanet.Mocks; - using Nekoyume; - using Nekoyume.Action; - using Nekoyume.Action.Guild; - using Nekoyume.Action.Guild.Migration; - using Nekoyume.Action.Loader; - using Nekoyume.Extensions; - using Nekoyume.Model.State; - using Nekoyume.Module; - using Nekoyume.Module.Guild; - using Nekoyume.TypedAddress; - using Xunit; - - public class MigratePledgeToGuildTest - { - [Fact] - public void Serialization() - { - var agentAddress = AddressUtil.CreateAgentAddress(); - var action = new MigratePledgeToGuild(agentAddress); - var plainValue = action.PlainValue; - - var actionLoader = new NCActionLoader(); - var deserialized = - Assert.IsType(actionLoader.LoadAction(0, plainValue)); - - Assert.Equal(agentAddress, deserialized.Target); - } - - [Fact] - public void Execute_When_WithPledgeContract() - { - var guildMasterAddress = GuildConfig.PlanetariumGuildOwner; - var guildAddress = AddressUtil.CreateGuildAddress(); - var target = AddressUtil.CreateAgentAddress(); - var caller = AddressUtil.CreateAgentAddress(); - var pledgeAddress = target.GetPledgeAddress(); - var world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMasterAddress) - .SetLegacyState(pledgeAddress, new List( - MeadConfig.PatronAddress.Serialize(), - true.Serialize(), - RequestPledge.DefaultRefillMead.Serialize())); - - Assert.Null(world.GetJoinedGuild(target)); - var action = new MigratePledgeToGuild(target); - - // Migrate by other. - IWorld newWorld = action.Execute(new ActionContext - { - PreviousState = world, - Signer = caller, - }); - - var joinedGuildAddress = Assert.IsType(newWorld.GetJoinedGuild(target)); - Assert.True(newWorld.TryGetGuild(joinedGuildAddress, out var guild)); - Assert.Equal(GuildConfig.PlanetariumGuildOwner, guild.GuildMasterAddress); - - // Migrate by itself. - newWorld = action.Execute(new ActionContext - { - PreviousState = world, - Signer = target, - }); - - joinedGuildAddress = Assert.IsType(newWorld.GetJoinedGuild(target)); - Assert.True(newWorld.TryGetGuild(joinedGuildAddress, out guild)); - Assert.Equal(GuildConfig.PlanetariumGuildOwner, guild.GuildMasterAddress); - } - - [Fact] - public void Execute_When_WithUnapprovedPledgeContract() - { - var guildMasterAddress = GuildConfig.PlanetariumGuildOwner; - var guildAddress = AddressUtil.CreateGuildAddress(); - var target = AddressUtil.CreateAgentAddress(); - var pledgeAddress = target.GetPledgeAddress(); - var caller = AddressUtil.CreateAgentAddress(); - var world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMasterAddress) - .SetLegacyState(pledgeAddress, new List( - MeadConfig.PatronAddress.Serialize(), - false.Serialize(), - RequestPledge.DefaultRefillMead.Serialize())); - - Assert.Null(world.GetJoinedGuild(target)); - var action = new MigratePledgeToGuild(target); - - // Migrate by other. - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = world, - Signer = caller, - })); - - // Migrate by itself. - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = world, - Signer = target, - })); - } - - [Fact] - public void Execute_When_WithoutPledgeContract() - { - var guildMasterAddress = GuildConfig.PlanetariumGuildOwner; - var guildAddress = AddressUtil.CreateGuildAddress(); - var target = AddressUtil.CreateAgentAddress(); - var caller = AddressUtil.CreateAgentAddress(); - var world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMasterAddress); - - Assert.Null(world.GetJoinedGuild(target)); - var action = new MigratePledgeToGuild(target); - - // Migrate by other. - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = world, - Signer = caller, - })); - - // Migrate by itself. - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = world, - Signer = target, - })); - } - - [Fact] - public void Execute_When_WithoutGuildYet() - { - var target = AddressUtil.CreateAgentAddress(); - var caller = AddressUtil.CreateAgentAddress(); - var pledgeAddress = target.GetPledgeAddress(); - var world = new World(MockUtil.MockModernWorldState) - .SetLegacyState(pledgeAddress, new List( - MeadConfig.PatronAddress.Serialize(), - true.Serialize(), - RequestPledge.DefaultRefillMead.Serialize())); - - Assert.Null(world.GetJoinedGuild(target)); - var action = new MigratePledgeToGuild(target); - - // Migrate by other. - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = world, - Signer = caller, - })); - - // Migrate by itself. - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = world, - Signer = target, - })); - } - } -} diff --git a/.Lib9c.Tests/Action/Guild/MoveGuildTest.cs b/.Lib9c.Tests/Action/Guild/MoveGuildTest.cs new file mode 100644 index 0000000000..d4e299d7f5 --- /dev/null +++ b/.Lib9c.Tests/Action/Guild/MoveGuildTest.cs @@ -0,0 +1,65 @@ +namespace Lib9c.Tests.Action.Guild +{ + using System; + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Mocks; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action.Guild; + using Nekoyume.Model.Guild; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.Module.Guild; + using Xunit; + + public class MoveGuildTest : GuildTestBase + { + [Fact] + public void Serialization() + { + var guildAddress = AddressUtil.CreateGuildAddress(); + var action = new MoveGuild(guildAddress); + var plainValue = action.PlainValue; + + var deserialized = new MoveGuild(); + deserialized.LoadPlainValue(plainValue); + Assert.Equal(guildAddress, deserialized.GuildAddress); + } + + [Fact] + public void Execute() + { + var validatorKey1 = new PrivateKey(); + var validatorKey2 = new PrivateKey(); + var agentAddress = AddressUtil.CreateAgentAddress(); + var guildMasterAddress1 = AddressUtil.CreateAgentAddress(); + var guildMasterAddress2 = AddressUtil.CreateAgentAddress(); + var guildAddress1 = AddressUtil.CreateGuildAddress(); + var guildAddress2 = AddressUtil.CreateGuildAddress(); + + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey1.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey1.PublicKey); + world = EnsureToMintAsset(world, validatorKey2.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey2.PublicKey); + world = EnsureToMakeGuild(world, guildAddress1, guildMasterAddress1, validatorKey1.Address); + world = EnsureToMakeGuild(world, guildAddress2, guildMasterAddress2, validatorKey2.Address); + world = EnsureToJoinGuild(world, guildAddress1, agentAddress, 1L); + + var moveGuild = new MoveGuild(guildAddress2); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = agentAddress, + }; + world = moveGuild.Execute(actionContext); + + var repository = new GuildRepository(world, actionContext); + var guildParticipant = repository.GetGuildParticipant(agentAddress); + + Assert.Equal(guildAddress2, guildParticipant.GuildAddress); + } + } +} diff --git a/.Lib9c.Tests/Action/Guild/QuitGuildTest.cs b/.Lib9c.Tests/Action/Guild/QuitGuildTest.cs index 4de4975872..51d10b2896 100644 --- a/.Lib9c.Tests/Action/Guild/QuitGuildTest.cs +++ b/.Lib9c.Tests/Action/Guild/QuitGuildTest.cs @@ -3,12 +3,19 @@ namespace Lib9c.Tests.Action.Guild using System; using Lib9c.Tests.Util; using Libplanet.Action.State; + using Libplanet.Crypto; using Libplanet.Mocks; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; using Nekoyume.Action.Guild; + using Nekoyume.Model.Guild; + using Nekoyume.Model.State; + using Nekoyume.Module; using Nekoyume.Module.Guild; using Xunit; - public class QuitGuildTest + public class QuitGuildTest : GuildTestBase { [Fact] public void Serialization() @@ -23,40 +30,28 @@ public void Serialization() [Fact] public void Execute() { + var validatorKey = new PrivateKey(); var agentAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var action = new QuitGuild(); - IWorld world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress); - - // This case should fail because guild master cannot quit the guild. - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = world, - Signer = guildMasterAddress, - })); - - // This case should fail because the agent is not a member of the guild. - Assert.Throws(() => action.Execute(new ActionContext - { - PreviousState = world, - Signer = agentAddress, - })); - - // Join the guild. - world = world.JoinGuild(guildAddress, agentAddress); - Assert.NotNull(world.GetJoinedGuild(agentAddress)); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, agentAddress, 1L); - // This case should fail because the agent is not a member of the guild. - world = action.Execute(new ActionContext + var quitGuild = new QuitGuild(); + var actionContext = new ActionContext { PreviousState = world, Signer = agentAddress, - }); + }; + world = quitGuild.Execute(actionContext); - Assert.Null(world.GetJoinedGuild(agentAddress)); + var repository = new GuildRepository(world, actionContext); + Assert.Throws( + () => repository.GetGuildParticipant(agentAddress)); } } } diff --git a/.Lib9c.Tests/Action/Guild/RejectGuildApplicationTest.cs b/.Lib9c.Tests/Action/Guild/RejectGuildApplicationTest.cs deleted file mode 100644 index 217096d9ed..0000000000 --- a/.Lib9c.Tests/Action/Guild/RejectGuildApplicationTest.cs +++ /dev/null @@ -1,83 +0,0 @@ -namespace Lib9c.Tests.Action.Guild -{ - using System; - using Lib9c.Tests.Util; - using Libplanet.Action.State; - using Libplanet.Mocks; - using Nekoyume.Action.Guild; - using Nekoyume.Action.Loader; - using Nekoyume.Module.Guild; - using Xunit; - - public class RejectGuildApplicationTest - { - [Fact] - public void Serialization() - { - var agentAddress = AddressUtil.CreateAgentAddress(); - var action = new RejectGuildApplication(agentAddress); - var plainValue = action.PlainValue; - - var deserialized = new RejectGuildApplication(); - deserialized.LoadPlainValue(plainValue); - Assert.Equal(agentAddress, deserialized.Target); - } - - [Fact] - public void Execute() - { - var appliedMemberAddress = AddressUtil.CreateAgentAddress(); - var nonAppliedMemberAddress = AddressUtil.CreateAgentAddress(); - var guildMasterAddress = AddressUtil.CreateAgentAddress(); - var guildAddress = AddressUtil.CreateGuildAddress(); - - IWorld world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .ApplyGuild(appliedMemberAddress, guildAddress); - - // These cases should fail because the member didn't apply the guild and - // non-guild-master-addresses cannot reject the guild application. - Assert.Throws( - () => new RejectGuildApplication(nonAppliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = guildMasterAddress, - })); - Assert.Throws( - () => new RejectGuildApplication(nonAppliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = appliedMemberAddress, - })); - Assert.Throws( - () => new RejectGuildApplication(nonAppliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = nonAppliedMemberAddress, - })); - - // These cases should fail because non-guild-master-addresses cannot reject the guild application. - Assert.Throws( - () => new RejectGuildApplication(appliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = appliedMemberAddress, - })); - Assert.Throws( - () => new RejectGuildApplication(appliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = nonAppliedMemberAddress, - })); - - world = new RejectGuildApplication(appliedMemberAddress).Execute(new ActionContext - { - PreviousState = world, - Signer = guildMasterAddress, - }); - - Assert.False(world.TryGetGuildApplication(appliedMemberAddress, out _)); - Assert.Null(world.GetJoinedGuild(appliedMemberAddress)); - } - } -} diff --git a/.Lib9c.Tests/Action/Guild/RemoveGuildTest.cs b/.Lib9c.Tests/Action/Guild/RemoveGuildTest.cs index 766094d4c3..7eebe282e0 100644 --- a/.Lib9c.Tests/Action/Guild/RemoveGuildTest.cs +++ b/.Lib9c.Tests/Action/Guild/RemoveGuildTest.cs @@ -3,12 +3,20 @@ namespace Lib9c.Tests.Action.Guild using System; using Lib9c.Tests.Util; using Libplanet.Action.State; + using Libplanet.Crypto; using Libplanet.Mocks; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; using Nekoyume.Action.Guild; + using Nekoyume.Model.Guild; + using Nekoyume.Model.Stake; + using Nekoyume.Model.State; + using Nekoyume.Module; using Nekoyume.Module.Guild; using Xunit; - public class RemoveGuildTest + public class RemoveGuildTest : GuildTestBase { [Fact] public void Serialization() @@ -21,86 +29,127 @@ public void Serialization() } [Fact] - public void Execute_By_GuildMember() + public void Execute() { - var action = new RemoveGuild(); + var validatorKey = new PrivateKey(); + var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); + var guildAddress = AddressUtil.CreateGuildAddress(); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + + var removeGuild = new RemoveGuild(); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = guildMasterAddress, + }; + world = removeGuild.Execute(actionContext); + + var repository = new GuildRepository(world, actionContext); + Assert.Throws(() => repository.GetGuild(guildAddress)); + } + + [Fact] + public void Execute_ByGuildMember_Throw() + { + var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); var guildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - IWorld world = new World(MockWorldState.CreateModern()); - world = world.MakeGuild(guildAddress, guildMasterAddress); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, 1L); - Assert.Throws(() => action.Execute(new ActionContext + var actionContext = new ActionContext { PreviousState = world, Signer = guildMemberAddress, - })); + }; + var removeGuild = new RemoveGuild(); + + Assert.Throws(() => removeGuild.Execute(actionContext)); } [Fact] - public void Execute_By_GuildMaster() + public void Execute_WhenDelegationExists_Throw() { - var action = new RemoveGuild(); - + var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var guildParticipantAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - IWorld world = new World(MockWorldState.CreateModern()); - world = world.MakeGuild(guildAddress, guildMasterAddress); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMintAsset(world, StakeState.DeriveAddress(guildMasterAddress), GG * 100); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, guildParticipantAddress, 1L); - var changedWorld = action.Execute(new ActionContext + var actionContext = new ActionContext { PreviousState = world, Signer = guildMasterAddress, - }); + }; + var removeGuild = new RemoveGuild(); - Assert.False(changedWorld.TryGetGuild(guildAddress, out _)); - Assert.Null(changedWorld.GetJoinedGuild(guildMasterAddress)); + Assert.Throws(() => removeGuild.Execute(actionContext)); } [Fact] - public void Execute_By_Other() + public void Execute_ByNonGuildMember_Throw() { - var action = new RemoveGuild(); - + var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); var otherAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); - IWorld world = new World(MockWorldState.CreateModern()); - world = world.MakeGuild(guildAddress, guildMasterAddress); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); - Assert.Throws(() => action.Execute(new ActionContext + var actionContext = new ActionContext { PreviousState = world, Signer = otherAddress, - })); + }; + var removeGuild = new RemoveGuild(); + + Assert.Throws(() => removeGuild.Execute(actionContext)); } [Fact] - public void ResetBannedAddresses() + public void Execute_ResetBannedAddresses() { - var action = new RemoveGuild(); - + var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); var bannedAddress = AddressUtil.CreateAgentAddress(); - IWorld world = new World(MockWorldState.CreateModern()); - world = world.MakeGuild(guildAddress, guildMasterAddress) - .Ban(guildAddress, guildMasterAddress, bannedAddress); - - Assert.True(world.IsBanned(guildAddress, bannedAddress)); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, bannedAddress, 1L); + world = EnsureToBanGuildMember(world, guildAddress, guildMasterAddress, bannedAddress); - world = action.Execute(new ActionContext + var actionContext = new ActionContext { PreviousState = world, Signer = guildMasterAddress, - }); + }; + var removeGuild = new RemoveGuild(); + world = removeGuild.Execute(actionContext); - Assert.False(world.IsBanned(guildAddress, bannedAddress)); + var repository = new GuildRepository(world, actionContext); + Assert.False(repository.IsBanned(guildAddress, bannedAddress)); } } } diff --git a/.Lib9c.Tests/Action/Guild/UnbanGuildMemberTest.cs b/.Lib9c.Tests/Action/Guild/UnbanGuildMemberTest.cs index 9aedb745e7..73f918653c 100644 --- a/.Lib9c.Tests/Action/Guild/UnbanGuildMemberTest.cs +++ b/.Lib9c.Tests/Action/Guild/UnbanGuildMemberTest.cs @@ -4,12 +4,12 @@ namespace Lib9c.Tests.Action.Guild using Lib9c.Tests.Util; using Libplanet.Action.State; using Libplanet.Crypto; - using Libplanet.Mocks; using Nekoyume.Action.Guild; + using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; using Xunit; - public class UnbanGuildMemberTest + public class UnbanGuildMemberTest : GuildTestBase { [Fact] public void Serialization() @@ -23,9 +23,38 @@ public void Serialization() Assert.Equal(guildMemberAddress, deserialized.Target); } + [Fact] + public void Execute() + { + var validatorKey = new PrivateKey(); + var guildMasterAddress = AddressUtil.CreateAgentAddress(); + var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); + var guildAddress = AddressUtil.CreateGuildAddress(); + + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, 1L); + world = EnsureToBanGuildMember(world, guildAddress, guildMasterAddress, targetGuildMemberAddress); + + var unbanGuildMember = new UnbanGuildMember(targetGuildMemberAddress); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = guildMasterAddress, + }; + world = unbanGuildMember.Execute(actionContext); + + var repository = new GuildRepository(world, actionContext); + Assert.False(repository.IsBanned(guildAddress, targetGuildMemberAddress)); + Assert.Null(repository.GetJoinedGuild(targetGuildMemberAddress)); + } + [Fact] public void Unban_By_GuildMember() { + var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); var guildMemberAddress = AddressUtil.CreateAgentAddress(); var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); @@ -33,22 +62,26 @@ public void Unban_By_GuildMember() var action = new UnbanGuildMember(targetGuildMemberAddress); - IWorld world = new World(MockWorldState.CreateModern()); - world = world.MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMemberAddress) - .JoinGuild(guildAddress, targetGuildMemberAddress); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, guildMemberAddress, 1L); + world = EnsureToBanGuildMember(world, guildAddress, guildMasterAddress, targetGuildMemberAddress); + + var repository = new GuildRepository(world, new ActionContext()); // GuildMember tries to ban other guild member. Assert.Throws(() => action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = guildMemberAddress, })); // GuildMember tries to ban itself. Assert.Throws(() => action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = targetGuildMemberAddress, })); } @@ -56,27 +89,34 @@ public void Unban_By_GuildMember() [Fact] public void Unban_By_GuildMaster() { + var validatorKey = new PrivateKey(); var guildMasterAddress = AddressUtil.CreateAgentAddress(); var targetGuildMemberAddress = AddressUtil.CreateAgentAddress(); var guildAddress = AddressUtil.CreateGuildAddress(); var action = new UnbanGuildMember(targetGuildMemberAddress); - IWorld world = new World(MockWorldState.CreateModern()); - world = world.MakeGuild(guildAddress, guildMasterAddress) - .Ban(guildAddress, guildMasterAddress, targetGuildMemberAddress); + IWorld world = World; + world = EnsureToMintAsset(world, validatorKey.Address, GG * 100); + world = EnsureToCreateValidator(world, validatorKey.PublicKey); + world = EnsureToMakeGuild(world, guildAddress, guildMasterAddress, validatorKey.Address); + world = EnsureToJoinGuild(world, guildAddress, targetGuildMemberAddress, 1L); + world = EnsureToBanGuildMember(world, guildAddress, guildMasterAddress, targetGuildMemberAddress); - Assert.True(world.IsBanned(guildAddress, targetGuildMemberAddress)); - Assert.Null(world.GetJoinedGuild(targetGuildMemberAddress)); + var repository = new GuildRepository(world, new ActionContext()); + + Assert.True(repository.IsBanned(guildAddress, targetGuildMemberAddress)); + Assert.Null(repository.GetJoinedGuild(targetGuildMemberAddress)); world = action.Execute(new ActionContext { - PreviousState = world, + PreviousState = repository.World, Signer = guildMasterAddress, }); - Assert.False(world.IsBanned(guildAddress, targetGuildMemberAddress)); - Assert.Null(world.GetJoinedGuild(targetGuildMemberAddress)); + repository.UpdateWorld(world); + Assert.False(repository.IsBanned(guildAddress, targetGuildMemberAddress)); + Assert.Null(repository.GetJoinedGuild(targetGuildMemberAddress)); } } } diff --git a/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs b/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs index d3d4a9d957..55ef05c0d1 100644 --- a/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs +++ b/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs @@ -545,8 +545,8 @@ public void ExecuteWithStake(int stakingLevel) var apStone = ItemFactory.CreateTradableMaterial(itemRow); avatarState.inventory.AddItem(apStone); - var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); - var stakeState = new StakeState(stakeStateAddress, 1); + var stakeStateAddress = LegacyStakeState.DeriveAddress(_agentAddress); + var stakeState = new LegacyStakeState(stakeStateAddress, 1); var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows .FirstOrDefault(r => r.Level == stakingLevel)?.RequiredGold ?? 0; var context = new ActionContext(); @@ -695,8 +695,8 @@ public void ExecuteDuplicatedException(int slotIndex, int runeId, int slotIndex2 var apStone = ItemFactory.CreateTradableMaterial(itemRow); avatarState.inventory.AddItem(apStone); - var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); - var stakeState = new StakeState(stakeStateAddress, 1); + var stakeStateAddress = LegacyStakeState.DeriveAddress(_agentAddress); + var stakeState = new LegacyStakeState(stakeStateAddress, 1); var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows .FirstOrDefault(r => r.Level == stakingLevel)?.RequiredGold ?? 0; var context = new ActionContext(); diff --git a/.Lib9c.Tests/Action/HackAndSlashTest.cs b/.Lib9c.Tests/Action/HackAndSlashTest.cs index 9d61aab639..c2d404e189 100644 --- a/.Lib9c.Tests/Action/HackAndSlashTest.cs +++ b/.Lib9c.Tests/Action/HackAndSlashTest.cs @@ -1304,8 +1304,8 @@ public void CheckUsedApByStaking(int level, int playCount) _tableSheets.WorldSheet, clearedStageId); - var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); - var stakeState = new StakeState(stakeStateAddress, 1); + var stakeStateAddress = LegacyStakeState.DeriveAddress(_agentAddress); + var stakeState = new LegacyStakeState(stakeStateAddress, 1); var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows .FirstOrDefault(r => r.Level == level)?.RequiredGold ?? 0; var context = new ActionContext(); @@ -1372,8 +1372,8 @@ public void CheckUsingApStoneWithStaking(int level, int apStoneCount, int totalR r.ItemSubType == ItemSubType.ApStone); var apStone = ItemFactory.CreateTradableMaterial(apStoneRow); previousAvatarState.inventory.AddItem(apStone, apStoneCount); - var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); - var stakeState = new StakeState(stakeStateAddress, 1); + var stakeStateAddress = LegacyStakeState.DeriveAddress(_agentAddress); + var stakeState = new LegacyStakeState(stakeStateAddress, 1); var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows .FirstOrDefault(r => r.Level == level)?.RequiredGold ?? 0; var context = new ActionContext(); diff --git a/.Lib9c.Tests/Action/InitializeStatesTest.cs b/.Lib9c.Tests/Action/InitializeStatesTest.cs index 7af13e7d5f..8f9898024e 100644 --- a/.Lib9c.Tests/Action/InitializeStatesTest.cs +++ b/.Lib9c.Tests/Action/InitializeStatesTest.cs @@ -9,6 +9,7 @@ namespace Lib9c.Tests.Action using Libplanet.Crypto; using Libplanet.Mocks; using Libplanet.Types.Assets; + using Libplanet.Types.Consensus; using Nekoyume; using Nekoyume.Action; using Nekoyume.Model; @@ -43,8 +44,11 @@ public void Execute() var privateKey = new PrivateKey(); (ActivationKey activationKey, PendingActivationState pendingActivation) = ActivationKey.Create(privateKey, nonce); + var validatorSet = new ValidatorSet( + new List { new (privateKey.PublicKey, 1_000_000_000_000_000_000) }); var action = new InitializeStates( + validatorSet: validatorSet, rankingState: new RankingState0(), shopState: new ShopState(), tableSheets: _sheets, @@ -105,8 +109,11 @@ public void ExecuteWithAuthorizedMinersState() var privateKey = new PrivateKey(); (ActivationKey activationKey, PendingActivationState pendingActivation) = ActivationKey.Create(privateKey, nonce); + var validatorSet = new ValidatorSet( + new List { new (privateKey.PublicKey, 1_000_000_000_000_000_000) }); var action = new InitializeStates( + validatorSet: validatorSet, rankingState: new RankingState0(), shopState: new ShopState(), tableSheets: _sheets, @@ -162,8 +169,11 @@ public void ExecuteWithActivateAdminKey() (ActivationKey activationKey, PendingActivationState pendingActivation) = ActivationKey.Create(privateKey, nonce); var adminAddress = new Address("F9A15F870701268Bd7bBeA6502eB15F4997f32f9"); + var validatorSet = new ValidatorSet( + new List { new (privateKey.PublicKey, 1_000_000_000_000_000_000) }); var action = new InitializeStates( + validatorSet: validatorSet, rankingState: new RankingState0(), shopState: new ShopState(), tableSheets: _sheets, @@ -213,8 +223,11 @@ public void ExecuteWithCredits() "山田太郎", } ); + var validatorSet = new ValidatorSet( + new List { new (minterKey.PublicKey, 1_000_000_000_000_000_000) }); var action = new InitializeStates( + validatorSet: validatorSet, rankingState: new RankingState0(), shopState: new ShopState(), tableSheets: _sheets, @@ -260,8 +273,11 @@ public void ExecuteWithoutAdminState() var privateKey = new PrivateKey(); (ActivationKey activationKey, PendingActivationState pendingActivation) = ActivationKey.Create(privateKey, nonce); + var validatorSet = new ValidatorSet( + new List { new (privateKey.PublicKey, 1_000_000_000_000_000_000) }); var action = new InitializeStates( + validatorSet: validatorSet, rankingState: new RankingState0(), shopState: new ShopState(), tableSheets: _sheets, @@ -303,8 +319,10 @@ public void ExecuteWithoutInitialSupply() #pragma warning restore CS0618 var nonce = new byte[] { 0x00, 0x01, 0x02, 0x03 }; var privateKey = new PrivateKey(); + var validatorSet = new ValidatorSet(new List { new (privateKey.PublicKey, 10) }); var action = new InitializeStates( + validatorSet: validatorSet, rankingState: new RankingState0(), shopState: new ShopState(), tableSheets: _sheets, @@ -343,8 +361,11 @@ public void ExecuteWithAssetMinters() #pragma warning restore CS0618 var nonce = new byte[] { 0x00, 0x01, 0x02, 0x03 }; var adminAddress = new Address("F9A15F870701268Bd7bBeA6502eB15F4997f32f9"); + var validatorSet = new ValidatorSet( + new List { new (minterKey.PublicKey, 1_000_000_000_000_000_000) }); var action = new InitializeStates( + validatorSet: validatorSet, rankingState: new RankingState0(), shopState: new ShopState(), tableSheets: _sheets, diff --git a/.Lib9c.Tests/Action/ItemEnhancementTest.cs b/.Lib9c.Tests/Action/ItemEnhancementTest.cs index b5fa83694e..6fd01c1d9f 100644 --- a/.Lib9c.Tests/Action/ItemEnhancementTest.cs +++ b/.Lib9c.Tests/Action/ItemEnhancementTest.cs @@ -311,13 +311,9 @@ public void Execute( nextState.GetBalance(_agentAddress, _currency) ); - var arenaSheet = _tableSheets.ArenaSheet; - var arenaData = arenaSheet.GetRoundByBlockIndex(1); - var feeStoreAddress = - ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); Assert.Equal( expectedCost * _currency, - nextState.GetBalance(feeStoreAddress, _currency) + nextState.GetBalance(Addresses.RewardPool, _currency) ); Assert.Equal(30, nextAvatarState.mailBox.Count); diff --git a/.Lib9c.Tests/Action/MigrateMonsterCollectionTest.cs b/.Lib9c.Tests/Action/MigrateMonsterCollectionTest.cs index 838f889080..6c8b8d7788 100644 --- a/.Lib9c.Tests/Action/MigrateMonsterCollectionTest.cs +++ b/.Lib9c.Tests/Action/MigrateMonsterCollectionTest.cs @@ -69,9 +69,9 @@ public void Execute_ThrowsIfAlreadyStakeStateExists() Address monsterCollectionAddress = MonsterCollectionState.DeriveAddress(_signer, 0); var monsterCollectionState = new MonsterCollectionState( monsterCollectionAddress, 1, 0); - Address stakeStateAddress = StakeState.DeriveAddress(_signer); + Address stakeStateAddress = LegacyStakeState.DeriveAddress(_signer); var states = _state.SetLegacyState( - stakeStateAddress, new StakeState(stakeStateAddress, 0).SerializeV2()) + stakeStateAddress, new LegacyStakeState(stakeStateAddress, 0).SerializeV2()) .SetLegacyState(monsterCollectionAddress, monsterCollectionState.Serialize()); MigrateMonsterCollection action = new MigrateMonsterCollection(_avatarAddress); Assert.Throws(() => action.Execute(new ActionContext @@ -138,7 +138,7 @@ public void Execute(int collectionLevel, long claimBlockIndex, long receivedBloc RandomSeed = 0, }); - Assert.True(states.TryGetStakeState(_signer, out StakeState stakeState)); + Assert.True(states.TryGetLegacyStakeState(_signer, out LegacyStakeState stakeState)); Assert.Equal( 0 * currency, states.GetBalance(monsterCollectionState.address, currency)); diff --git a/.Lib9c.Tests/Action/PetEnhancement0Test.cs b/.Lib9c.Tests/Action/PetEnhancement0Test.cs index f909ef1479..25584ec6c5 100644 --- a/.Lib9c.Tests/Action/PetEnhancement0Test.cs +++ b/.Lib9c.Tests/Action/PetEnhancement0Test.cs @@ -154,20 +154,6 @@ public void Execute_Throw_PetCostNotFoundException() removePetCostRowWithTargetPetLevel: true)); } - [Fact] - public void Execute_Throw_RoundNotFoundException() - { - Assert.Throws(() => - Execute( - _initialStatesWithAvatarStateV2, - _firstRoundStartBlockIndex - 1, - _agentAddr, - _avatarAddr, - _targetPetId, - 0, - 1)); - } - [Theory] [InlineData(0, 1)] public void Execute_Throw_NotEnoughFungibleAssetValueException( diff --git a/.Lib9c.Tests/Action/RaidTest.cs b/.Lib9c.Tests/Action/RaidTest.cs index 5526cb0cfe..ae829f198d 100644 --- a/.Lib9c.Tests/Action/RaidTest.cs +++ b/.Lib9c.Tests/Action/RaidTest.cs @@ -386,10 +386,7 @@ int runeId2 { Assert.Equal(0 * _goldCurrency, nextState.GetBalance(_agentAddress, _goldCurrency)); Assert.Equal(purchaseCount + 1, nextState.GetRaiderState(raiderAddress).PurchaseCount); - var arenaData = _tableSheets.ArenaSheet.GetRoundByBlockIndex(ctx.BlockIndex); - var feeAddress = - ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); - Assert.True(nextState.GetBalance(feeAddress, _goldCurrency) > 0 * _goldCurrency); + Assert.True(nextState.GetBalance(Addresses.RewardPool, _goldCurrency) > 0 * _goldCurrency); } Assert.True(nextState.TryGetLegacyState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); diff --git a/.Lib9c.Tests/Action/RewardGoldTest.cs b/.Lib9c.Tests/Action/RewardGoldTest.cs index 63df7d4cf9..3bf008441e 100644 --- a/.Lib9c.Tests/Action/RewardGoldTest.cs +++ b/.Lib9c.Tests/Action/RewardGoldTest.cs @@ -449,48 +449,48 @@ public void GoldDistributedEachAccount() Assert.Equal(currency * 0, delta.GetBalance(address2, currency)); } - [Fact] - public void MiningReward() - { - Address miner = new Address("F9A15F870701268Bd7bBeA6502eB15F4997f32f9"); - Currency currency = _baseState.GetGoldCurrency(); - var ctx = new ActionContext() - { - BlockIndex = 0, - PreviousState = _baseState, - Miner = miner, - }; - - var action = new RewardGold(); - - void AssertMinerReward(int blockIndex, string expected) - { - ctx.BlockIndex = blockIndex; - IWorld delta = action.MinerReward(ctx, _baseState); - Assert.Equal(FungibleAssetValue.Parse(currency, expected), delta.GetBalance(miner, currency)); - } - - // Before halving (10 / 2^0 = 10) - AssertMinerReward(0, "10"); - AssertMinerReward(1, "10"); - AssertMinerReward(12614400, "10"); - - // First halving (10 / 2^1 = 5) - AssertMinerReward(12614401, "5"); - AssertMinerReward(25228800, "5"); - - // Second halving (10 / 2^2 = 2.5) - AssertMinerReward(25228801, "2.5"); - AssertMinerReward(37843200, "2.5"); - - // Third halving (10 / 2^3 = 1.25) - AssertMinerReward(37843201, "1.25"); - AssertMinerReward(50457600, "1.25"); - - // Rewardless era - AssertMinerReward(50457601, "0"); - } - + // Temporarily commented, since mining reward action is changing. + //[Fact] + //public void MiningReward() + //{ + // Address miner = new Address("F9A15F870701268Bd7bBeA6502eB15F4997f32f9"); + // Currency currency = _baseState.GetGoldCurrency(); + // var ctx = new ActionContext() + // { + // BlockIndex = 0, + // PreviousState = _baseState, + // Miner = miner, + // }; + + // var action = new RewardGold(); + + // void AssertMinerReward(int blockIndex, string expected) + // { + // ctx.BlockIndex = blockIndex; + // IWorld delta = action.MinerReward(ctx, _baseState); + // Assert.Equal(FungibleAssetValue.Parse(currency, expected), delta.GetBalance(Addresses.RewardPool, currency)); + // } + + // // Before halving (10 / 2^0 = 10) + // AssertMinerReward(0, "10"); + // AssertMinerReward(1, "10"); + // AssertMinerReward(12614400, "10"); + + // // First halving (10 / 2^1 = 5) + // AssertMinerReward(12614401, "5"); + // AssertMinerReward(25228800, "5"); + + // // Second halving (10 / 2^2 = 2.5) + // AssertMinerReward(25228801, "2.5"); + // AssertMinerReward(37843200, "2.5"); + + // // Third halving (10 / 2^3 = 1.25) + // AssertMinerReward(37843201, "1.25"); + // AssertMinerReward(50457600, "1.25"); + + // // Rewardless era + // AssertMinerReward(50457601, "0"); + //} [Theory] [InlineData(5, 4)] [InlineData(101, 100)] diff --git a/.Lib9c.Tests/Action/Scenario/MarketScenarioTest.cs b/.Lib9c.Tests/Action/Scenario/MarketScenarioTest.cs index dd6c3f0b77..ab0bb70b44 100644 --- a/.Lib9c.Tests/Action/Scenario/MarketScenarioTest.cs +++ b/.Lib9c.Tests/Action/Scenario/MarketScenarioTest.cs @@ -306,8 +306,6 @@ public void Register_And_Buy() var latestState = action3.Execute(ctx); var buyerAvatarState = latestState.GetAvatarState(_buyerAvatarAddress); - var arenaData = _tableSheets.ArenaSheet.GetRoundByBlockIndex(3L); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); var totalTax = 0 * _currency; foreach (var group in action3.ProductInfos.GroupBy(p => p.AgentAddress)) { @@ -348,7 +346,7 @@ public void Register_And_Buy() Assert.True(totalTax > 0 * _currency); Assert.Equal(0 * _currency, latestState.GetBalance(_buyerAgentAddress, _currency)); - Assert.Equal(totalTax, latestState.GetBalance(feeStoreAddress, _currency)); + Assert.Equal(totalTax, latestState.GetBalance(Addresses.RewardPool, _currency)); } [Fact] @@ -1082,8 +1080,6 @@ public void HardFork() }); var buyerAvatarState = tradedState.GetAvatarState(_buyerAvatarAddress); - var arenaData = _tableSheets.ArenaSheet.GetRoundByBlockIndex(3L); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); var totalTax = 0 * _currency; foreach (var group in buyAction.ProductInfos.GroupBy(p => p.AgentAddress)) { @@ -1124,7 +1120,7 @@ public void HardFork() Assert.True(totalTax > 0 * _currency); Assert.Equal(0 * _currency, tradedState.GetBalance(_buyerAgentAddress, _currency)); - Assert.Equal(totalTax, tradedState.GetBalance(feeStoreAddress, _currency)); + Assert.Equal(totalTax, tradedState.GetBalance(Addresses.RewardPool, _currency)); } [Theory] diff --git a/.Lib9c.Tests/Action/Scenario/MeadScenarioTest.cs b/.Lib9c.Tests/Action/Scenario/MeadScenarioTest.cs index 0bf467e478..c0db972084 100644 --- a/.Lib9c.Tests/Action/Scenario/MeadScenarioTest.cs +++ b/.Lib9c.Tests/Action/Scenario/MeadScenarioTest.cs @@ -79,7 +79,7 @@ public void Contract() Assert.Equal(4 * mead, states6.GetBalance(agentAddress, mead)); } - [Fact] + [Fact(Skip = "No way tracing gas usage outside of ActionEvaluator for now")] public void UseGas() { Type baseType = typeof(Nekoyume.Action.ActionBase); @@ -119,7 +119,7 @@ bool IsTarget(Type type) long expectedGasLimit = action is ITransferAsset || action is ITransferAssets ? expectedTransferActionGasLimit : expectedActionGasLimit; - long gasUsed = actionContext.GasUsed(); + long gasUsed = GasTracer.GasUsed; Assert.True(expectedGasLimit == gasUsed, $"{action} invalid used gas. {gasUsed}"); } } diff --git a/.Lib9c.Tests/Action/Scenario/StakeAndClaimScenarioTest.cs b/.Lib9c.Tests/Action/Scenario/StakeAndClaimScenarioTest.cs index e4c0b7bedb..ac4c3e119c 100644 --- a/.Lib9c.Tests/Action/Scenario/StakeAndClaimScenarioTest.cs +++ b/.Lib9c.Tests/Action/Scenario/StakeAndClaimScenarioTest.cs @@ -15,6 +15,7 @@ namespace Lib9c.Tests.Action.Scenario using Nekoyume.Module; using Nekoyume.TableData; using Nekoyume.TableData.Stake; + using Nekoyume.ValidatorDelegation; using Serilog; using Xunit; using Xunit.Abstractions; @@ -95,7 +96,7 @@ public void Test() state, _agentAddr, _avatarAddr, - stake2BlockIndex + StakeState.LockupInterval); + stake2BlockIndex + LegacyStakeState.LockupInterval); // Validate staked. ValidateStakedStateV2( @@ -105,14 +106,20 @@ public void Test() stake2BlockIndex, "StakeRegularFixedRewardSheet_V1", "StakeRegularRewardSheet_V1", - StakeState.RewardInterval, - StakeState.LockupInterval); + LegacyStakeState.RewardInterval, + LegacyStakeState.LockupInterval); + + var validatorKey = new PrivateKey().PublicKey; + state = DelegationUtil.EnsureValidatorPromotionReady(state, validatorKey, 0L); + state = DelegationUtil.MakeGuild(state, _agentAddr, validatorKey.Address, 0L); // Withdraw stake via stake3. - state = Stake3(state, _agentAddr, 0, stake2BlockIndex + StakeState.LockupInterval + 1); + state = Stake3(state, _agentAddr, 0, stake2BlockIndex + LegacyStakeState.LockupInterval + 1); + + state = DelegationUtil.EnsureStakeReleased(state, stake2BlockIndex + ValidatorDelegatee.ValidatorUnbondingPeriod); // Stake 50 NCG via stake3 before patching. - const long firstStake3BlockIndex = stake2BlockIndex + StakeState.LockupInterval + 1; + const long firstStake3BlockIndex = stake2BlockIndex + LegacyStakeState.LockupInterval + 1; state = Stake3( state, _agentAddr, @@ -222,10 +229,10 @@ private static void ValidateStakedState( FungibleAssetValue expectStakedAmount, long expectStartedBlockIndex) { - var stakeAddr = StakeState.DeriveAddress(agentAddr); + var stakeAddr = LegacyStakeState.DeriveAddress(agentAddr); var actualStakedAmount = state.GetBalance(stakeAddr, expectStakedAmount.Currency); Assert.Equal(expectStakedAmount, actualStakedAmount); - var stakeState = new StakeState((Dictionary)state.GetLegacyState(stakeAddr)); + var stakeState = new LegacyStakeState((Dictionary)state.GetLegacyState(stakeAddr)); Assert.Equal(expectStartedBlockIndex, stakeState.StartedBlockIndex); } @@ -239,10 +246,10 @@ private static void ValidateStakedStateV2( long expectRewardInterval, long expectLockupInterval) { - var stakeAddr = StakeState.DeriveAddress(agentAddr); + var stakeAddr = LegacyStakeState.DeriveAddress(agentAddr); var actualStakedAmount = state.GetBalance(stakeAddr, expectStakedAmount.Currency); Assert.Equal(expectStakedAmount, actualStakedAmount); - var stakeState = new StakeStateV2(state.GetLegacyState(stakeAddr)); + var stakeState = new StakeState(state.GetLegacyState(stakeAddr)); Assert.Equal(expectStartedBlockIndex, stakeState.StartedBlockIndex); Assert.Equal( expectStakeRegularFixedRewardSheetName, diff --git a/.Lib9c.Tests/Action/Stake2Test.cs b/.Lib9c.Tests/Action/Stake2Test.cs index c1e7958343..4cc232559a 100644 --- a/.Lib9c.Tests/Action/Stake2Test.cs +++ b/.Lib9c.Tests/Action/Stake2Test.cs @@ -94,10 +94,10 @@ public void Execute_Throws_WhenThereIsMonsterCollection() [Fact] public void Execute_Throws_WhenClaimableExisting() { - Address stakeStateAddress = StakeState.DeriveAddress(_signerAddress); + Address stakeStateAddress = LegacyStakeState.DeriveAddress(_signerAddress); var context = new ActionContext(); var states = _initialState - .SetLegacyState(stakeStateAddress, new StakeState(stakeStateAddress, 0).Serialize()) + .SetLegacyState(stakeStateAddress, new LegacyStakeState(stakeStateAddress, 0).Serialize()) .MintAsset(context, stakeStateAddress, _currency * 50); var action = new Stake2(100); Assert.Throws(() => @@ -105,7 +105,7 @@ public void Execute_Throws_WhenClaimableExisting() { PreviousState = states, Signer = _signerAddress, - BlockIndex = StakeState.RewardInterval, + BlockIndex = LegacyStakeState.RewardInterval, })); } @@ -139,11 +139,11 @@ public void Execute_Throws_WhenCancelOrUpdateWhileLockup() })); // Same (since 4611070) - if (states.TryGetStakeState(_signerAddress, out StakeState stakeState)) + if (states.TryGetLegacyStakeState(_signerAddress, out LegacyStakeState stakeState)) { states = states.SetLegacyState( stakeState.address, - new StakeState(stakeState.address, 4611070 - 100).Serialize()); + new LegacyStakeState(stakeState.address, 4611070 - 100).Serialize()); } updateAction = new Stake2(51); @@ -160,7 +160,7 @@ public void Execute_Throws_WhenCancelOrUpdateWhileLockup() PreviousState = states, Signer = _signerAddress, BlockIndex = 4611070 - 99, - }).TryGetStakeState(_signerAddress, out stakeState)); + }).TryGetLegacyStakeState(_signerAddress, out stakeState)); Assert.Equal(4611070 - 99, stakeState.StartedBlockIndex); } @@ -178,11 +178,11 @@ public void Execute() Assert.Equal(_currency * 0, states.GetBalance(_signerAddress, _currency)); Assert.Equal( _currency * 100, - states.GetBalance(StakeState.DeriveAddress(_signerAddress), _currency)); + states.GetBalance(LegacyStakeState.DeriveAddress(_signerAddress), _currency)); - states.TryGetStakeState(_signerAddress, out StakeState stakeState); + states.TryGetLegacyStakeState(_signerAddress, out LegacyStakeState stakeState); Assert.Equal(0, stakeState.StartedBlockIndex); - Assert.Equal(0 + StakeState.LockupInterval, stakeState.CancellableBlockIndex); + Assert.Equal(0 + LegacyStakeState.LockupInterval, stakeState.CancellableBlockIndex); Assert.Equal(0, stakeState.ReceivedBlockIndex); Assert.Equal(_currency * 100, states.GetBalance(stakeState.address, _currency)); Assert.Equal(_currency * 0, states.GetBalance(_signerAddress, _currency)); @@ -192,20 +192,20 @@ public void Execute() Assert.False(achievements.Check(0, 1)); Assert.False(achievements.Check(1, 0)); - StakeState producedStakeState = new StakeState( + LegacyStakeState producedLegacyStakeState = new LegacyStakeState( stakeState.address, stakeState.StartedBlockIndex, // Produce a situation that it already received rewards. - StakeState.LockupInterval - 1, + LegacyStakeState.LockupInterval - 1, stakeState.CancellableBlockIndex, stakeState.Achievements); - states = states.SetLegacyState(stakeState.address, producedStakeState.SerializeV2()); + states = states.SetLegacyState(stakeState.address, producedLegacyStakeState.SerializeV2()); var cancelAction = new Stake2(0); states = cancelAction.Execute(new ActionContext { PreviousState = states, Signer = _signerAddress, - BlockIndex = StakeState.LockupInterval, + BlockIndex = LegacyStakeState.LockupInterval, }); Assert.Null(states.GetLegacyState(stakeState.address)); @@ -224,9 +224,9 @@ public void Update() BlockIndex = 0, }); - states.TryGetStakeState(_signerAddress, out StakeState stakeState); + states.TryGetLegacyStakeState(_signerAddress, out LegacyStakeState stakeState); Assert.Equal(0, stakeState.StartedBlockIndex); - Assert.Equal(0 + StakeState.LockupInterval, stakeState.CancellableBlockIndex); + Assert.Equal(0 + LegacyStakeState.LockupInterval, stakeState.CancellableBlockIndex); Assert.Equal(0, stakeState.ReceivedBlockIndex); Assert.Equal(_currency * 50, states.GetBalance(stakeState.address, _currency)); Assert.Equal(_currency * 50, states.GetBalance(_signerAddress, _currency)); @@ -239,9 +239,9 @@ public void Update() BlockIndex = 1, }); - states.TryGetStakeState(_signerAddress, out stakeState); + states.TryGetLegacyStakeState(_signerAddress, out stakeState); Assert.Equal(1, stakeState.StartedBlockIndex); - Assert.Equal(1 + StakeState.LockupInterval, stakeState.CancellableBlockIndex); + Assert.Equal(1 + LegacyStakeState.LockupInterval, stakeState.CancellableBlockIndex); Assert.Equal(0, stakeState.ReceivedBlockIndex); Assert.Equal(_currency * 100, states.GetBalance(stakeState.address, _currency)); Assert.Equal(_currency * 0, states.GetBalance(_signerAddress, _currency)); @@ -254,8 +254,8 @@ public void RestrictForStakeStateV2() Assert.Throws(() => action.Execute(new ActionContext { PreviousState = _initialState.SetLegacyState( - StakeState.DeriveAddress(_signerAddress), - new StakeStateV2( + LegacyStakeState.DeriveAddress(_signerAddress), + new StakeState( new Contract( "StakeRegularFixedRewardSheet_V1", "StakeRegularRewardSheet_V1", diff --git a/.Lib9c.Tests/Action/StakeTest.cs b/.Lib9c.Tests/Action/StakeTest.cs index fc42f3e94a..5de72ac960 100644 --- a/.Lib9c.Tests/Action/StakeTest.cs +++ b/.Lib9c.Tests/Action/StakeTest.cs @@ -11,11 +11,17 @@ namespace Lib9c.Tests.Action using Libplanet.Types.Assets; using Nekoyume; using Nekoyume.Action; + using Nekoyume.Action.ValidatorDelegation; using Nekoyume.Exceptions; + using Nekoyume.Model.Guild; using Nekoyume.Model.Stake; using Nekoyume.Model.State; using Nekoyume.Module; + using Nekoyume.Module.Guild; + using Nekoyume.Module.ValidatorDelegation; using Nekoyume.TableData.Stake; + using Nekoyume.TypedAddress; + using Nekoyume.ValidatorDelegation; using Serilog; using Xunit; using Xunit.Abstractions; @@ -222,9 +228,9 @@ public void Execute_Throw_StateNullException_Via_0_Amount() [Theory] // NOTE: minimum required_gold of StakeRegularRewardSheetFixtures.V2 is 50. - [InlineData(0, 50, StakeState.RewardInterval)] + [InlineData(0, 50, LegacyStakeState.RewardInterval)] [InlineData( - long.MaxValue - StakeState.RewardInterval, + long.MaxValue - LegacyStakeState.RewardInterval, long.MaxValue, long.MaxValue)] public void Execute_Throw_StakeExistingClaimableException_With_StakeState( @@ -232,8 +238,8 @@ public void Execute_Throw_StakeExistingClaimableException_With_StakeState( long previousAmount, long blockIndex) { - var stakeStateAddr = StakeState.DeriveAddress(_agentAddr); - var stakeState = new StakeState( + var stakeStateAddr = LegacyStakeState.DeriveAddress(_agentAddr); + var stakeState = new LegacyStakeState( address: stakeStateAddr, startedBlockIndex: previousStartedBlockIndex); Assert.True(stakeState.IsClaimable(blockIndex)); @@ -265,8 +271,8 @@ public void Execute_Throw_StakeExistingClaimableException_With_StakeStateV2( long previousAmount, long blockIndex) { - var stakeStateAddr = StakeStateV2.DeriveAddress(_agentAddr); - var stakeStateV2 = new StakeStateV2( + var stakeStateAddr = StakeState.DeriveAddress(_agentAddr); + var stakeStateV2 = new StakeState( contract: new Contract(_stakePolicySheet), startedBlockIndex: previousStartedBlockIndex); var previousState = _initialState @@ -284,193 +290,176 @@ public void Execute_Throw_StakeExistingClaimableException_With_StakeStateV2( previousAmount)); } - [Theory] - // NOTE: minimum required_gold of StakeRegularRewardSheetFixtures.V2 is 50. - // NOTE: LockupInterval of StakePolicySheetFixtures.V2 is 201,600. - [InlineData(0, 50 + 1, 201_600 - 1, 50)] - [InlineData( - long.MaxValue - 201_600, - 50 + 1, - long.MaxValue - 1, - 50)] - public void - Execute_Throw_RequiredBlockIndexException_Via_Reduced_Amount_When_Lucked_Up_With_StakeState( - long previousStartedBlockIndex, - long previousAmount, - long blockIndex, - long reducedAmount) - { - var stakeStateAddr = StakeState.DeriveAddress(_agentAddr); - var stakeState = new StakeState( - address: stakeStateAddr, - startedBlockIndex: previousStartedBlockIndex); - Assert.False(stakeState.IsCancellable(blockIndex)); - stakeState.Claim(blockIndex); - Assert.False(stakeState.IsClaimable(blockIndex)); - var previousState = _initialState - .MintAsset( - new ActionContext { Signer = Addresses.Admin }, - stakeStateAddr, - _ncg * previousAmount) - .SetLegacyState(stakeStateAddr, stakeState.Serialize()); - Assert.Throws(() => - Execute( - blockIndex, - previousState, - new TestRandom(), - _agentAddr, - reducedAmount)); - } - - [Theory] - // NOTE: minimum required_gold of StakeRegularRewardSheetFixtures.V2 is 50. - // NOTE: LockupInterval of StakePolicySheetFixtures.V2 is 201,600. - [InlineData(0, 50 + 1, 201_600 - 1, 50)] - [InlineData( - long.MaxValue - 201_600, - 50 + 1, - long.MaxValue - 1, - 50)] - public void - Execute_Throw_RequiredBlockIndexException_Via_Reduced_Amount_When_Locked_Up_With_StakeStateV2( - long previousStartedBlockIndex, - long previousAmount, - long blockIndex, - long reducedAmount) - { - var stakeStateAddr = StakeStateV2.DeriveAddress(_agentAddr); - var stakeStateV2 = new StakeStateV2( - contract: new Contract(_stakePolicySheet), - startedBlockIndex: previousStartedBlockIndex, - receivedBlockIndex: blockIndex); - var previousState = _initialState - .MintAsset( - new ActionContext { Signer = Addresses.Admin }, - stakeStateAddr, - _ncg * previousAmount) - .SetLegacyState(stakeStateAddr, stakeStateV2.Serialize()); - Assert.Throws(() => - Execute( - blockIndex, - previousState, - new TestRandom(), - _agentAddr, - reducedAmount)); - } - [Theory] // NOTE: minimum required_gold of StakeRegularRewardSheetFixtures.V2 is 50. [InlineData(50)] [InlineData(long.MaxValue)] public void Execute_Success_When_Staking_State_Null(long amount) { - var previousState = _initialState.MintAsset( + var world = _initialState.MintAsset( new ActionContext { Signer = Addresses.Admin }, _agentAddr, _ncg * amount); + var height = 0L; + + var validatorKey = new PrivateKey().PublicKey; + world = DelegationUtil.EnsureValidatorPromotionReady(world, validatorKey, height++); + world = DelegationUtil.MakeGuild(world, _agentAddr, validatorKey.Address, height++); + Execute( 0, - previousState, + world, new TestRandom(), _agentAddr, amount); + + world = DelegationUtil.EnsureStakeReleased( + world, height + ValidatorDelegatee.ValidatorUnbondingPeriod); } [Theory] - // NOTE: minimum required_gold of StakeRegularRewardSheetFixtures.V2 is 50. - // NOTE: non claimable, locked up, same amount. - [InlineData(0, 50, 0, 50)] - [InlineData(0, 50, StakeState.LockupInterval - 1, 50)] - [InlineData(0, long.MaxValue, 0, long.MaxValue)] - [InlineData(0, long.MaxValue, StakeState.LockupInterval - 1, long.MaxValue)] - // NOTE: non claimable, locked up, increased amount. - [InlineData(0, 50, 0, 500)] - [InlineData(0, 50, StakeState.LockupInterval - 1, 500)] - // NOTE: non claimable, unlocked, same amount. - [InlineData(0, 50, StakeState.LockupInterval, 50)] - [InlineData(0, long.MaxValue, StakeState.LockupInterval, long.MaxValue)] - // NOTE: non claimable, unlocked, increased amount. - [InlineData(0, 50, StakeState.LockupInterval, 500)] - // NOTE: non claimable, unlocked, decreased amount. - [InlineData(0, 50, StakeState.LockupInterval, 0)] - [InlineData(0, long.MaxValue, StakeState.LockupInterval, 50)] - public void Execute_Success_When_Exist_StakeState( - long previousStartedBlockIndex, + // NOTE: non + [InlineData(50, 50)] + [InlineData(long.MaxValue, long.MaxValue)] + // NOTE: delegate + [InlineData(0, 500)] + [InlineData(50, 100)] + [InlineData(0, long.MaxValue)] + // NOTE: undelegate + [InlineData(50, 0)] + [InlineData(75, 50)] + [InlineData(long.MaxValue, 0)] + [InlineData(long.MaxValue, 500)] + public void Execute_Success_When_Exist_StakeStateV3( long previousAmount, - long blockIndex, long amount) { + var interval = previousAmount < amount + ? LegacyStakeState.RewardInterval : LegacyStakeState.LockupInterval; var stakeStateAddr = StakeState.DeriveAddress(_agentAddr); var stakeState = new StakeState( - address: stakeStateAddr, - startedBlockIndex: previousStartedBlockIndex); - stakeState.Claim(blockIndex); - var previousState = _initialState - .MintAsset( - new ActionContext(), - _agentAddr, - _ncg * Math.Max(previousAmount, amount)) - .TransferAsset( - new ActionContext(), - _agentAddr, - stakeStateAddr, - _ncg * previousAmount) - .SetLegacyState(stakeStateAddr, stakeState.Serialize()); - Execute( - blockIndex, - previousState, + contract: new Contract(_stakePolicySheet), + startedBlockIndex: 0L, + receivedBlockIndex: interval, + stateVersion: 3); + var world = _initialState; + var height = 0L; + + var validatorKey = new PrivateKey().PublicKey; + world = DelegationUtil.EnsureValidatorPromotionReady(world, validatorKey, height); + world = DelegationUtil.MakeGuild(world, _agentAddr, validatorKey.Address, height); + if (previousAmount > 0) + { + var ncgToStake = _ncg * previousAmount; + var gg = FungibleAssetValue.Parse(Currencies.GuildGold, ncgToStake.GetQuantityString(true)); + world = DelegationUtil.MintGuildGold(world, _agentAddr, gg, height); + world = world.MintAsset(new ActionContext(), _agentAddr, ncgToStake); + world = world.TransferAsset( + new ActionContext(), _agentAddr, stakeStateAddr, ncgToStake); + + var guildRepository = new GuildRepository(world, new ActionContext { Signer = _agentAddr }); + var guildParticipant = guildRepository.GetGuildParticipant(_agentAddr); + var guild = guildRepository.GetGuild(guildParticipant.GuildAddress); + guildParticipant.Delegate(guild, gg, height); + world = guildRepository.World; + } + + world = world.SetLegacyState(stakeStateAddr, stakeState.Serialize()); + + if (amount - previousAmount > 0) + { + var ncgToStake = _ncg * (amount - previousAmount); + world = world.MintAsset(new ActionContext(), _agentAddr, ncgToStake); + } + + var nextState = Execute( + height + interval, + world, new TestRandom(), _agentAddr, amount); + + if (amount > 0) + { + Assert.True(nextState.TryGetStakeState(_agentAddr, out StakeState nextStakeState)); + Assert.Equal(3, nextStakeState.StateVersion); + } + + world = DelegationUtil.EnsureStakeReleased( + nextState, height + LegacyStakeState.LockupInterval); + + var expectedBalance = _ncg * Math.Max(0, previousAmount - amount); + var actualBalance = world.GetBalance(_agentAddr, _ncg); + Assert.Equal(expectedBalance, actualBalance); } [Theory] - // NOTE: minimum required_gold of StakeRegularRewardSheetFixtures.V2 is 50. - // NOTE: LockupInterval of StakePolicySheetFixtures.V2 is 201,600. - // NOTE: non claimable, locked up, same amount. - [InlineData(0, 50, 0, 50)] - [InlineData(0, 50, 201_599, 50)] - [InlineData(0, long.MaxValue, 0, long.MaxValue)] - [InlineData(0, long.MaxValue, 201_599, long.MaxValue)] - // NOTE: non claimable, locked up, increased amount. - [InlineData(0, 50, 0, 500)] - [InlineData(0, 50, 201_599, 500)] - // NOTE: non claimable, unlocked, same amount. - [InlineData(0, 50, 201_600, 50)] - [InlineData(0, long.MaxValue, 201_600, long.MaxValue)] - // NOTE: non claimable, unlocked, increased amount. - [InlineData(0, 50, 201_600, 500)] - // NOTE: non claimable, unlocked, decreased amount. - [InlineData(0, 50, 201_600, 0)] - [InlineData(0, long.MaxValue, StakeState.LockupInterval, 50)] - public void Execute_Success_When_Exist_StakeStateV2( - long previousStartedBlockIndex, + // NOTE: non + [InlineData(50, 50)] + [InlineData(long.MaxValue, long.MaxValue)] + // NOTE: delegate + [InlineData(0, 500)] + [InlineData(50, 100)] + [InlineData(0, long.MaxValue)] + // NOTE: undelegate + [InlineData(50, 0)] + [InlineData(75, 50)] + [InlineData(long.MaxValue, 0)] + [InlineData(long.MaxValue, 500)] + public void Execute_Success_When_Exist_StakeStateV3_Without_Guild( long previousAmount, - long blockIndex, long amount) { - var stakeStateAddr = StakeStateV2.DeriveAddress(_agentAddr); - var stakeStateV2 = new StakeStateV2( + var interval = previousAmount < amount + ? LegacyStakeState.RewardInterval : LegacyStakeState.LockupInterval; + var stakeStateAddr = StakeState.DeriveAddress(_agentAddr); + var stakeState = new StakeState( contract: new Contract(_stakePolicySheet), - startedBlockIndex: previousStartedBlockIndex, - receivedBlockIndex: blockIndex); - var previousState = _initialState - .MintAsset( - new ActionContext(), - _agentAddr, - _ncg * Math.Max(previousAmount, amount)) - .TransferAsset( - new ActionContext(), - _agentAddr, - stakeStateAddr, - _ncg * previousAmount) - .SetLegacyState(stakeStateAddr, stakeStateV2.Serialize()); - Execute( - blockIndex, - previousState, + startedBlockIndex: 0L, + receivedBlockIndex: interval, + stateVersion: 3); + var world = _initialState; + var height = 0L; + + if (previousAmount > 0) + { + var ncgToStake = _ncg * previousAmount; + var gg = FungibleAssetValue.Parse(Currencies.GuildGold, ncgToStake.GetQuantityString(true)); + world = DelegationUtil.MintGuildGold(world, _agentAddr, gg, height); + world = world.MintAsset(new ActionContext(), _agentAddr, ncgToStake); + world = world.TransferAsset( + new ActionContext(), _agentAddr, stakeStateAddr, ncgToStake); + world = world.TransferAsset( + new ActionContext(), stakeStateAddr, Addresses.NonValidatorDelegatee, gg); + } + + world = world.SetLegacyState(stakeStateAddr, stakeState.Serialize()); + + if (amount - previousAmount > 0) + { + var ncgToStake = _ncg * (amount - previousAmount); + world = world.MintAsset(new ActionContext(), _agentAddr, ncgToStake); + } + + var nextState = Execute( + height + interval, + world, new TestRandom(), _agentAddr, amount); + + if (amount > 0) + { + Assert.True(nextState.TryGetStakeState(_agentAddr, out StakeState nextStakeState)); + Assert.Equal(3, nextStakeState.StateVersion); + } + + world = DelegationUtil.EnsureStakeReleased( + nextState, height + LegacyStakeState.LockupInterval); + + var expectedBalance = _ncg * Math.Max(0, previousAmount - amount); + var actualBalance = world.GetBalance(_agentAddr, _ncg); + Assert.Equal(expectedBalance, actualBalance); } private IWorld Execute( @@ -480,11 +469,6 @@ private IWorld Execute( Address signer, long amount) { - var previousBalance = previousState.GetBalance(signer, _ncg); - var previousStakeBalance = previousState.GetBalance( - StakeState.DeriveAddress(signer), - _ncg); - var previousTotalBalance = previousBalance + previousStakeBalance; var action = new Stake(amount); var nextState = action.Execute(new ActionContext { @@ -494,21 +478,25 @@ private IWorld Execute( Signer = signer, }); - var amountNCG = _ncg * amount; - var nextBalance = nextState.GetBalance(signer, _ncg); - var nextStakeBalance = nextState.GetBalance( - StakeState.DeriveAddress(signer), - _ncg); - Assert.Equal(previousTotalBalance - amountNCG, nextBalance); - Assert.Equal(amountNCG, nextStakeBalance); + var guildRepository = new GuildRepository(nextState, new ActionContext()); + if (guildRepository.TryGetGuildParticipant(new AgentAddress(signer), out var guildParticipant)) + { + var guild = guildRepository.GetGuild(guildParticipant.GuildAddress); + var validator = guildRepository.GetGuildDelegatee(guild.ValidatorAddress); + var bond = guildRepository.GetBond(validator, signer); + var amountNCG = _ncg * amount; + var expectedGG = DelegationUtil.GetGuildCoinFromNCG(amountNCG); + var expectedShare = validator.ShareFromFAV(expectedGG); + Assert.Equal(expectedShare, bond.Share); + } if (amount == 0) { - Assert.False(nextState.TryGetStakeStateV2(_agentAddr, out _)); + Assert.False(nextState.TryGetStakeState(_agentAddr, out _)); } else if (amount > 0) { - Assert.True(nextState.TryGetStakeStateV2(_agentAddr, out var stakeStateV2)); + Assert.True(nextState.TryGetStakeState(_agentAddr, out var stakeStateV2)); Assert.Equal( _stakePolicySheet.StakeRegularFixedRewardSheetValue, stakeStateV2.Contract.StakeRegularFixedRewardSheetTableName); @@ -523,9 +511,6 @@ private IWorld Execute( stakeStateV2.Contract.LockupInterval); Assert.Equal(blockIndex, stakeStateV2.StartedBlockIndex); Assert.Equal(0, stakeStateV2.ReceivedBlockIndex); - Assert.Equal( - blockIndex + stakeStateV2.Contract.LockupInterval, - stakeStateV2.CancellableBlockIndex); Assert.Equal(blockIndex, stakeStateV2.ClaimedBlockIndex); Assert.Equal( blockIndex + stakeStateV2.Contract.RewardInterval, diff --git a/.Lib9c.Tests/Action/TransferAssetTest.cs b/.Lib9c.Tests/Action/TransferAssetTest.cs index 4ec020e08e..b3b0faac60 100644 --- a/.Lib9c.Tests/Action/TransferAssetTest.cs +++ b/.Lib9c.Tests/Action/TransferAssetTest.cs @@ -290,7 +290,7 @@ public void Execute_Throw_ArgumentException() .SetBalance(_sender, _currency * 1000)); var action = new TransferAsset( sender: _sender, - recipient: StakeState.DeriveAddress(_recipient), + recipient: LegacyStakeState.DeriveAddress(_recipient), amount: _currency * 100 ); // 스테이킹 주소에 송금하려고 하면 실패합니다. @@ -298,8 +298,8 @@ public void Execute_Throw_ArgumentException() { PreviousState = baseState .SetLegacyState( - StakeState.DeriveAddress(_recipient), - new StakeState(StakeState.DeriveAddress(_recipient), 0).SerializeV2()), + LegacyStakeState.DeriveAddress(_recipient), + new LegacyStakeState(LegacyStakeState.DeriveAddress(_recipient), 0).SerializeV2()), Signer = _sender, BlockIndex = 1, })); @@ -307,8 +307,8 @@ public void Execute_Throw_ArgumentException() { PreviousState = baseState .SetLegacyState( - StakeState.DeriveAddress(_recipient), - new StakeStateV2( + LegacyStakeState.DeriveAddress(_recipient), + new StakeState( new Contract( "StakeRegularFixedRewardSheet_V1", "StakeRegularRewardSheet_V1", @@ -322,7 +322,7 @@ public void Execute_Throw_ArgumentException() { PreviousState = baseState .SetLegacyState( - StakeState.DeriveAddress(_recipient), + LegacyStakeState.DeriveAddress(_recipient), new MonsterCollectionState( MonsterCollectionState.DeriveAddress(_sender, 0), 1, @@ -338,7 +338,7 @@ public void Execute_Throw_ArgumentException() { PreviousState = baseState .SetLegacyState( - StakeState.DeriveAddress(_recipient), + LegacyStakeState.DeriveAddress(_recipient), new MonsterCollectionState0( MonsterCollectionState.DeriveAddress(_sender, 0), 1, diff --git a/.Lib9c.Tests/Action/TransferAssetsTest.cs b/.Lib9c.Tests/Action/TransferAssetsTest.cs index 70a3169f44..a44bdc0e10 100644 --- a/.Lib9c.Tests/Action/TransferAssetsTest.cs +++ b/.Lib9c.Tests/Action/TransferAssetsTest.cs @@ -372,7 +372,7 @@ public void Execute_Throw_ArgumentException() sender: _sender, new List<(Address, FungibleAssetValue)> { - (StakeState.DeriveAddress(_recipient), _currency * 100), + (LegacyStakeState.DeriveAddress(_recipient), _currency * 100), (_recipient2, _currency * 100), } ); @@ -381,8 +381,8 @@ public void Execute_Throw_ArgumentException() { PreviousState = baseState .SetLegacyState( - StakeState.DeriveAddress(_recipient), - new StakeState(StakeState.DeriveAddress(_recipient), 0).SerializeV2()), + LegacyStakeState.DeriveAddress(_recipient), + new LegacyStakeState(LegacyStakeState.DeriveAddress(_recipient), 0).SerializeV2()), Signer = _sender, BlockIndex = 1, })); @@ -390,8 +390,8 @@ public void Execute_Throw_ArgumentException() { PreviousState = baseState .SetLegacyState( - StakeState.DeriveAddress(_recipient), - new StakeStateV2( + LegacyStakeState.DeriveAddress(_recipient), + new StakeState( new Contract( "StakeRegularFixedRewardSheet_V1", "StakeRegularRewardSheet_V1", @@ -405,7 +405,7 @@ public void Execute_Throw_ArgumentException() { PreviousState = baseState .SetLegacyState( - StakeState.DeriveAddress(_recipient), + LegacyStakeState.DeriveAddress(_recipient), new MonsterCollectionState( MonsterCollectionState.DeriveAddress(_sender, 0), 1, @@ -421,7 +421,7 @@ public void Execute_Throw_ArgumentException() { PreviousState = baseState .SetLegacyState( - StakeState.DeriveAddress(_recipient), + LegacyStakeState.DeriveAddress(_recipient), new MonsterCollectionState0( MonsterCollectionState.DeriveAddress(_sender, 0), 1, diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/AllocateGuildRewardTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/AllocateGuildRewardTest.cs new file mode 100644 index 0000000000..789736d50f --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/AllocateGuildRewardTest.cs @@ -0,0 +1,333 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; +using Nekoyume; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.Model.State; +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class AllocateGuildRewardTest : ValidatorDelegationTestBase +{ + private interface IAllocateRewardFixture + { + FungibleAssetValue TotalReward { get; } + + ValidatorInfo[] ValidatorsInfos { get; } + + DelegatorInfo[] Delegatorinfos { get; } + + PrivateKey[] ValidatorKeys => ValidatorsInfos.Select(info => info.Key).ToArray(); + + FungibleAssetValue[] ValidatorCashes => ValidatorsInfos.Select(info => info.Cash).ToArray(); + + FungibleAssetValue[] ValidatorBalances + => ValidatorsInfos.Select(info => info.Balance).ToArray(); + + PrivateKey GetProposerKey(List validators) + { + return ValidatorsInfos + .Where(item => item.VoteFlag == VoteFlag.PreCommit) + .Where(item => validators.Any(v => v.PublicKey.Equals(item.Key.PublicKey))) + .Take(ValidatorList.MaxActiveSetSize) + .First() + .Key; + } + } + + public static IEnumerable RandomSeeds => new List + { + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + }; + + [Fact] + public void Serialization() + { + var action = new AllocateGuildReward(); + var plainValue = action.PlainValue; + + var deserialized = new AllocateGuildReward(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + var fixture = new StaticFixture + { + TotalReward = GuildAllocateRewardCurrency * 1000, + ValidatorsInfos = CreateArray(4, i => new ValidatorInfo + { + Key = new PrivateKey(), + Cash = DelegationCurrency * 10, + Balance = DelegationCurrency * 100, + VoteFlag = i % 2 == 0 ? VoteFlag.PreCommit : VoteFlag.Null, + }), + Delegatorinfos = Array.Empty(), + }; + ExecuteWithFixture(fixture); + } + + [Theory] + [InlineData(1, 1000)] + [InlineData(33, 33)] + [InlineData(33, 33.33)] + [InlineData(17, 71.57)] + [InlineData(1, 3)] + [InlineData(10, 2.79)] + [InlineData(1, 0.01)] + [InlineData(10, 0.01)] + public void Execute_Theory(int validatorCount, double totalReward) + { + var fixture = new StaticFixture + { + TotalReward = FungibleAssetValue.Parse(GuildAllocateRewardCurrency, $"{totalReward:R}"), + ValidatorsInfos = CreateArray(validatorCount, i => new ValidatorInfo + { + Key = new PrivateKey(), + Cash = DelegationCurrency * 10, + Balance = DelegationCurrency * 100, + VoteFlag = i % 2 == 0 ? VoteFlag.PreCommit : VoteFlag.Null, + }), + Delegatorinfos = Array.Empty(), + }; + ExecuteWithFixture(fixture); + } + + [Fact] + public void Execute_WithoutReward_Throw() + { + var fixture = new StaticFixture + { + TotalReward = GuildAllocateRewardCurrency * 0, + ValidatorsInfos = CreateArray(4, i => new ValidatorInfo + { + Key = new PrivateKey(), + Cash = DelegationCurrency * 10, + Balance = DelegationCurrency * 100, + VoteFlag = i % 2 == 0 ? VoteFlag.PreCommit : VoteFlag.Null, + }), + Delegatorinfos = Array.Empty(), + }; + Assert.Throws(() => ExecuteWithFixture(fixture)); + } + + [Theory] + [InlineData(0)] + [InlineData(1181126949)] + [InlineData(793705868)] + [InlineData(707058493)] + [InlineData(37149681)] + public void Execute_Theory_WithStaticSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Theory] + [MemberData(nameof(RandomSeeds))] + public void Execute_Theory_WithRandomSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + private static ImmutableArray CreateVotes( + ValidatorInfo[] validatorInfos, IReadOnlyList validatorList, long blockHeight) + { + var infoByPublicKey = validatorInfos.ToDictionary(k => k.Key.PublicKey, k => k); + var voteList = new List(validatorList.Count); + for (var i = 0; i < validatorList.Count; i++) + { + var validator = validatorList[i]; + var validatorInfo = infoByPublicKey[validator.PublicKey]; + var voteFlags = validatorInfo.VoteFlag; + var privateKey = voteFlags == VoteFlag.PreCommit ? validatorInfo.Key : null; + var voteMetadata = new VoteMetadata( + height: blockHeight, + round: 0, + blockHash: EmptyBlockHash, + timestamp: DateTimeOffset.UtcNow, + validatorPublicKey: validator.PublicKey, + validatorPower: validator.Power, + flag: voteFlags); + var vote = voteMetadata.Sign(privateKey); + voteList.Add(vote); + } + + return voteList.ToImmutableArray(); + } + + private void ExecuteWithFixture(IAllocateRewardFixture fixture) + { + // Given + // var length = fixture.ValidatorLength; + var totalReward = fixture.TotalReward; + var world = World; + var actionContext = new ActionContext { }; + var voteCount = fixture.ValidatorsInfos.Where( + item => item.VoteFlag == VoteFlag.PreCommit).Count(); + var validatorInfos = fixture.ValidatorsInfos; + var validatorKeys = fixture.ValidatorKeys; + var validatorCashes = fixture.ValidatorCashes; + var validatorBalances = fixture.ValidatorBalances; + var height = 1L; + world = EnsureToMintAssets(world, validatorKeys, validatorBalances, height++); + world = EnsurePromotedValidators(world, validatorKeys, validatorCashes, height++); + world = world.MintAsset(actionContext, Addresses.RewardPool, totalReward); + var repository = new ValidatorRepository(world, actionContext); + var activeSet = repository.GetValidatorList().ActiveSet(); + var proposerKey = fixture.GetProposerKey(activeSet); + world = EnsureProposer(world, proposerKey, height++); + + // Calculate expected values for comparison with actual values. + var votes = CreateVotes(validatorInfos, activeSet, height - 1); + var expectedProposerReward + = CalculatePropserReward(totalReward) + CalculateBonusPropserReward(votes, totalReward); + var expectedValidatorsReward = totalReward - expectedProposerReward; + var expectedCommunityFund = CalculateCommunityFund(votes, expectedValidatorsReward); + var expectedAllocatedReward = totalReward - expectedCommunityFund; + + // When + var lastCommit = new BlockCommit(height - 1, round: 0, votes[0].BlockHash, votes); + var allocateReward = new AllocateGuildReward(); + actionContext = new ActionContext + { + PreviousState = world, + Signer = proposerKey.PublicKey.Address, + LastCommit = lastCommit, + BlockIndex = height++, + }; + world = allocateReward.Execute(actionContext); + + // Then + var totalPower = votes.Select(item => item.ValidatorPower) + .OfType() + .Aggregate(BigInteger.Zero, (accum, next) => accum + next); + var actualRepository = new ValidatorRepository(world, actionContext); + var actualAllocatedReward = GuildAllocateRewardCurrency * 0; + var actualCommunityFund = world.GetBalance(Addresses.CommunityPool, GuildAllocateRewardCurrency); + foreach (var (vote, index) in votes.Select((v, i) => (v, i))) + { + if (vote.ValidatorPower is not { } validatorPower) + { + throw new InvalidOperationException("ValidatorPower cannot be null."); + } + + var validatorAddress = vote.ValidatorPublicKey.Address; + var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorAddress); + var validatorRewardAddress = actualDelegatee.CurrentLumpSumRewardsRecordAddress(); + var actualDelegationBalance = world.GetBalance(validatorAddress, DelegationCurrency); + var actualCommission = world.GetBalance(validatorAddress, GuildAllocateRewardCurrency); + var actualUnclaimedReward = world.GetBalance(validatorRewardAddress, GuildAllocateRewardCurrency); + var isProposer = vote.ValidatorPublicKey.Equals(proposerKey.PublicKey); + + if (vote.Flag == VoteFlag.Null) + { + Assert.Equal(GuildAllocateRewardCurrency * 0, actualCommission); + Assert.Equal(GuildAllocateRewardCurrency * 0, actualUnclaimedReward); + Assert.False(isProposer); + continue; + } + + var reward = (expectedValidatorsReward * validatorPower).DivRem(totalPower).Quotient; + var expectedCommission = CalculateCommission(reward, actualDelegatee); + var expectedUnclaimedReward = reward - expectedCommission; + expectedCommission = isProposer + ? expectedCommission + expectedProposerReward + : expectedCommission; + + Assert.Equal(expectedCommission, actualCommission); + Assert.Equal(expectedUnclaimedReward, actualUnclaimedReward); + + actualAllocatedReward += expectedCommission + expectedUnclaimedReward; + } + + Assert.Equal(expectedAllocatedReward, actualAllocatedReward); + Assert.Equal(expectedCommunityFund, actualCommunityFund); + } + + private struct ValidatorInfo + { + public PrivateKey Key { get; set; } + + public FungibleAssetValue Cash { get; set; } + + public FungibleAssetValue Balance { get; set; } + + public VoteFlag VoteFlag { get; set; } + } + + private struct DelegatorInfo + { + public PrivateKey Key { get; set; } + + public FungibleAssetValue Cash { get; set; } + + public FungibleAssetValue Balance { get; set; } + } + + private struct StaticFixture : IAllocateRewardFixture + { + public FungibleAssetValue TotalReward { get; set; } + + public ValidatorInfo[] ValidatorsInfos { get; set; } + + public DelegatorInfo[] Delegatorinfos { get; set; } + } + + private class RandomFixture : IAllocateRewardFixture + { + private readonly Random _random; + + public RandomFixture(int randomSeed) + { + _random = new Random(randomSeed); + TotalReward = GetRandomFAV(GuildAllocateRewardCurrency, _random); + ValidatorsInfos = CreateArray(_random.Next(1, 200), i => + { + var balance = GetRandomFAV(DelegationCurrency, _random); + var cash = GetRandomCash(_random, balance); + var flag = i == 0 || _random.Next() % 2 == 0 ? VoteFlag.PreCommit : VoteFlag.Null; + return new ValidatorInfo + { + Key = new PrivateKey(), + Balance = balance < MinimumDelegation ? MinimumDelegation : balance, + Cash = cash < MinimumDelegation ? MinimumDelegation : cash, + VoteFlag = flag, + }; + }); + Delegatorinfos = CreateArray(_random.Next(1, 200), i => + { + var balance = GetRandomFAV(DelegationCurrency, _random); + return new DelegatorInfo + { + Key = new PrivateKey(), + Balance = balance, + Cash = GetRandomCash(_random, balance), + }; + }); + } + + public FungibleAssetValue TotalReward { get; } + + public ValidatorInfo[] ValidatorsInfos { get; } + + public DelegatorInfo[] Delegatorinfos { get; } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ConstantTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ConstantTest.cs new file mode 100644 index 0000000000..f03a1a7b93 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/ConstantTest.cs @@ -0,0 +1,16 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class ConstantTest +{ + [Fact(Skip = "Allow after positive unbonding period")] + public void StaticPropertyTest() + { + Assert.True(ValidatorDelegatee.ValidatorUnbondingPeriod > 0); + Assert.True(ValidatorDelegatee.MaxCommissionPercentage < int.MaxValue); + Assert.True(ValidatorDelegatee.MaxCommissionPercentage >= 0); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/DelegateValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/DelegateValidatorTest.cs new file mode 100644 index 0000000000..246c1df300 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/DelegateValidatorTest.cs @@ -0,0 +1,291 @@ +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Collections.Generic; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class DelegateValidatorTest : ValidatorDelegationTestBase +{ + private interface IDelegateValidatorFixture + { + ValidatorInfo ValidatorInfo { get; } + } + + public static IEnumerable RandomSeeds => new List + { + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + }; + + [Fact] + public void Serialization() + { + var gold = DelegationCurrency * 10; + var action = new DelegateValidator(gold); + var plainValue = action.PlainValue; + + var deserialized = new DelegateValidator(); + deserialized.LoadPlainValue(plainValue); + Assert.Equal(gold, deserialized.FAV); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var validatorGold = DelegationCurrency * 10; + var delegatorGold = DelegationCurrency * 20; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 100, height++); + + // When + var delegateValidator = new DelegateValidator(delegatorGold); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + world = delegateValidator.Execute(actionContext); + + // Then + var repository = new ValidatorRepository(world, actionContext); + var validator = repository.GetValidatorDelegatee(validatorKey.Address); + var bond = repository.GetBond(validator, validatorKey.Address); + var validatorList = repository.GetValidatorList(); + + Assert.Equal(validatorGold.RawValue + delegatorGold.RawValue, bond.Share); + Assert.Equal(validatorGold.RawValue + delegatorGold.RawValue, validator.Validator.Power); + Assert.Equal(validator.Validator, Assert.Single(validatorList.Validators)); + Assert.Equal(DelegationCurrency * 80, GetBalance(world, validatorKey.Address)); + } + + [Fact] + public void Execute_Fact() + { + var fixture = new StaticFixture + { + ValidatorInfo = new ValidatorInfo + { + Key = new PrivateKey(), + Cash = DelegationCurrency * 10, + Balance = DelegationCurrency * 100, + }, + }; + ExecuteWithFixture(fixture); + } + + [Theory] + [InlineData(0)] + [InlineData(1181126949)] + [InlineData(793705868)] + [InlineData(559431555)] + public void Execute_Fact_WithStaticSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Theory] + [MemberData(nameof(RandomSeeds))] + public void Execute_Fact_WithRandomSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Fact] + public void Execute_WithInvalidCurrency_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var validatorGold = DelegationCurrency * 10; + var delegatorDollar = Dollar * 20; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureToMintAsset(world, validatorKey, delegatorDollar, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + var delegateValidator = new DelegateValidator(delegatorDollar); + + // Then + Assert.Throws(() => delegateValidator.Execute(actionContext)); + } + + [Fact] + public void Execute_WithInsufficientBalance_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var validatorGold = DelegationCurrency * 10; + var delegatorGold = DelegationCurrency * 10; + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureToMintAsset(world, validatorKey, delegatorGold, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + var delegateValidator = new DelegateValidator(DelegationCurrency * 11); + + // Then + Assert.Throws(() => delegateValidator.Execute(actionContext)); + } + + [Fact] + public void Execute_ToInvalidValidator_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 100, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + }; + var delegateValidator = new DelegateValidator(DelegationCurrency * 10); + + // Then + Assert.Throws(() => delegateValidator.Execute(actionContext)); + } + + [Fact] + public void Execute_ToTombstonedValidator_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var validatorGold = DelegationCurrency * 10; + var delegatorGold = DelegationCurrency * 10; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureTombstonedValidator(world, validatorKey, height++); + world = EnsureToMintAsset(world, validatorKey, delegatorGold, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + }; + var delegateValidator = new DelegateValidator(delegatorGold); + + // Then + Assert.Throws(() => delegateValidator.Execute(actionContext)); + } + + private void ExecuteWithFixture(IDelegateValidatorFixture fixture) + { + // Given + var world = World; + var validatorKey = fixture.ValidatorInfo.Key; + var height = 1L; + var validatorCashToDelegate = fixture.ValidatorInfo.CashToDelegate; + var validatorCash = fixture.ValidatorInfo.Cash; + var validatorBalance = fixture.ValidatorInfo.Balance; + var actionContext = new ActionContext { }; + world = EnsureToMintAsset(world, validatorKey, validatorBalance, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorCash, height++); + + // When + var expectedRepository = new ValidatorRepository(world, new ActionContext()); + var expectedValidator = expectedRepository.GetValidatorDelegatee(validatorKey.Address); + var expectedValidatorBalance = validatorBalance - validatorCash - validatorCashToDelegate; + var expectedPower = validatorCash.RawValue + validatorCashToDelegate.RawValue; + + var delegateValidator = new DelegateValidator(validatorCashToDelegate); + actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + world = delegateValidator.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualValidator = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualValidatorBalance = GetBalance(world, validatorKey.Address); + var actualPower = actualValidator.Power; + var actualBond = actualRepository.GetBond(actualValidator, validatorKey.Address); + + Assert.Equal(expectedPower, actualBond.Share); + Assert.Equal(expectedValidatorBalance, actualValidatorBalance); + Assert.Equal(expectedPower, actualPower); + } + + private struct ValidatorInfo + { + public ValidatorInfo() + { + } + + public ValidatorInfo(Random random) + { + Balance = GetRandomFAV(DelegationCurrency, random); + Cash = GetRandomCash(random, Balance, minDivisor: 2); + CashToDelegate = GetRandomCash(random, Balance - Cash); + } + + public PrivateKey Key { get; set; } = new PrivateKey(); + + public FungibleAssetValue CashToDelegate { get; set; } = DelegationCurrency * 10; + + public FungibleAssetValue Cash { get; set; } = DelegationCurrency * 10; + + public FungibleAssetValue Balance { get; set; } = DelegationCurrency * 100; + } + + private struct StaticFixture : IDelegateValidatorFixture + { + public DelegateValidator DelegateValidator { get; set; } + + public ValidatorInfo ValidatorInfo { get; set; } + } + + private class RandomFixture : IDelegateValidatorFixture + { + private readonly Random _random; + + public RandomFixture(int randomSeed) + { + _random = new Random(randomSeed); + ValidatorInfo = new ValidatorInfo(_random); + } + + public ValidatorInfo ValidatorInfo { get; } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/GasTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/GasTest.cs new file mode 100644 index 0000000000..306f56640d --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/GasTest.cs @@ -0,0 +1,181 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Linq; +using Libplanet.Action; +using Libplanet.Crypto; +using Nekoyume.Action; +using Xunit; + +public class GasTest : TxAcitonTestBase +{ + [Theory] + [InlineData(0, 0, 4)] + [InlineData(1, 1, 4)] + [InlineData(4, 4, 4)] + public void Execute(long gasLimit, long gasConsumption, long gasOwned) + { + if (gasLimit < gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be greater than or equal to {nameof(gasConsumption)}."); + } + + if (gasOwned < gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be greater than or equal to {nameof(gasConsumption)}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + EnsureToMintAsset(signerKey, signerMead); + + // When + var expectedMead = signerMead - Mead * gasConsumption; + var gasActions = new IAction[] + { + new GasAction { Consumption = gasConsumption }, + }; + + MakeTransaction( + signerKey, + gasActions, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + MoveToNextBlock(throwOnError: true); + + // Then + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(0, 4)] + [InlineData(1, 4)] + [InlineData(4, 4)] + public void Execute_Without_GasLimit_And_MaxGasPrice( + long gasConsumption, long gasOwned) + { + if (gasOwned < gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be greater than or equal to {nameof(gasConsumption)}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + EnsureToMintAsset(signerKey, signerMead); + + // When + var expectedMead = signerMead; + var gasActions = new IAction[] + { + new GasAction { Consumption = gasConsumption }, + }; + + MakeTransaction(signerKey, gasActions); + MoveToNextBlock(throwOnError: true); + + // Then + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(1, 1, 0)] + [InlineData(4, 4, 0)] + [InlineData(4, 4, 1)] + public void Execute_InsufficientMead_Throw( + long gasLimit, long gasConsumption, long gasOwned) + { + if (gasLimit < gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be greater than or equal to {nameof(gasConsumption)}."); + } + + if (gasOwned >= gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be less than {nameof(gasConsumption)}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + EnsureToMintAsset(signerKey, signerMead); + + // When + var expectedMead = Mead * 0; + var gasAction = new GasAction { Consumption = gasConsumption }; + MakeTransaction( + signerKey, + new ActionBase[] { gasAction, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + + // Then + var e = Assert.Throws(() => MoveToNextBlock(throwOnError: true)); + var innerExceptions = e.InnerExceptions + .Cast() + .ToArray(); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Single(innerExceptions); + Assert.IsType(innerExceptions[0].Action); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(4, 5, 5)] + [InlineData(1, 5, 10)] + public void Execute_ExcceedGasLimit_Throw( + long gasLimit, long gasConsumption, long gasOwned) + { + if (gasLimit >= gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be less than {nameof(gasConsumption)}."); + } + + if (gasOwned < gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be greater than or equal to {nameof(gasConsumption)}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + EnsureToMintAsset(signerKey, signerMead); + + // When + var expectedMead = signerMead - (Mead * gasLimit); + var gasAction = new GasAction { Consumption = gasConsumption }; + MakeTransaction( + signerKey, + new ActionBase[] { gasAction, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + + // Then + var e = Assert.Throws(() => MoveToNextBlock(throwOnError: true)); + var innerExceptions = e.InnerExceptions + .Cast() + .ToArray(); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Single(innerExceptions); + Assert.Contains(innerExceptions, i => i.Action is GasAction); + Assert.Equal(expectedMead, actualMead); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/GasWithTransferAssetTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/GasWithTransferAssetTest.cs new file mode 100644 index 0000000000..1815411ccd --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/GasWithTransferAssetTest.cs @@ -0,0 +1,205 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Linq; +using Libplanet.Action; +using Libplanet.Crypto; +using Nekoyume.Action; +using Xunit; + +public class GasWithTransferAssetTest : TxAcitonTestBase +{ + public const long GasConsumption = 4; + + [Theory] + [InlineData(4, 4)] + [InlineData(4, 5)] + [InlineData(4, 6)] + public void Execute(long gasLimit, long gasOwned) + { + if (gasLimit < GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be greater than or equal to {GasConsumption}."); + } + + if (gasOwned < GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be greater than or equal to {GasConsumption}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + var recipientKey = new PrivateKey(); + EnsureToMintAsset(signerKey, signerMead); + EnsureToMintAsset(signerKey, NCG * 100); + + // When + var amount = NCG * 1; + var expectedNCG = NCG * 1; + var expectedMead = signerMead - Mead * GasConsumption; + var transferAsset = new TransferAsset( + signerKey.Address, recipientKey.Address, amount, memo: "test"); + MakeTransaction( + signerKey, + new ActionBase[] { transferAsset, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + + // ` + MoveToNextBlock(throwOnError: true); + var actualNCG = GetBalance(recipientKey.Address, NCG); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Equal(expectedNCG, actualNCG); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(0, 4)] + [InlineData(1, 4)] + [InlineData(4, 4)] + public void Execute_Without_GasLimit_And_MaxGasPrice( + long gasConsumption, long gasOwned) + { + if (gasOwned < gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be greater than or equal to {nameof(gasConsumption)}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + var recipientKey = new PrivateKey(); + EnsureToMintAsset(signerKey, signerMead); + EnsureToMintAsset(signerKey, NCG * 100); + + // When + var amount = NCG * 1; + var expectedNCG = NCG * 1; + var expectedMead = signerMead; + var transferAsset = new TransferAsset( + signerKey.Address, recipientKey.Address, amount, memo: "test"); + MakeTransaction( + signerKey, + new ActionBase[] { transferAsset, }); + + // Then + MoveToNextBlock(throwOnError: true); + var actualNCG = GetBalance(recipientKey.Address, NCG); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Equal(expectedNCG, actualNCG); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(4, 0)] + [InlineData(5, 0)] + [InlineData(6, 1)] + public void Execute_InsufficientMead_Throw( + long gasLimit, long gasOwned) + { + if (gasLimit < GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be greater than or equal to {GasConsumption}."); + } + + if (gasOwned >= GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be less than {GasConsumption}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + var recipientKey = new PrivateKey(); + EnsureToMintAsset(signerKey, signerMead); + EnsureToMintAsset(signerKey, NCG * 100); + + // When + var amount = NCG * 1; + var expectedMead = Mead * 0; + var expectedNCG = NCG * 0; + var transferAsset = new TransferAsset( + signerKey.Address, recipientKey.Address, amount, memo: "test"); + MakeTransaction( + signerKey, + new ActionBase[] { transferAsset, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + + // Then + var e = Assert.Throws(() => MoveToNextBlock(throwOnError: true)); + var innerExceptions = e.InnerExceptions + .Cast() + .ToArray(); + var actualNCG = GetBalance(recipientKey.Address, NCG); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Single(innerExceptions); + Assert.Contains(innerExceptions, i => i.Action is TransferAsset); + Assert.Equal(expectedNCG, actualNCG); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(3, 5)] + [InlineData(1, 10)] + public void Execute_ExcceedGasLimit_Throw( + long gasLimit, long gasOwned) + { + if (gasLimit >= GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be less than {GasConsumption}."); + } + + if (gasOwned < GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be greater than or equal to {GasConsumption}."); + } + + // Given + var amount = NCG * 1; + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + var recipientKey = new PrivateKey(); + EnsureToMintAsset(signerKey, signerMead); + EnsureToMintAsset(signerKey, NCG * 100); + + // When + var expectedMead = signerMead - (Mead * gasLimit); + var expectedNCG = NCG * 0; + var transferAsset = new TransferAsset( + signerKey.Address, recipientKey.Address, amount, memo: "test"); + MakeTransaction( + signerKey, + new ActionBase[] { transferAsset, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + + // Then + var e = Assert.Throws(() => MoveToNextBlock(throwOnError: true)); + var innerExceptions = e.InnerExceptions + .Cast() + .ToArray(); + var actualNCG = GetBalance(recipientKey.Address, NCG); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Single(innerExceptions); + Assert.Contains(innerExceptions, i => i.Action is TransferAsset); + Assert.Equal(expectedNCG, actualNCG); + Assert.Equal(expectedMead, actualMead); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/PromoteValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/PromoteValidatorTest.cs new file mode 100644 index 0000000000..9c99515b23 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/PromoteValidatorTest.cs @@ -0,0 +1,156 @@ +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class PromoteValidatorTest : ValidatorDelegationTestBase +{ + [Fact] + public void Serialization() + { + var publicKey = new PrivateKey().PublicKey; + var gold = DelegationCurrency * 10; + var action = new PromoteValidator(publicKey, gold); + var plainValue = action.PlainValue; + + var deserialized = new PromoteValidator(); + deserialized.LoadPlainValue(plainValue); + Assert.Equal(publicKey, deserialized.PublicKey); + Assert.Equal(gold, deserialized.FAV); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var gold = DelegationCurrency * 10; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 100, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + var promoteValidator = new PromoteValidator(validatorKey.PublicKey, gold); + world = promoteValidator.ExecutePublic(actionContext); + + // Then + var repository = new ValidatorRepository(world, actionContext); + var validator = repository.GetValidatorDelegatee(validatorKey.Address); + var bond = repository.GetBond(validator, validatorKey.Address); + var validatorList = repository.GetValidatorList(); + + Assert.Equal(validatorKey.Address, Assert.Single(validator.Delegators)); + Assert.Equal(gold.RawValue, bond.Share); + Assert.Equal(validator.Validator, Assert.Single(validatorList.Validators)); + Assert.Equal(validator.Validator, Assert.Single(validatorList.ActiveSet())); + Assert.Equal(DelegationCurrency * 90, GetBalance(world, validatorKey.Address)); + Assert.Empty(validatorList.InActiveSet()); + } + + [Fact] + public void Execute_ToInvalidValidator_Throw() + { + // Given + var world = World; + var context = new ActionContext { }; + var validatorKey = new PrivateKey(); + var height = 1L; + var gold = DelegationCurrency * 10; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 100, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + var promoteValidator = new PromoteValidator(new PrivateKey().PublicKey, gold); + + // Then + Assert.Throws( + () => promoteValidator.ExecutePublic(actionContext)); + } + + [Fact] + public void Execute_WithInvalidCurrency_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var gold = Dollar * 10; + world = EnsureToMintAsset(world, validatorKey, Dollar * 100, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + var promoteValidator = new PromoteValidator(validatorKey.PublicKey, gold); + + // Then + Assert.Throws( + () => promoteValidator.ExecutePublic(actionContext)); + } + + [Fact] + public void Execute_WithInsufficientBalance_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey().PublicKey; + var height = 1L; + var gold = DelegationCurrency * 10; + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + var promoteValidator = new PromoteValidator(validatorKey, gold); + + // Then + Assert.Throws( + () => promoteValidator.ExecutePublic(actionContext)); + } + + [Fact] + public void Execute_PromotedValidator_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var validatorGold = DelegationCurrency * 10; + world = EnsureToMintAsset(world, validatorKey, validatorGold * 2, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + + // When + var promoteValidator = new PromoteValidator(validatorKey.PublicKey, validatorGold); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height++, + Signer = validatorKey.Address, + }; + + // Then + Assert.Throws( + () => promoteValidator.ExecutePublic(actionContext)); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/RecordProposerTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/RecordProposerTest.cs new file mode 100644 index 0000000000..5f0e6b7697 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/RecordProposerTest.cs @@ -0,0 +1,48 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using Libplanet.Crypto; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.Module.ValidatorDelegation; +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class RecordProposerTest : ValidatorDelegationTestBase +{ + [Fact] + public void Serialization() + { + var action = new RecordProposer(); + var plainValue = action.PlainValue; + + var deserialized = new RecordProposer(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var minerKey = new PrivateKey(); + var blockIndex = (long)Random.Shared.Next(1, 100); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockIndex++, + Miner = minerKey.Address, + }; + var recordProposer = new RecordProposer(); + world = recordProposer.Execute(actionContext); + + // Then + var repository = new ValidatorRepository(world, actionContext); + var proposerInfo = repository.GetProposerInfo(); + + Assert.Equal(blockIndex - 1, proposerInfo.BlockIndex); + Assert.Equal(minerKey.Address, proposerInfo.Proposer); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ReleaseValidatorUnbondingsTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ReleaseValidatorUnbondingsTest.cs new file mode 100644 index 0000000000..79b1840e4f --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/ReleaseValidatorUnbondingsTest.cs @@ -0,0 +1,105 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System.Numerics; +using Libplanet.Crypto; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.Model.Guild; +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class ReleaseValidatorUnbondingsTest : ValidatorDelegationTestBase +{ + [Fact] + public void Serialization() + { + var action = new ReleaseValidatorUnbondings(); + var plainValue = action.PlainValue; + + var deserialized = new ReleaseValidatorUnbondings(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var validatorGold = DelegationCurrency * 50; + var validatorBalance = DelegationCurrency * 100; + var share = new BigInteger(10); + var height = 1L; + var actionContext = new ActionContext { }; + + world = EnsureToMintAsset(world, validatorKey, validatorBalance, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureUnbondingValidator(world, validatorKey.Address, share, height); + + // When + var expectedRepository = new GuildRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetGuildDelegatee(validatorKey.Address); + var expectedUnbondingSet = expectedRepository.GetUnbondingSet(); + var expectedReleaseCount = expectedUnbondingSet.UnbondingRefs.Count; + var expectedDepositGold = expectedDelegatee.FAVFromShare(share); + var expectedBalance = GetBalance(world, validatorKey.Address) + expectedDepositGold; + + var releaseValidatorUnbondings = new ReleaseValidatorUnbondings(validatorKey.Address); + actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height + ValidatorDelegatee.ValidatorUnbondingPeriod, + }; + world = releaseValidatorUnbondings.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualBalance = GetBalance(world, validatorKey.Address); + var actualUnbondingSet = actualRepository.GetUnbondingSet(); + var actualReleaseCount = actualUnbondingSet.UnbondingRefs.Count; + + Assert.Equal(expectedBalance, actualBalance); + Assert.NotEqual(expectedUnbondingSet.IsEmpty, actualUnbondingSet.IsEmpty); + Assert.True(actualUnbondingSet.IsEmpty); + Assert.Equal(expectedReleaseCount - 1, actualReleaseCount); + } + + [Fact(Skip = "Skip due to zero unbonding period before migration")] + public void Execute_ThereIsNoUnbonding_AtEarlyHeight() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var actionContext = new ActionContext { }; + var share = new BigInteger(10); + + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 100, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 50, height++); + world = EnsureUnbondingValidator(world, validatorKey.Address, share, height); + + // When + var expectedRepository = new GuildRepository(world, actionContext); + var expectedUnbondingSet = expectedRepository.GetUnbondingSet(); + var expectedReleaseCount = expectedUnbondingSet.UnbondingRefs.Count; + + var releaseValidatorUnbondings = new ReleaseValidatorUnbondings(validatorKey.Address); + actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height, + }; + world = releaseValidatorUnbondings.Execute(actionContext); + + // Then + var actualRepository = new GuildRepository(world, actionContext); + var actualUnbondingSet = actualRepository.GetUnbondingSet(); + var actualReleaseCount = actualUnbondingSet.UnbondingRefs.Count; + + Assert.Equal(expectedUnbondingSet.IsEmpty, actualUnbondingSet.IsEmpty); + Assert.False(actualUnbondingSet.IsEmpty); + Assert.Equal(expectedReleaseCount, actualReleaseCount); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/SetValidatorCommissionTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/SetValidatorCommissionTest.cs new file mode 100644 index 0000000000..6886fa952c --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/SetValidatorCommissionTest.cs @@ -0,0 +1,250 @@ +namespace Lib9c.Tests.Action.ValidatorDelegation +{ + using System; + using System.Collections.Generic; + using System.Numerics; + using Libplanet.Crypto; + using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.ValidatorDelegation; + using Xunit; + + public class SetValidatorCommissionTest : ValidatorDelegationTestBase + { + /// + /// Tested that ValidatorDelegatee.MaxCommissionPercentage is less than int.MaxValue + /// in ConstantTest.cs file. + /// + private static readonly int MaxCommissionPercentage + = (int)ValidatorDelegatee.MaxCommissionPercentage; + + private static readonly long CommissionPercentageChangeCooldown + = ValidatorDelegatee.CommissionPercentageUpdateCooldown; + + public static IEnumerable RandomCommisionPercentage => new List + { + new object[] { Random.Shared.Next(MaxCommissionPercentage) }, + new object[] { Random.Shared.Next(MaxCommissionPercentage) }, + new object[] { Random.Shared.Next(MaxCommissionPercentage) }, + }; + + public static IEnumerable RandomInvalidCommisionPercentage => new List + { + new object[] { Random.Shared.Next(MaxCommissionPercentage, int.MaxValue) }, + new object[] { Random.Shared.Next(MaxCommissionPercentage, int.MaxValue) }, + new object[] { Random.Shared.Next(MaxCommissionPercentage, int.MaxValue) }, + }; + + public static IEnumerable InvalidCommisionPercentageCooldown => new List + { + new object[] { 0 }, + new object[] { CommissionPercentageChangeCooldown - 1 }, + }; + + public static IEnumerable ValidCommisionPercentagePeriod => new List + { + new object[] { CommissionPercentageChangeCooldown }, + new object[] { CommissionPercentageChangeCooldown + 1 }, + }; + + [Fact] + public void Serialization() + { + var address = new PrivateKey().Address; + BigInteger commissionPercentage = 10; + var action = new SetValidatorCommission(address, commissionPercentage); + var plainValue = action.PlainValue; + + var deserialized = new SetValidatorCommission(); + deserialized.LoadPlainValue(plainValue); + Assert.Equal(address, deserialized.ValidatorDelegatee); + Assert.Equal(commissionPercentage, deserialized.CommissionPercentage); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var validatorGold = DelegationCurrency * 10; + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height); + + // When + var setValidatorCommission = new SetValidatorCommission( + validatorKey.Address, commissionPercentage: 11); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height + CommissionPercentageChangeCooldown, + }; + world = setValidatorCommission.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualPercentage = actualDelegatee.CommissionPercentage; + + Assert.Equal(11, actualPercentage); + } + + [Theory] + [InlineData(9, 10)] + [InlineData(9, 8)] + [InlineData(0, 1)] + [InlineData(20, 19)] + public void Execute_Theory(int oldCommissionPercentage, int newCommissionPercentage) + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var validatorGold = DelegationCurrency * 10; + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureCommissionChangedValidator( + world, validatorKey, oldCommissionPercentage, ref height); + + // When + var setValidatorCommission = new SetValidatorCommission( + validatorKey.Address, + newCommissionPercentage); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height + CommissionPercentageChangeCooldown, + }; + world = setValidatorCommission.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualPercentage = actualDelegatee.CommissionPercentage; + + Assert.Equal(newCommissionPercentage, actualPercentage); + } + + [Theory] + [MemberData(nameof(RandomInvalidCommisionPercentage))] + public void Execute_Theory_WithValueGreaterThanMaximum_Throw(int commissionPercentage) + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var validatorGold = DelegationCurrency * 10; + var height = 1L; + + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height + CommissionPercentageChangeCooldown, + }; + var setValidatorCommission = new SetValidatorCommission( + validatorKey.Address, + commissionPercentage); + + // Then + Assert.Throws( + () => setValidatorCommission.Execute(actionContext)); + } + + [Theory] + [InlineData(-1)] + [InlineData(-2)] + public void Execute_Theory_WithNegative_Throw(int commissionPercentage) + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var validatorGold = DelegationCurrency * 10; + var height = 1L; + + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height + CommissionPercentageChangeCooldown, + }; + var setValidatorCommission = new SetValidatorCommission( + validatorKey.Address, + commissionPercentage); + + // Then + Assert.Throws( + () => setValidatorCommission.Execute(actionContext)); + } + + [Theory] + [MemberData(nameof(InvalidCommisionPercentageCooldown))] + public void Execute_Theory_WithInvalidValue_Throw(int cooldown) + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var validatorGold = DelegationCurrency * 10; + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureCommissionChangedValidator(world, validatorKey, 15, ref height); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height + cooldown, + }; + var setValidatorCommission = new SetValidatorCommission( + validatorKey.Address, commissionPercentage: 14); + + // Then + Assert.Throws( + () => setValidatorCommission.Execute(actionContext)); + } + + [Theory] + [MemberData(nameof(ValidCommisionPercentagePeriod))] + public void Execute_Theory_WitValue(int period) + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var validatorGold = DelegationCurrency * 10; + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureCommissionChangedValidator(world, validatorKey, 11, ref height); + + // When + var expectedCommission = 12; + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height + period, + }; + var setValidatorCommission = new SetValidatorCommission( + validatorKey.Address, commissionPercentage: expectedCommission); + world = setValidatorCommission.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualPercentage = actualDelegatee.CommissionPercentage; + + Assert.Equal(expectedCommission, actualPercentage); + } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs new file mode 100644 index 0000000000..3173039718 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs @@ -0,0 +1,213 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; +using Libplanet.Types.Evidence; +using Nekoyume.Action; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class SlashValidatorTest : ValidatorDelegationTestBase +{ + [Fact] + public void Serialization() + { + var action = new SlashValidator(); + var plainValue = action.PlainValue; + + var deserialized = new SlashValidator(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + // Given + const int length = 10; + var world = World; + var validatorKey = new PrivateKey(); + var validatorGold = DelegationCurrency * 10; + var delegatorKeys = CreateArray(length, _ => new PrivateKey()); + var delegatorGolds = CreateArray(length, i => DelegationCurrency * Random.Shared.Next(10, 100)); + var height = 1L; + var actionContext = new ActionContext { }; + int seed = 0; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsureToMintAssets(world, delegatorKeys, delegatorGolds, height++); + world = delegatorKeys.Aggregate(world, (w, d) => EnsureMakeGuild( + w, d.Address, validatorKey.Address, height++, seed++)); + + // When + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee(validatorKey.Address); + var expectedValidatorShare = expectedRepository.GetBond( + expectedDelegatee, validatorKey.Address).Share; + + var validatorSet = new ValidatorSet(new List + { + new (validatorKey.PublicKey, new BigInteger(1000)), + }); + var vote1 = new VoteMetadata( + height: height - 1, + round: 0, + blockHash: new BlockHash(CreateArray(BlockHash.Size, _ => (byte)0x01)), + timestamp: DateTimeOffset.UtcNow, + validatorPublicKey: validatorKey.PublicKey, + validatorPower: BigInteger.One, + flag: VoteFlag.PreCommit).Sign(validatorKey); + var vote2 = new VoteMetadata( + height: height - 1, + round: 0, + blockHash: new BlockHash(CreateArray(BlockHash.Size, _ => (byte)0x02)), + timestamp: DateTimeOffset.UtcNow, + validatorPublicKey: validatorKey.PublicKey, + validatorPower: BigInteger.One, + flag: VoteFlag.PreCommit).Sign(validatorKey); + var evidence = new DuplicateVoteEvidence( + vote1, + vote2, + validatorSet, + vote1.Timestamp); + var lastCommit = new BlockCommit( + height: height - 1, + round: 0, + blockHash: EmptyBlockHash, + ImmutableArray.Create(vote1)); + var slashValidator = new SlashValidator(); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height++, + Evidence = new List { evidence }, + LastCommit = lastCommit, + }; + world = slashValidator.Execute(actionContext); + + // Then + var balance = GetBalance(world, validatorKey.Address); + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualValidatorShare = actualRepository.GetBond(actualDelegatee, validatorKey.Address).Share; + + Assert.True(actualDelegatee.Jailed); + Assert.Equal(long.MaxValue, actualDelegatee.JailedUntil); + Assert.True(actualDelegatee.Tombstoned); + } + + [Fact] + public void Execute_ToNotPromotedValidator_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + + // When + var evidence = CreateDuplicateVoteEvidence(validatorKey, height - 1); + var lastCommit = new BlockCommit( + height: height - 1, + round: 0, + blockHash: EmptyBlockHash, + ImmutableArray.Create(evidence.VoteRef)); + var slashValidator = new SlashValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height++, + Evidence = new List { evidence }, + LastCommit = lastCommit, + }; + + // Then + Assert.Throws(() => slashValidator.Execute(actionContext)); + } + + [Fact] + public void Execute_ByAbstain() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var actionContext = new ActionContext { }; + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + + // When + for (var i = 0L; i <= AbstainHistory.MaxAbstainAllowance; i++) + { + var vote = CreateNullVote(validatorKey, height - 1); + var lastCommit = new BlockCommit( + height: height - 1, + round: 0, + blockHash: vote.BlockHash, + ImmutableArray.Create(vote)); + var slashValidator = new SlashValidator(); + actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + LastCommit = lastCommit, + }; + world = slashValidator.Execute(actionContext); + } + + // Then + var repository = new ValidatorRepository(world, actionContext); + var delegatee = repository.GetValidatorDelegatee(validatorKey.Address); + Assert.True(delegatee.Jailed); + Assert.False(delegatee.Tombstoned); + } + + [Fact] + public void Execute_ToJailedValidator_ThenNothingHappens() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var actionContext = new ActionContext(); + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsureTombstonedValidator(world, validatorKey, height++); + + // When + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee(validatorKey.Address); + var expectedJailed = expectedDelegatee.Jailed; + var evidence = CreateDuplicateVoteEvidence(validatorKey, height - 1); + var lastCommit = new BlockCommit( + height: height - 1, + round: 0, + blockHash: EmptyBlockHash, + ImmutableArray.Create(evidence.VoteRef)); + var slashValidator = new SlashValidator(); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height + SlashValidator.AbstainJailTime, + Signer = validatorKey.Address, + Evidence = new List { evidence }, + LastCommit = lastCommit, + }; + world = slashValidator.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualJailed = actualDelegatee.Jailed; + + Assert.Equal(expectedJailed, actualJailed); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/TxAcitonTestBase.cs b/.Lib9c.Tests/Action/ValidatorDelegation/TxAcitonTestBase.cs new file mode 100644 index 0000000000..04e63903b5 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/TxAcitonTestBase.cs @@ -0,0 +1,275 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Action.State; +using Libplanet.Blockchain; +using Libplanet.Blockchain.Policies; +using Libplanet.Blockchain.Renderers; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; +using Nekoyume; +using Nekoyume.Action; +using Nekoyume.Action.Loader; +using Nekoyume.Blockchain.Policy; +using Nekoyume.Model; +using Nekoyume.Model.State; + +public abstract class TxAcitonTestBase +{ + protected static readonly Currency Mead = Currencies.Mead; + protected static readonly Currency NCG = Currency.Legacy("NCG", 2, null); + private readonly PrivateKey _privateKey = new PrivateKey(); + private BlockCommit? _lastCommit; + + protected TxAcitonTestBase() + { + var validatorKey = new PrivateKey(); + + var blockPolicySource = new BlockPolicySource( + actionLoader: new GasActionLoader()); + var policy = blockPolicySource.GetPolicy( + maxTransactionsBytesPolicy: null!, + minTransactionsPerBlockPolicy: null!, + maxTransactionsPerBlockPolicy: null!, + maxTransactionsPerSignerPerBlockPolicy: null!); + var stagePolicy = new VolatileStagePolicy(); + var validator = new Validator(validatorKey.PublicKey, 10_000_000_000_000_000_000); + var genesis = MakeGenesisBlock( + new ValidatorSet(new List { validator })); + using var store = new MemoryStore(); + using var keyValueStore = new MemoryKeyValueStore(); + using var stateStore = new TrieStateStore(keyValueStore); + var actionEvaluator = new ActionEvaluator( + policy.PolicyActionsRegistry, + stateStore: stateStore, + actionTypeLoader: new GasActionLoader()); + var actionRenderer = new ActionRenderer(); + + var blockChain = BlockChain.Create( + policy, + stagePolicy, + store, + stateStore, + genesis, + actionEvaluator, + renderers: new[] { actionRenderer }); + + BlockChain = blockChain; + Renderer = actionRenderer; + ValidatorKey = validatorKey; + } + + protected BlockChain BlockChain { get; } + + protected ActionRenderer Renderer { get; } + + protected PrivateKey ValidatorKey { get; } + + protected void EnsureToMintAsset(PrivateKey privateKey, FungibleAssetValue fav) + { + var prepareRewardAssets = new PrepareRewardAssets + { + RewardPoolAddress = privateKey.Address, + Assets = new List + { + fav, + }, + }; + var actions = new ActionBase[] { prepareRewardAssets, }; + + Renderer.Reset(); + MakeTransaction(privateKey, actions); + MoveToNextBlock(); + Renderer.Wait(); + } + + protected void MoveToNextBlock(bool throwOnError = false) + { + var blockChain = BlockChain; + var lastCommit = _lastCommit; + var validatorKey = ValidatorKey; + var block = blockChain.ProposeBlock(validatorKey, lastCommit); + var worldState = blockChain.GetNextWorldState() + ?? throw new InvalidOperationException("Failed to get next world state"); + var validatorSet = worldState.GetValidatorSet(); + var blockCommit = GenerateBlockCommit( + block, validatorSet, new PrivateKey[] { validatorKey }); + + Renderer.Reset(); + blockChain.Append(block, blockCommit); + Renderer.Wait(); + if (throwOnError && Renderer.Exceptions.Any()) + { + throw new AggregateException(Renderer.Exceptions); + } + + _lastCommit = blockCommit; + } + + protected IWorldState GetNextWorldState() + { + var blockChain = BlockChain; + return blockChain.GetNextWorldState() + ?? throw new InvalidOperationException("Failed to get next world state"); + } + + protected void MakeTransaction( + PrivateKey privateKey, + IEnumerable actions, + FungibleAssetValue? maxGasPrice = null, + long? gasLimit = null, + DateTimeOffset? timestamp = null) + { + var blockChain = BlockChain; + blockChain.MakeTransaction( + privateKey, actions, maxGasPrice, gasLimit, timestamp); + } + + protected FungibleAssetValue GetBalance(Address address, Currency currency) + => GetNextWorldState().GetBalance(address, currency); + + private BlockCommit GenerateBlockCommit( + Block block, ValidatorSet validatorSet, IEnumerable validatorPrivateKeys) + { + return block.Index != 0 + ? new BlockCommit( + block.Index, + 0, + block.Hash, + validatorPrivateKeys.Select(k => new VoteMetadata( + block.Index, + 0, + block.Hash, + DateTimeOffset.UtcNow, + k.PublicKey, + validatorSet.GetValidator(k.PublicKey).Power, + VoteFlag.PreCommit).Sign(k)).ToImmutableArray()) + : throw new InvalidOperationException("Block index must be greater than 0"); + } + + private Block MakeGenesisBlock(ValidatorSet validators) + { + var nonce = new byte[] { 0x00, 0x01, 0x02, 0x03 }; + (ActivationKey _, PendingActivationState pendingActivation) = + ActivationKey.Create(_privateKey, nonce); + var pendingActivations = new PendingActivationState[] { pendingActivation }; + + var sheets = TableSheetsImporter.ImportSheets(); + return BlockHelper.ProposeGenesisBlock( + validators, + sheets, + new GoldDistribution[0], + pendingActivations); + } + + protected sealed class ActionRenderer : IActionRenderer + { + private readonly ManualResetEvent _resetEvent = new ManualResetEvent(false); + private List _exceptionList = new List(); + + public Exception[] Exceptions => _exceptionList.ToArray(); + + public void RenderAction(IValue action, ICommittedActionContext context, HashDigest nextState) + { + } + + public void RenderActionError(IValue action, ICommittedActionContext context, Exception exception) + { + _exceptionList.Add(exception); + } + + public void RenderBlock(Block oldTip, Block newTip) + { + _exceptionList.Clear(); + } + + public void RenderBlockEnd(Block oldTip, Block newTip) + { + _resetEvent.Set(); + } + + public void Reset() => _resetEvent.Reset(); + + public void Wait(int timeout) + { + if (!_resetEvent.WaitOne(timeout)) + { + throw new TimeoutException("Timeout"); + } + } + + public void Wait() => Wait(10000); + } + + [ActionType(TypeIdentifier)] + protected class GasAction : ActionBase + { + public const string TypeIdentifier = "gas_action"; + + public GasAction() + { + } + + public long Consumption { get; set; } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("consumption", new Integer(Consumption)); + + public override void LoadPlainValue(IValue plainValue) + { + if (plainValue is not Dictionary root || + !root.TryGetValue((Text)"consumption", out var rawValues) || + rawValues is not Integer value) + { + throw new InvalidCastException(); + } + + Consumption = (long)value.Value; + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(Consumption); + return context.PreviousState; + } + } + + protected class GasActionLoader : IActionLoader + { + private readonly NCActionLoader _actionLoader; + + public GasActionLoader() + { + _actionLoader = new NCActionLoader(); + } + + public IAction LoadAction(long index, IValue value) + { + if (value is Dictionary pv && + pv.TryGetValue((Text)"type_id", out IValue rawTypeId) && + rawTypeId is Text typeId && typeId == GasAction.TypeIdentifier) + { + var action = new GasAction(); + action.LoadPlainValue(pv); + return action; + } + + return _actionLoader.LoadAction(index, value); + } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/UndelegateValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/UndelegateValidatorTest.cs new file mode 100644 index 0000000000..24dc21628d --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/UndelegateValidatorTest.cs @@ -0,0 +1,351 @@ +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Collections.Generic; +using System.Numerics; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class UndelegateValidatorTest : ValidatorDelegationTestBase +{ + private interface IUndelegateValidatorFixture + { + ValidatorInfo ValidatorInfo { get; } + } + + public static IEnumerable RandomSeeds => new List + { + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + new object[] { Random.Shared.Next() }, + }; + + [Fact] + public void Serialization() + { + var share = BigInteger.One; + var action = new UndelegateValidator(share); + var plainValue = action.PlainValue; + + var deserialized = new UndelegateValidator(); + deserialized.LoadPlainValue(plainValue); + Assert.Equal(share, deserialized.Share); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var actionContext = new ActionContext { }; + var validatorKey = new PrivateKey(); + var height = 1L; + var validatorGold = DelegationCurrency * 10; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 100, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + + // When + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee(validatorKey.Address); + var expectedBond = expectedRepository.GetBond(expectedDelegatee, validatorKey.Address); + var undelegateValidator = new UndelegateValidator(expectedBond.Share); + actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + + world = undelegateValidator.Execute(actionContext); + + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualValidatorList = actualRepository.GetValidatorList(); + var actualBond = actualRepository.GetBond(actualDelegatee, validatorKey.Address); + + Assert.NotEqual(expectedDelegatee.Delegators, actualDelegatee.Delegators); + Assert.NotEqual(expectedDelegatee.Validator.Power, actualDelegatee.Validator.Power); + Assert.Equal(BigInteger.Zero, actualDelegatee.Validator.Power); + Assert.Empty(actualValidatorList.Validators); + Assert.NotEqual(expectedBond.Share, actualBond.Share); + Assert.Equal(BigInteger.Zero, actualBond.Share); + } + + [Fact] + public void Execute_Theory() + { + var fixture = new StaticFixture + { + ValidatorInfo = new ValidatorInfo + { + Key = new PrivateKey(), + Cash = DelegationCurrency * 10, + Balance = DelegationCurrency * 100, + SubtractShare = 10, + }, + }; + ExecuteWithFixture(fixture); + } + + [Theory] + [InlineData(0)] + [InlineData(1181126949)] + [InlineData(793705868)] + [InlineData(17046502)] + public void Execute_Theory_WithStaticSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Theory] + [MemberData(nameof(RandomSeeds))] + public void Execute_Theory_WithRandomSeed(int randomSeed) + { + var fixture = new RandomFixture(randomSeed); + ExecuteWithFixture(fixture); + } + + [Fact(Skip ="Allow after Planetarium validator restriction")] + public void Execute_FromInvalidValidtor_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + var undelegateValidator = new UndelegateValidator(10); + + // Then + Assert.Throws( + () => undelegateValidator.Execute(actionContext)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void Execute_WithNotPositiveShare_Throw(long share) + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + var undelegateValidator = new UndelegateValidator(share); + + // Then + Assert.Throws( + () => undelegateValidator.Execute(actionContext)); + } + + [Fact] + public void Execute_FromJailedValidator() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsureJailedValidator(world, validatorKey, ref height); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height, + }; + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee(validatorKey.Address); + var expectedBond = expectedRepository.GetBond(expectedDelegatee, validatorKey.Address); + + var undelegateValidator = new UndelegateValidator(10); + world = undelegateValidator.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualBond = actualRepository.GetBond(actualDelegatee, validatorKey.Address); + + Assert.Equal(expectedBond.Share - 10, actualBond.Share); + } + + [Fact] + public void Execute_FromTombstonedValidator() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsureTombstonedValidator(world, validatorKey, height++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height, + }; + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee(validatorKey.Address); + var expectedBond = expectedRepository.GetBond(expectedDelegatee, validatorKey.Address); + + var undelegateValidator = new UndelegateValidator(10); + world = undelegateValidator.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualBond = actualRepository.GetBond(actualDelegatee, validatorKey.Address); + + Assert.Equal(expectedBond.Share - 10, actualBond.Share); + } + + [Fact] + public void Execute_JailsValidatorWhenUndelegationCausesLowDelegation() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var validatorCash = MinimumDelegation; + var validatorGold = MinimumDelegation; + var actionContext = new ActionContext { }; + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorCash, height++); + + // When + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee(validatorKey.Address); + var expectedJailed = expectedDelegatee.Jailed; + + var undelegateValidator = new UndelegateValidator(10); + actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height, + }; + world = undelegateValidator.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualJailed = actualDelegatee.Jailed; + var actualJailedUntil = actualDelegatee.JailedUntil; + + Assert.True(actualJailed); + Assert.NotEqual(expectedJailed, actualJailed); + } + + private void ExecuteWithFixture(IUndelegateValidatorFixture fixture) + { + // Given + var world = World; + var validatorKey = fixture.ValidatorInfo.Key; + var height = 1L; + var validatorCash = fixture.ValidatorInfo.Cash; + var validatorBalance = fixture.ValidatorInfo.Balance; + var actionContext = new ActionContext { }; + world = EnsureToMintAsset(world, validatorKey, validatorBalance, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorCash, height++); + + // When + var expectedRepository = new ValidatorRepository(world, new ActionContext()); + var expectedValidator = expectedRepository.GetValidatorDelegatee(validatorKey.Address); + var expectedValidatorBalance = validatorBalance - validatorCash; + var expectedValidatorPower = expectedValidator.Power - fixture.ValidatorInfo.SubtractShare; + + var subtractShare = fixture.ValidatorInfo.SubtractShare; + var undelegateValidator = new UndelegateValidator(subtractShare); + actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + }; + world = undelegateValidator.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualValidator = actualRepository.GetValidatorDelegatee(validatorKey.Address); + var actualValidatorBalance = GetBalance(world, validatorKey.Address); + var actualValiatorPower = actualValidator.Power; + + Assert.Equal(expectedValidatorBalance, actualValidatorBalance); + Assert.Equal(expectedValidatorPower, actualValiatorPower); + } + + private struct ValidatorInfo + { + public ValidatorInfo() + { + } + + public ValidatorInfo(Random random) + { + Balance = GetRandomFAV(DelegationCurrency, random); + Cash = GetRandomCash(random, Balance); + SubtractShare = GetRandomCash(random, Cash).RawValue; + if (SubtractShare == 0) + { + Console.WriteLine("123"); + } + } + + public PrivateKey Key { get; set; } = new PrivateKey(); + + public FungibleAssetValue Cash { get; set; } = DelegationCurrency * 10; + + public FungibleAssetValue Balance { get; set; } = DelegationCurrency * 100; + + public BigInteger SubtractShare { get; set; } = 100; + } + + private struct StaticFixture : IUndelegateValidatorFixture + { + public ValidatorInfo ValidatorInfo { get; set; } + } + + private class RandomFixture : IUndelegateValidatorFixture + { + private readonly Random _random; + + public RandomFixture(int randomSeed) + { + _random = new Random(randomSeed); + ValidatorInfo = new ValidatorInfo(_random); + } + + public ValidatorInfo ValidatorInfo { get; } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/UnjailValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/UnjailValidatorTest.cs new file mode 100644 index 0000000000..6bf61fd62e --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/UnjailValidatorTest.cs @@ -0,0 +1,172 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using Libplanet.Crypto; +using Nekoyume.Action; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class UnjailValidatorTest : ValidatorDelegationTestBase +{ + [Fact] + public void Serialization() + { + var action = new UnjailValidator(); + var plainValue = action.PlainValue; + + var deserialized = new UnjailValidator(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var validatorGold = DelegationCurrency * 100; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureJailedValidator(world, validatorKey, ref height); + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height + SlashValidator.AbstainJailTime, + Signer = validatorKey.Address, + }; + world = unjailValidator.Execute(actionContext); + + // Then + var repository = new ValidatorRepository(world, actionContext); + var delegatee = repository.GetValidatorDelegatee(validatorKey.Address); + Assert.False(delegatee.Jailed); + Assert.Equal(-1, delegatee.JailedUntil); + Assert.False(delegatee.Tombstoned); + } + + [Fact] + public void Execute_OnNotPromotedValidator_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height + SlashValidator.AbstainJailTime, + Signer = validatorKey.Address, + }; + + // Then + Assert.Throws(() => unjailValidator.Execute(actionContext)); + } + + [Fact] + public void Execute_OnNotJailedValidator_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height + SlashValidator.AbstainJailTime, + Signer = validatorKey.Address, + }; + + // Then + Assert.Throws(() => unjailValidator.Execute(actionContext)); + } + + [Fact] + public void Execute_TooEarly_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsureJailedValidator(world, validatorKey, ref height); + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height + SlashValidator.AbstainJailTime - 1, + Signer = validatorKey.Address, + }; + + // Then + Assert.Throws( + () => unjailValidator.Execute(actionContext)); + } + + [Fact] + public void Execute_OnTombstonedValidator_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsureTombstonedValidator(world, validatorKey, height++); + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height + SlashValidator.AbstainJailTime, + Signer = validatorKey.Address, + }; + + // Then + Assert.Throws( + () => unjailValidator.Execute(actionContext)); + } + + [Fact] + public void Execute_OnLowDelegatedValidator_Throw() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var validatorGold = MinimumDelegation; + world = EnsureToMintAsset(world, validatorKey, validatorGold, height++); + world = EnsurePromotedValidator(world, validatorKey, validatorGold, height++); + world = EnsureUnbondingValidator(world, validatorKey.Address, 10, height); + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = height++, + Signer = validatorKey.Address, + }; + + // Then + Assert.Throws( + () => unjailValidator.Execute(actionContext)); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/UpdateValidatorsTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/UpdateValidatorsTest.cs new file mode 100644 index 0000000000..54741e28f9 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/UpdateValidatorsTest.cs @@ -0,0 +1,100 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Linq; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.Model.State; +using Nekoyume.ValidatorDelegation; +using Xunit; + +public class UpdateValidatorsTest : ValidatorDelegationTestBase +{ + [Fact] + public void Serialization() + { + var action = new UpdateValidators(); + var plainValue = action.PlainValue; + + var deserialized = new UpdateValidators(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact(Skip ="Allow after Planetarium validator restriction")] + public void Execute() + { + // Given + const int length = 10; + var world = World; + var validatorKeys = CreateArray(length, _ => new PrivateKey()); + var validatorGolds = CreateArray(length, i => DelegationCurrency * Random.Shared.Next(1, length + 1)); + var height = 1L; + var actionContext = new ActionContext { }; + world = EnsureToMintAssets(world, validatorKeys, validatorGolds, height++); + world = EnsurePromotedValidators(world, validatorKeys, validatorGolds, height); + + // When + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedValidators = expectedRepository.GetValidatorList() + .ActiveSet().OrderBy(item => item.OperatorAddress).ToList(); + actionContext = new ActionContext + { + BlockIndex = height, + PreviousState = world, + Signer = AdminKey.Address, + }; + world = new UpdateValidators().Execute(actionContext); + + // Then + var actualValidators = world.GetValidatorSet().Validators; + Assert.Equal(expectedValidators.Count, actualValidators.Count); + for (var i = 0; i < expectedValidators.Count; i++) + { + var expectedValidator = expectedValidators[i]; + var actualValidator = actualValidators[i]; + Assert.Equal(expectedValidator, actualValidator); + } + } + + [Fact] + public void Execute_ExcludesTombstonedValidator() + { + // Given + const int length = 10; + var world = World; + var validatorKeys = CreateArray(length, _ => new PrivateKey()); + var validatorGolds = CreateArray(length, i => DelegationCurrency * 100); + var height = 1L; + var validatorGold = DelegationCurrency * 100; + var actionContext = new ActionContext { }; + world = EnsureToMintAssets(world, validatorKeys, validatorGolds, height++); + world = EnsurePromotedValidators(world, validatorKeys, validatorGolds, height++); + + // When + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedValidators = expectedRepository.GetValidatorList() + .ActiveSet().OrderBy(item => item.OperatorAddress).ToList(); + var updateValidators = new UpdateValidators(); + world = EnsureTombstonedValidator(world, validatorKeys[0], height); + actionContext = new ActionContext + { + BlockIndex = height, + PreviousState = world, + Signer = AdminKey.Address, + }; + world = updateValidators.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualValidators = actualRepository.GetValidatorList() + .ActiveSet().OrderBy(item => item.OperatorAddress).ToList(); + var tombstonedValidator = actualRepository.GetValidatorDelegatee(validatorKeys[0].Address); + + Assert.True(tombstonedValidator.Tombstoned); + Assert.Equal(expectedValidators.Count - 1, actualValidators.Count); + Assert.Contains(expectedValidators, v => v.PublicKey == validatorKeys[0].PublicKey); + Assert.DoesNotContain(actualValidators, v => v.PublicKey == validatorKeys[0].PublicKey); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs new file mode 100644 index 0000000000..7cd66f9f5d --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs @@ -0,0 +1,655 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Mocks; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; +using Libplanet.Types.Evidence; +using Nekoyume; +using Nekoyume.Action.Guild; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.Model.Guild; +using Nekoyume.Model.Stake; +using Nekoyume.Model.State; +using Nekoyume.Module; +using Nekoyume.Module.Guild; +using Nekoyume.TypedAddress; +using Nekoyume.ValidatorDelegation; +using Xunit; +using Nekoyume.Action.Guild.Migration.LegacyModels; + +public class ValidatorDelegationTestBase +{ + protected static readonly Currency NCG = Currency.Uncapped("NCG", 2, null); + protected static readonly Currency GuildGold = Currencies.GuildGold; + protected static readonly Currency Mead = Currencies.Mead; + protected static readonly Currency GoldCurrency = NCG; + protected static readonly Currency DelegationCurrency = GuildGold; + protected static readonly Currency[] GuildRewardCurrencies = new Currency[] { NCG, Mead }; + protected static readonly Currency RewardCurrency = NCG; + protected static readonly Currency GuildAllocateRewardCurrency = Mead; + protected static readonly Currency AllocateRewardCurrency = NCG; + protected static readonly Currency Dollar = Currency.Uncapped("dollar", 2, null); + private static readonly int _maximumIntegerLength = 15; + + public ValidatorDelegationTestBase() + { + var world = new World(MockUtil.MockModernWorldState); + var goldCurrencyState = new GoldCurrencyState(GoldCurrency); + World = world + .SetLegacyState(Addresses.GoldCurrency, goldCurrencyState.Serialize()) + .SetDelegationMigrationHeight(0); + } + + protected static BlockHash EmptyBlockHash { get; } + = new BlockHash(CreateArray(BlockHash.Size, _ => (byte)0x01)); + + protected static FungibleAssetValue MinimumDelegation { get; } = DelegationCurrency * 10; + + protected PrivateKey AdminKey { get; } = new PrivateKey(); + + protected IWorld World { get; } + + protected static T[] CreateArray(int length, Func creator) + => Enumerable.Range(0, length).Select(creator).ToArray(); + + protected static IWorld EnsureToMintAsset( + IWorld world, PrivateKey privateKey, FungibleAssetValue amount, long blockHeight) + { + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + }; + var address = privateKey.Address; + var poolAddress = StakeState.DeriveAddress(address); + return world.MintAsset(actionContext, poolAddress, amount); + } + + protected static IWorld EnsureToMintAssets( + IWorld world, PrivateKey[] privateKeys, FungibleAssetValue[] amounts, long blockHeight) + { + if (privateKeys.Length != amounts.Length) + { + throw new ArgumentException( + "The length of privateKeys and amounts must be the same."); + } + + for (var i = 0; i < privateKeys.Length; i++) + { + world = EnsureToMintAsset(world, privateKeys[i], amounts[i], blockHeight); + } + + return world; + } + + protected static IWorld EnsureProposer( + IWorld world, PrivateKey validatorKey, long blockHeight) + { + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = validatorKey.Address, + Miner = validatorKey.Address, + }; + return new RecordProposer().Execute(actionContext); + } + + protected static IWorld EnsurePromotedValidators( + IWorld world, + PrivateKey[] validatorKeys, + FungibleAssetValue[] amounts, + long blockHeight) + { + if (validatorKeys.Length != amounts.Length) + { + throw new ArgumentException( + "The length of validatorPrivateKeys and amounts must be the same."); + } + + for (var i = 0; i < validatorKeys.Length; i++) + { + world = EnsurePromotedValidator( + world, validatorKeys[i], amounts[i], blockHeight); + } + + return world; + } + + protected static IWorld EnsurePromotedValidator( + IWorld world, + PrivateKey validatorKey, + FungibleAssetValue amount, + long blockHeight) + { + var validatorPublicKey = validatorKey.PublicKey; + var promoteValidator = new PromoteValidator(validatorPublicKey, amount); + + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorPublicKey.Address, + BlockIndex = blockHeight, + }; + return promoteValidator.ExecutePublic(actionContext); + } + + protected static IWorld EnsureUnbondingValidator( + IWorld world, + Address validatorAddress, + BigInteger share, + long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = validatorAddress, + }; + var undelegateValidator = new UndelegateValidator(share); + return undelegateValidator.Execute(actionContext); + } + + protected static IWorld ExecuteSlashValidator( + IWorld world, PublicKey validatorKey, BlockCommit lastCommit, long blockHeight) + { + var slashValidator = new SlashValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = blockHeight, + LastCommit = lastCommit, + }; + return slashValidator.Execute(actionContext); + } + + protected static IWorld EnsureMakeGuild( + IWorld world, + Address guildMasterAddress, + Address validatorAddress, + long blockHeight, + int seed) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = guildMasterAddress, + RandomSeed = seed, + }; + var makeGuild = new MakeGuild(validatorAddress); + return makeGuild.ExecutePublic(actionContext); + } + + protected static IWorld EnsureJoinGuild( + IWorld world, + Address guildParticipantAddress, + Address guildMasterAddress, + Address validatorAddress, + long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var repo = new GuildRepository(world, new ActionContext()); + var guildAddress = repo.GetJoinedGuild(new AgentAddress(guildMasterAddress)) + ?? throw new ArgumentException($"Guild master {guildMasterAddress} does not have guild"); + if (validatorAddress != repo.GetGuild(guildAddress).ValidatorAddress) + { + throw new ArgumentException( + $"The guild of guild master does not belong to validator {validatorAddress}."); + } + + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = guildParticipantAddress, + }; + var joinGuild = new JoinGuild(guildAddress); + return joinGuild.Execute(actionContext); + } + + protected static IWorld EnsureQuitGuild( + IWorld world, + Address guildParticipantAddress, + long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var delegatorAddress = guildParticipantAddress; + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = delegatorAddress, + }; + var undelegateValidator = new QuitGuild(); + return undelegateValidator.Execute(actionContext); + } + + protected static IWorld EnsureJailedValidator( + IWorld world, PrivateKey validatorKey, ref long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var repository = new ValidatorRepository(world, new ActionContext()); + var delegatee = repository.GetValidatorDelegatee(validatorKey.Address); + if (delegatee.Jailed) + { + throw new ArgumentException( + "The validator is already jailed.", nameof(validatorKey)); + } + + for (var i = 0L; i <= AbstainHistory.MaxAbstainAllowance; i++) + { + var vote = CreateNullVote(validatorKey, blockHeight - 1); + var lastCommit = new BlockCommit( + height: blockHeight - 1, + round: 0, + blockHash: vote.BlockHash, + ImmutableArray.Create(vote)); + world = ExecuteSlashValidator( + world, validatorKey.PublicKey, lastCommit, blockHeight); + blockHeight++; + repository = new ValidatorRepository(world, new ActionContext()); + delegatee = repository.GetValidatorDelegatee(validatorKey.Address); + if (delegatee.Jailed) + { + break; + } + } + + return world; + } + + protected static IWorld EnsureTombstonedValidator( + IWorld world, PrivateKey validatorKey, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var evidence = CreateDuplicateVoteEvidence(validatorKey, blockHeight - 1); + var lastCommit = new BlockCommit( + height: blockHeight - 1, + round: 0, + blockHash: EmptyBlockHash, + ImmutableArray.Create(evidence.VoteRef)); + + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Evidence = new List { evidence }, + LastCommit = lastCommit, + }; + var slashValidator = new SlashValidator(); + + return slashValidator.Execute(actionContext); + } + + protected static IWorld EnsureUnjailedValidator( + IWorld world, PrivateKey validatorKey, ref long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var repository = new ValidatorRepository(world, new ActionContext()); + var delegatee = repository.GetValidatorDelegatee(validatorKey.Address); + if (!delegatee.Jailed) + { + throw new ArgumentException( + "The validator is not jailed.", nameof(validatorKey)); + } + + if (delegatee.Tombstoned) + { + throw new ArgumentException( + "The validator is tombstoned.", nameof(validatorKey)); + } + + blockHeight = Math.Max(blockHeight, delegatee.JailedUntil + 1); + + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = validatorKey.Address, + }; + var unjailedValidator = new UnjailValidator(); + return unjailedValidator.Execute(actionContext); + } + + protected static IWorld EnsureRewardAllocatedValidator( + IWorld world, + PrivateKey validatorKey, + FungibleAssetValue guildReward, + FungibleAssetValue reward, + ref long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var actionContext1 = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = validatorKey.Address, + Miner = validatorKey.Address, + }; + world = new RecordProposer().Execute(actionContext1); + + var lastCommit2 = CreateLastCommit(validatorKey, blockHeight - 1); + var actionContext2 = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = validatorKey.Address, + LastCommit = lastCommit2, + }; + + world = world.MintAsset(actionContext2, Addresses.RewardPool, guildReward); + + actionContext2 = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = validatorKey.Address, + LastCommit = lastCommit2, + }; + + world = world.MintAsset(actionContext2, Addresses.RewardPool, reward); + + var lastCommit3 = CreateLastCommit(validatorKey, blockHeight - 1); + + var actionContext3 = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = validatorKey.Address, + LastCommit = lastCommit3, + }; + + world = new AllocateGuildReward().Execute(actionContext3); + + actionContext3 = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = validatorKey.Address, + LastCommit = lastCommit3, + }; + + world = new AllocateReward().Execute(actionContext3); + + return world; + } + + protected static IWorld EnsureCommissionChangedValidator( + IWorld world, + PrivateKey validatorKey, + BigInteger commissionPercentage, + ref long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + if (commissionPercentage < ValidatorDelegatee.MinCommissionPercentage + || commissionPercentage > ValidatorDelegatee.MaxCommissionPercentage) + { + throw new ArgumentOutOfRangeException(nameof(commissionPercentage)); + } + + var cooldown = ValidatorDelegatee.CommissionPercentageUpdateCooldown; + var repository = new ValidatorRepository(world, new ActionContext()); + var delegatee = repository.GetValidatorDelegatee(validatorKey.Address); + var currentCommission = delegatee.CommissionPercentage; + var increment = commissionPercentage > currentCommission ? 1 : -1; + var preferredHeight = delegatee.CommissionPercentageLastUpdateHeight + cooldown; + + while (commissionPercentage != currentCommission) + { + blockHeight = Math.Min(preferredHeight, blockHeight + cooldown); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = validatorKey.Address, + }; + var setValidatorCommission = new SetValidatorCommission( + validatorKey.Address, currentCommission + increment); + world = setValidatorCommission.Execute(actionContext); + currentCommission += increment; + preferredHeight = blockHeight + cooldown; + } + + return world; + } + + protected static Vote CreateNullVote( + PrivateKey privateKey, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var power = new BigInteger(100); + var validator = new Validator(privateKey.PublicKey, power); + var blockHash = EmptyBlockHash; + var timestamp = DateTimeOffset.UtcNow; + var voteMetadata = new VoteMetadata( + height: blockHeight, + round: 0, + blockHash: blockHash, + timestamp: timestamp, + validatorPublicKey: validator.PublicKey, + validatorPower: power, + flag: VoteFlag.Null); + return voteMetadata.Sign(null); + } + + protected static Vote CreateVote( + PrivateKey privateKey, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var power = new BigInteger(100); + var validator = new Validator(privateKey.PublicKey, power); + var blockHash = EmptyBlockHash; + var timestamp = DateTimeOffset.UtcNow; + var voteMetadata = new VoteMetadata( + height: blockHeight, + round: 0, + blockHash: blockHash, + timestamp: timestamp, + validatorPublicKey: validator.PublicKey, + validatorPower: power, + flag: VoteFlag.PreCommit); + return voteMetadata.Sign(privateKey); + } + + protected static BlockCommit CreateLastCommit( + PrivateKey privateKey, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var vote = CreateVote(privateKey, blockHeight); + return new BlockCommit( + height: blockHeight, + round: 0, + blockHash: vote.BlockHash, + ImmutableArray.Create(vote)); + } + + protected static DuplicateVoteEvidence CreateDuplicateVoteEvidence( + PrivateKey validatorKey, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var validatorSet = new ValidatorSet(new List + { + new (validatorKey.PublicKey, new BigInteger(1000)), + }); + var vote1 = new VoteMetadata( + height: blockHeight, + round: 0, + blockHash: new BlockHash(CreateArray(BlockHash.Size, _ => (byte)0x01)), + timestamp: DateTimeOffset.UtcNow, + validatorPublicKey: validatorKey.PublicKey, + validatorPower: BigInteger.One, + flag: VoteFlag.PreCommit).Sign(validatorKey); + var vote2 = new VoteMetadata( + height: blockHeight, + round: 0, + blockHash: new BlockHash(CreateArray(BlockHash.Size, _ => (byte)0x02)), + timestamp: DateTimeOffset.UtcNow, + validatorPublicKey: validatorKey.PublicKey, + validatorPower: BigInteger.One, + flag: VoteFlag.PreCommit).Sign(validatorKey); + var evidence = new DuplicateVoteEvidence( + vote1, + vote2, + validatorSet, + vote1.Timestamp); + + return evidence; + } + + protected static FungibleAssetValue CalculateCommission( + FungibleAssetValue gold, ValidatorDelegatee delegatee) + => CalculateCommission(gold, delegatee.CommissionPercentage); + + protected static FungibleAssetValue CalculateCommission( + FungibleAssetValue gold, BigInteger percentage) + => (gold * percentage).DivRem(100).Quotient; + + protected static FungibleAssetValue CalculatePropserReward(FungibleAssetValue reward) + => (reward * ValidatorDelegatee.BaseProposerRewardPercentage).DivRem(100).Quotient; + + protected static FungibleAssetValue CalculateBonusPropserReward( + BigInteger preCommitPower, BigInteger totalPower, FungibleAssetValue reward) + => (reward * preCommitPower * ValidatorDelegatee.BonusProposerRewardPercentage) + .DivRem(totalPower * 100).Quotient; + + protected static FungibleAssetValue CalculateBonusPropserReward( + ImmutableArray votes, FungibleAssetValue reward) + { + var totalPower = votes.Select(item => item.ValidatorPower) + .OfType() + .Aggregate(BigInteger.Zero, (accum, next) => accum + next); + + var preCommitPower = votes.Where(item => item.Flag == VoteFlag.PreCommit) + .Select(item => item.ValidatorPower) + .OfType() + .Aggregate(BigInteger.Zero, (accum, next) => accum + next); + + return CalculateBonusPropserReward(preCommitPower, totalPower, reward); + } + + protected static FungibleAssetValue CalculateClaim(BigInteger share, BigInteger totalShare, FungibleAssetValue totalClaim) + => (totalClaim * share).DivRem(totalShare).Quotient; + + protected static FungibleAssetValue CalculateCommunityFund(ImmutableArray votes, FungibleAssetValue reward) + { + var totalPower = votes.Select(item => item.ValidatorPower) + .OfType() + .Aggregate(BigInteger.Zero, (accum, next) => accum + next); + + var powers = votes.Where(item => item.Flag == VoteFlag.PreCommit) + .Select(item => item.ValidatorPower) + .OfType(); + + var communityFund = reward; + foreach (var power in powers) + { + var distribution = (reward * power).DivRem(totalPower).Quotient; + System.Diagnostics.Trace.WriteLine($"expected validator distribution: {reward} * {power} / {totalPower} = {distribution}"); + communityFund -= distribution; + } + + return communityFund; + } + + protected static FungibleAssetValue GetRandomFAV(Currency currency) => GetRandomFAV(currency, Random.Shared); + + protected static FungibleAssetValue GetRandomFAV(Currency currency, Random random) + { + var decimalLength = random.Next(currency.DecimalPlaces); + var integerLength = random.Next(1, _maximumIntegerLength); + var decimalPart = Enumerable.Range(0, decimalLength) + .Aggregate(string.Empty, (s, i) => s + random.Next(10)); + var integerPart = Enumerable.Range(0, integerLength) + .Aggregate(string.Empty, (s, i) => s + (i != 0 ? random.Next(10) : random.Next(1, 10))); + var isDecimalZero = decimalLength == 0 || decimalPart.All(c => c == '0'); + var text = isDecimalZero is false ? $"{integerPart}.{decimalPart}" : integerPart; + + return FungibleAssetValue.Parse(currency, text); + } + + protected static FungibleAssetValue GetRandomCash( + Random random, FungibleAssetValue fav, int minDivisor = 1, int maxDivisor = 100) + { + Assert.True(minDivisor > 0); + Assert.True(maxDivisor > minDivisor && maxDivisor <= 100); + var denominator = random.Next(minDivisor, maxDivisor + 1); + var cash = fav.DivRem(denominator, out var remainder); + if (cash.Sign < 0 || cash > fav) + { + throw new InvalidOperationException("Invalid cash value."); + } + + return cash; + } + + protected static FungibleAssetValue GetBalance(IWorld world, Address address) + { + var poolAddress = StakeState.DeriveAddress(address); + return world.GetBalance(poolAddress, DelegationCurrency); + } +} diff --git a/.Lib9c.Tests/Delegation/DelegateeTest.cs b/.Lib9c.Tests/Delegation/DelegateeTest.cs new file mode 100644 index 0000000000..10dae9b102 --- /dev/null +++ b/.Lib9c.Tests/Delegation/DelegateeTest.cs @@ -0,0 +1,305 @@ +namespace Lib9c.Tests.Delegation +{ + using System; + using System.Numerics; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Delegation; + using Xunit; + + public class DelegateeTest + { + private readonly DelegationFixture _fixture; + + public DelegateeTest() + { + _fixture = new DelegationFixture(); + } + + [Fact] + public void Ctor() + { + var address = new Address("0xe8327129891e1A0B2E3F0bfa295777912295942a"); + var delegatee = new TestDelegatee(address, _fixture.TestRepository.DelegateeAccountAddress, _fixture.TestRepository); + Assert.Equal(address, delegatee.Address); + Assert.Equal(DelegationFixture.TestDelegationCurrency, delegatee.DelegationCurrency); + Assert.Equal(new Currency[] { DelegationFixture.TestRewardCurrency }, delegatee.RewardCurrencies); + Assert.Equal(3, delegatee.UnbondingPeriod); + Assert.Equal(5, delegatee.MaxUnbondLockInEntries); + Assert.Equal(5, delegatee.MaxRebondGraceEntries); + } + + [Fact] + public void GetSet() + { + var repo = _fixture.TestRepository; + var delegatee = _fixture.TestDelegatee1; + var delegator = _fixture.TestDelegator1; + delegatee.Bond(delegator, delegatee.DelegationCurrency * 10, 10L); + var delegateeRecon = repo.GetDelegatee(delegatee.Address); + Assert.Equal(delegatee.Address, delegateeRecon.Address); + Assert.Equal(delegator.Address, Assert.Single(delegateeRecon.Delegators)); + Assert.Equal(delegatee.TotalDelegated, delegateeRecon.TotalDelegated); + Assert.Equal(delegatee.TotalShares, delegateeRecon.TotalShares); + } + + [Fact] + public void Exchange() + { + // TODO: Test exchange after slashing is implemented. + // (Delegatee.ShareToBond & Delegatee.BondToShare) + } + + [Fact] + public void Bond() + { + var testDelegatee = _fixture.TestDelegatee1; + var testDelegator1 = _fixture.TestDelegator1; + var testDelegator2 = _fixture.TestDelegator2; + + var share1 = BigInteger.Zero; + var share2 = BigInteger.Zero; + var totalShare = BigInteger.Zero; + var totalBonding = testDelegatee.DelegationCurrency * 0; + + var bonding = testDelegatee.DelegationCurrency * 10; + var share = testDelegatee.ShareFromFAV(bonding); + share1 += share; + totalShare += share; + totalBonding += bonding; + + var bondedShare = testDelegatee.Bond(testDelegator1, bonding, 10L); + var bondedShare1 = _fixture.TestRepository.GetBond(testDelegatee, testDelegator1.Address).Share; + Assert.Equal(testDelegator1.Address, Assert.Single(testDelegatee.Delegators)); + Assert.Equal(share, bondedShare); + Assert.Equal(share1, bondedShare1); + Assert.Equal(totalShare, testDelegatee.TotalShares); + Assert.Equal(totalBonding, testDelegatee.TotalDelegated); + + bonding = testDelegatee.DelegationCurrency * 20; + share = testDelegatee.ShareFromFAV(bonding); + share1 += share; + totalShare += share; + totalBonding += bonding; + bondedShare = testDelegatee.Bond(testDelegator1, bonding, 20L); + bondedShare1 = _fixture.TestRepository.GetBond(testDelegatee, testDelegator1.Address).Share; + Assert.Equal(testDelegator1.Address, Assert.Single(testDelegatee.Delegators)); + Assert.Equal(share, bondedShare); + Assert.Equal(share1, bondedShare1); + Assert.Equal(totalShare, testDelegatee.TotalShares); + Assert.Equal(totalBonding, testDelegatee.TotalDelegated); + + bonding = testDelegatee.DelegationCurrency * 30; + share = testDelegatee.ShareFromFAV(bonding); + share2 += share; + totalShare += share; + totalBonding += bonding; + bondedShare = testDelegatee.Bond(testDelegator2, bonding, 30L); + var bondedShare2 = _fixture.TestRepository.GetBond(testDelegatee, testDelegator2.Address).Share; + Assert.Equal(2, testDelegatee.Delegators.Count); + Assert.Contains(testDelegator1.Address, testDelegatee.Delegators); + Assert.Contains(testDelegator2.Address, testDelegatee.Delegators); + Assert.Equal(share, bondedShare); + Assert.Equal(share2, bondedShare2); + Assert.Equal(totalShare, testDelegatee.TotalShares); + Assert.Equal(totalBonding, testDelegatee.TotalDelegated); + } + + [Fact] + public void CannotBondInvalidDelegator() + { + IDelegatee testDelegatee = _fixture.TestDelegatee1; + var testDelegator = _fixture.TestDelegator1; + var dummyDelegator = _fixture.DummyDelegator1; + + Assert.Throws( + () => testDelegatee.Bond( + dummyDelegator, testDelegatee.DelegationCurrency * 10, 10L)); + } + + [Fact] + public void CannotBondInvalidCurrency() + { + var testDelegatee = _fixture.TestDelegatee1; + var testDelegator = _fixture.TestDelegator1; + var dummyDelegator = _fixture.DummyDelegator1; + var invalidCurrency = Currency.Uncapped("invalid", 3, null); + + Assert.Throws( + () => testDelegatee.Bond( + testDelegator, invalidCurrency * 10, 10L)); + } + + [Fact] + public void Unbond() + { + var testDelegatee = _fixture.TestDelegatee1; + var testDelegator1 = _fixture.TestDelegator1; + var testDelegator2 = _fixture.TestDelegator2; + + var share1 = BigInteger.Zero; + var share2 = BigInteger.Zero; + var totalShares = BigInteger.Zero; + var totalDelegated = testDelegatee.DelegationCurrency * 0; + + var bonding = testDelegatee.DelegationCurrency * 100; + var share = testDelegatee.ShareFromFAV(bonding); + share1 += share; + totalShares += share; + totalDelegated += bonding; + testDelegatee.Bond(testDelegator1, bonding, 1L); + + bonding = testDelegatee.DelegationCurrency * 50; + share = testDelegatee.ShareFromFAV(bonding); + share2 += share; + totalShares += share; + totalDelegated += bonding; + testDelegatee.Bond(testDelegator2, bonding, 2L); + + var unbonding = share1 / 2; + share1 -= unbonding; + totalShares -= unbonding; + var unbondingFAV = testDelegatee.FAVFromShare(unbonding); + totalDelegated -= unbondingFAV; + var unbondedFAV = testDelegatee.Unbond(testDelegator1, unbonding, 3L); + var shareAfterUnbond = _fixture.TestRepository.GetBond(testDelegatee, testDelegator1.Address).Share; + Assert.Equal(2, testDelegatee.Delegators.Count); + Assert.Contains(testDelegator1.Address, testDelegatee.Delegators); + Assert.Contains(testDelegator2.Address, testDelegatee.Delegators); + Assert.Equal(unbondingFAV, unbondedFAV); + Assert.Equal(share1, shareAfterUnbond); + Assert.Equal(totalShares, testDelegatee.TotalShares); + Assert.Equal(totalDelegated, testDelegatee.TotalDelegated); + + unbonding = share2 / 2; + share2 -= unbonding; + totalShares -= unbonding; + unbondingFAV = testDelegatee.FAVFromShare(unbonding); + totalDelegated -= unbondingFAV; + unbondedFAV = testDelegatee.Unbond(testDelegator2, unbonding, 4L); + shareAfterUnbond = _fixture.TestRepository.GetBond(testDelegatee, testDelegator2.Address).Share; + Assert.Equal(2, testDelegatee.Delegators.Count); + Assert.Contains(testDelegator1.Address, testDelegatee.Delegators); + Assert.Contains(testDelegator2.Address, testDelegatee.Delegators); + Assert.Equal(unbondingFAV, unbondedFAV); + Assert.Equal(share2, shareAfterUnbond); + Assert.Equal(totalShares, testDelegatee.TotalShares); + Assert.Equal(totalDelegated, testDelegatee.TotalDelegated); + + totalShares -= share1; + unbondingFAV = testDelegatee.FAVFromShare(share1); + totalDelegated -= unbondingFAV; + unbondedFAV = testDelegatee.Unbond(testDelegator1, share1, 5L); + shareAfterUnbond = _fixture.TestRepository.GetBond(testDelegatee, testDelegator1.Address).Share; + Assert.Equal(testDelegator2.Address, Assert.Single(testDelegatee.Delegators)); + Assert.Equal(unbondingFAV, unbondedFAV); + Assert.Equal(BigInteger.Zero, shareAfterUnbond); + Assert.Equal(totalShares, testDelegatee.TotalShares); + Assert.Equal(totalDelegated, testDelegatee.TotalDelegated); + } + + [Fact] + public void CannotUnbondInvalidDelegator() + { + IDelegatee delegatee = _fixture.TestDelegatee1; + Assert.Throws( + () => delegatee.Unbond( + _fixture.DummyDelegator1, BigInteger.One, 10L)); + } + + [Fact] + public void ClearRemainderRewards() + { + var repo = _fixture.TestRepository; + var testDelegatee = _fixture.TestDelegatee1; + var testDelegator1 = _fixture.TestDelegator1; + var testDelegator2 = _fixture.TestDelegator2; + + var bonding1 = testDelegatee.DelegationCurrency * 3; + var bonding2 = testDelegatee.DelegationCurrency * 8; + + var bondedShare1 = testDelegatee.Bond(testDelegator1, bonding1, 10L); + var bondedShare2 = testDelegatee.Bond(testDelegator2, bonding2, 10L); + + foreach (var currency in testDelegatee.RewardCurrencies) + { + repo.MintAsset(testDelegatee.RewardPoolAddress, currency * 10); + } + + testDelegatee.CollectRewards(11L); + + testDelegatee.DistributeReward(testDelegator1, 11L); + + foreach (var currency in testDelegatee.RewardCurrencies) + { + var remainder = repo.GetBalance(DelegationFixture.FixedPoolAddress, currency); + Assert.Equal(currency * 0, remainder); + } + + testDelegatee.DistributeReward(testDelegator2, 11L); + + foreach (var currency in testDelegatee.RewardCurrencies) + { + var remainder = repo.GetBalance(DelegationFixture.FixedPoolAddress, currency); + Assert.Equal(new FungibleAssetValue(currency, 0, 1), remainder); + } + } + + [Fact] + public void AddressConsistency() + { + var testDelegatee1 = _fixture.TestDelegatee1; + var testDelegatee2 = _fixture.TestDelegatee2; + var testDelegator1 = _fixture.TestDelegator1; + var testDelegator2 = _fixture.TestDelegator2; + var dummyDelegatee1 = _fixture.DummyDelegatee1; + + Assert.Equal( + testDelegatee1.BondAddress(testDelegator1.Address), + testDelegatee1.BondAddress(testDelegator1.Address)); + Assert.NotEqual( + testDelegatee1.BondAddress(testDelegator1.Address), + testDelegatee1.BondAddress(testDelegator2.Address)); + Assert.NotEqual( + testDelegatee1.BondAddress(testDelegator1.Address), + testDelegatee2.BondAddress(testDelegator1.Address)); + + Assert.Equal( + testDelegatee1.UnbondLockInAddress(testDelegator1.Address), + testDelegatee1.UnbondLockInAddress(testDelegator1.Address)); + Assert.NotEqual( + testDelegatee1.UnbondLockInAddress(testDelegator1.Address), + testDelegatee1.UnbondLockInAddress(testDelegator2.Address)); + Assert.NotEqual( + testDelegatee1.UnbondLockInAddress(testDelegator1.Address), + testDelegatee2.UnbondLockInAddress(testDelegator1.Address)); + + Assert.Equal( + testDelegatee1.RebondGraceAddress(testDelegator1.Address), + testDelegatee1.RebondGraceAddress(testDelegator1.Address)); + Assert.NotEqual( + testDelegatee1.RebondGraceAddress(testDelegator1.Address), + testDelegatee1.RebondGraceAddress(testDelegator2.Address)); + Assert.NotEqual( + testDelegatee1.RebondGraceAddress(testDelegator1.Address), + testDelegatee2.RebondGraceAddress(testDelegator1.Address)); + + Assert.Equal(testDelegatee1.Address, dummyDelegatee1.Address); + Assert.NotEqual( + testDelegatee1.CurrentLumpSumRewardsRecordAddress(), + dummyDelegatee1.CurrentLumpSumRewardsRecordAddress()); + Assert.NotEqual( + testDelegatee1.LumpSumRewardsRecordAddress(1L), + dummyDelegatee1.LumpSumRewardsRecordAddress(1L)); + Assert.NotEqual( + testDelegatee1.BondAddress(testDelegator1.Address), + dummyDelegatee1.BondAddress(testDelegator1.Address)); + Assert.NotEqual( + testDelegatee1.UnbondLockInAddress(testDelegator1.Address), + dummyDelegatee1.UnbondLockInAddress(testDelegator1.Address)); + Assert.NotEqual( + testDelegatee1.RebondGraceAddress(testDelegator1.Address), + dummyDelegatee1.RebondGraceAddress(testDelegator1.Address)); + } + } +} diff --git a/.Lib9c.Tests/Delegation/DelegationFixture.cs b/.Lib9c.Tests/Delegation/DelegationFixture.cs new file mode 100644 index 0000000000..961775d217 --- /dev/null +++ b/.Lib9c.Tests/Delegation/DelegationFixture.cs @@ -0,0 +1,90 @@ +namespace Lib9c.Tests.Delegation +{ + using System.Collections.Generic; + using System.Linq; + using Lib9c.Tests.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Store; + using Libplanet.Store.Trie; + using Libplanet.Types.Assets; + using Libplanet.Types.Blocks; + using Nekoyume.Delegation; + + public class DelegationFixture + { + public static readonly Currency TestDelegationCurrency = Currency.Uncapped("test-del", 5, null); + public static readonly Currency TestRewardCurrency = Currency.Uncapped("test-reward", 5, null); + public static readonly Address FixedPoolAddress = new Address("0x75b21EbC56e5dAc817A1128Fb05d45853183117c"); + + public DelegationFixture() + { + var stateStore = new TrieStateStore(new MemoryKeyValueStore()); + var world = new World( + new WorldBaseState( + stateStore.Commit( + stateStore.GetStateRoot(null).SetMetadata( + new TrieMetadata(BlockMetadata.CurrentProtocolVersion))), + stateStore)); + var context = new ActionContext() + { + BlockProtocolVersion = BlockMetadata.CurrentProtocolVersion, + }; + + TestRepository = new TestRepository(world, context); + TestDelegator1 = new TestDelegator( + new Address("0x0054E98312C47E7Fa0ABed45C23Fa187e31C373a"), TestRepository.DelegatorAccountAddress, TestRepository); + TestDelegator2 = new TestDelegator( + new Address("0x327CCff388255E9399207C3d5a09357D0BBc73dF"), TestRepository.DelegatorAccountAddress, TestRepository); + TestDelegatee1 = new TestDelegatee( + new Address("0x67A44E11506b8f0Bb625fEECccb205b33265Bb48"), TestRepository.DelegateeAccountAddress, TestRepository); + TestDelegatee2 = new TestDelegatee( + new Address("0xea1C4eedEfC99691DEfc6eF2753FAfa8C17F4584"), TestRepository.DelegateeAccountAddress, TestRepository); + + DummyRepository = new DummyRepository(world, context); + DummyDelegatee1 = new DummyDelegatee( + new Address("0x67A44E11506b8f0Bb625fEECccb205b33265Bb48"), DummyRepository.DelegateeAccountAddress, DummyRepository); + DummyDelegator1 = new DummyDelegator( + new Address("0x0054E98312C47E7Fa0ABed45C23Fa187e31C373a"), DummyRepository.DelegateeAccountAddress, DummyRepository); + } + + public TestRepository TestRepository { get; } + + public DummyRepository DummyRepository { get; } + + public TestDelegator TestDelegator1 { get; } + + public TestDelegator TestDelegator2 { get; } + + public TestDelegatee TestDelegatee1 { get; } + + public TestDelegatee TestDelegatee2 { get; } + + public DummyDelegatee DummyDelegatee1 { get; } + + public DummyDelegator DummyDelegator1 { get; } + + public static FungibleAssetValue[] TotalRewardsOfRecords(IDelegatee delegatee, IDelegationRepository repo) + { + var rewards = delegatee.RewardCurrencies.Select(r => r * 0).ToArray(); + var record = repo.GetCurrentLumpSumRewardsRecord(delegatee); + + while (true) + { + foreach (var reward in rewards.Select((v, i) => (v, i))) + { + rewards[reward.i] += repo.World.GetBalance(record.Address, reward.v.Currency); + } + + if (record.LastStartHeight is null) + { + break; + } + + record = repo.GetLumpSumRewardsRecord(delegatee, record.LastStartHeight.Value); + } + + return rewards; + } + } +} diff --git a/.Lib9c.Tests/Delegation/DelegatorTest.cs b/.Lib9c.Tests/Delegation/DelegatorTest.cs new file mode 100644 index 0000000000..c599e2fb25 --- /dev/null +++ b/.Lib9c.Tests/Delegation/DelegatorTest.cs @@ -0,0 +1,587 @@ +namespace Lib9c.Tests.Delegation +{ + using System.Linq; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Xunit; + + public class DelegatorTest + { + private DelegationFixture _fixture; + + public DelegatorTest() + { + _fixture = new DelegationFixture(); + } + + [Fact] + public void Ctor() + { + var address = new Address("0x070e5719767CfB86712C31F5AB0072c48959d862"); + var delegator = new TestDelegator(address, _fixture.TestRepository.DelegatorAccountAddress, _fixture.TestRepository); + Assert.Equal(address, delegator.Address); + Assert.Empty(delegator.Delegatees); + } + + [Fact] + public void GetSet() + { + var repo = _fixture.TestRepository; + var delegator = _fixture.TestDelegator1; + var delegatee = _fixture.TestDelegatee1; + repo.MintAsset(delegator.Address, delegatee.DelegationCurrency * 100); + delegator.Delegate(delegatee, delegatee.DelegationCurrency * 10, 10L); + var delegatorRecon = repo.GetDelegator(delegator.Address); + Assert.Equal(delegator.Address, delegatorRecon.Address); + Assert.Equal(delegatee.Address, Assert.Single(delegatorRecon.Delegatees)); + } + + [Fact] + public void Delegate() + { + var repo = _fixture.TestRepository; + var delegator = _fixture.TestDelegator1; + var delegatee1 = _fixture.TestDelegatee1; + var delegatee2 = _fixture.TestDelegatee2; + var delegatorInitialBalance = delegatee1.DelegationCurrency * 100; + repo.MintAsset(delegator.Address, delegatorInitialBalance); + + var delegateFAV = delegatee1.DelegationCurrency * 10; + var delegateShare = delegatee1.ShareFromFAV(delegateFAV); + delegator.Delegate(delegatee1, delegateFAV, 1L); + var delegatorBalance = repo.World.GetBalance(delegator.Address, delegatee1.DelegationCurrency); + var delegateeBalance = repo.World.GetBalance(delegatee1.DelegationPoolAddress, delegatee1.DelegationCurrency); + var share = repo.GetBond(delegatee1, delegator.Address).Share; + Assert.Equal(delegatorInitialBalance - delegateFAV, delegatorBalance); + Assert.Equal(delegateFAV, delegateeBalance); + Assert.Equal(delegateShare, share); + Assert.Equal(delegateFAV, delegatee1.TotalDelegated); + Assert.Equal(delegateShare, delegatee1.TotalShares); + Assert.Equal(delegator.Address, Assert.Single(delegatee1.Delegators)); + Assert.Equal(delegatee1.Address, Assert.Single(delegator.Delegatees)); + + var delegateFAV2 = delegatee1.DelegationCurrency * 20; + var delegateShare2 = delegatee1.ShareFromFAV(delegateFAV2); + delegator.Delegate(delegatee1, delegateFAV2, 2L); + delegatorBalance = repo.World.GetBalance(delegator.Address, delegatee1.DelegationCurrency); + delegateeBalance = repo.World.GetBalance(delegatee1.DelegationPoolAddress, delegatee1.DelegationCurrency); + share = repo.GetBond(delegatee1, delegator.Address).Share; + Assert.Equal(delegatorInitialBalance - delegateFAV - delegateFAV2, delegatorBalance); + Assert.Equal(delegateFAV + delegateFAV2, delegateeBalance); + Assert.Equal(delegateShare + delegateShare2, share); + Assert.Equal(delegateFAV + delegateFAV2, delegatee1.TotalDelegated); + Assert.Equal(delegateShare + delegateShare2, delegatee1.TotalShares); + Assert.Equal(delegator.Address, Assert.Single(delegatee1.Delegators)); + Assert.Equal(delegatee1.Address, Assert.Single(delegator.Delegatees)); + + delegator.Delegate(delegatee2, delegateFAV, 3L); + delegatorBalance = repo.World.GetBalance(delegator.Address, delegatee2.DelegationCurrency); + delegateeBalance = repo.World.GetBalance(delegatee2.DelegationPoolAddress, delegatee2.DelegationCurrency); + share = repo.GetBond(delegatee2, delegator.Address).Share; + Assert.Equal(delegatorInitialBalance - delegateFAV * 2 - delegateFAV2, delegatorBalance); + Assert.Equal(delegateFAV, delegateeBalance); + Assert.Equal(delegateShare, share); + Assert.Equal(delegateFAV, delegatee2.TotalDelegated); + Assert.Equal(delegateShare, delegatee2.TotalShares); + Assert.Equal(2, delegator.Delegatees.Count); + Assert.Contains(delegatee1.Address, delegator.Delegatees); + Assert.Contains(delegatee2.Address, delegator.Delegatees); + } + + [Fact] + public void Undelegate() + { + var repo = _fixture.TestRepository; + var delegator = _fixture.TestDelegator1; + var delegatee = _fixture.TestDelegatee1; + var delegatorInitialBalance = delegatee.DelegationCurrency * 100; + repo.MintAsset(delegator.Address, delegatorInitialBalance); + var delegatingFAV = delegatee.DelegationCurrency * 10; + delegator.Delegate(delegatee, delegatingFAV, 9L); + var initialShare = repo.GetBond(delegatee, delegator.Address).Share; + var undelegatingShare = initialShare / 3; + var undelegatingFAV = delegatee.FAVFromShare(undelegatingShare); + delegator.Undelegate(delegatee, undelegatingShare, 10L); + var delegatorBalance = repo.World.GetBalance(delegator.Address, delegatee.DelegationCurrency); + var delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + var share1 = repo.GetBond(delegatee, delegator.Address).Share; + var unbondLockIn = repo.GetUnbondLockIn(delegatee, delegator.Address); + var unbondingSet = repo.GetUnbondingSet(); + Assert.Equal(delegatorInitialBalance - delegatingFAV, delegatorBalance); + Assert.Equal(delegatingFAV, delegateeBalance); + Assert.Equal(initialShare - undelegatingShare, share1); + Assert.Equal(delegatingFAV - undelegatingFAV, delegatee.TotalDelegated); + Assert.Equal(initialShare - undelegatingShare, delegatee.TotalShares); + Assert.Equal(delegator.Address, Assert.Single(delegatee.Delegators)); + Assert.Equal(delegatee.Address, Assert.Single(delegator.Delegatees)); + Assert.Equal(unbondLockIn.Address, Assert.Single(unbondingSet.FlattenedUnbondingRefs).Address); + var entriesByExpireHeight = Assert.Single(unbondLockIn.Entries); + Assert.Equal(10L + delegatee.UnbondingPeriod, entriesByExpireHeight.Key); + var entry = Assert.Single(entriesByExpireHeight.Value); + Assert.Equal(undelegatingFAV, entry.InitialUnbondingFAV); + Assert.Equal(undelegatingFAV, entry.UnbondingFAV); + Assert.Equal(10L, entry.CreationHeight); + Assert.Equal(10L + delegatee.UnbondingPeriod, entry.ExpireHeight); + + undelegatingShare = repo.GetBond(delegatee, delegator.Address).Share; + var undelegatingFAV2 = delegatee.FAVFromShare(undelegatingShare); + delegator.Undelegate(delegatee, undelegatingShare, 12L); + delegatorBalance = repo.World.GetBalance(delegator.Address, delegatee.DelegationCurrency); + delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + var share2 = repo.GetBond(delegatee, delegator.Address).Share; + unbondLockIn = repo.GetUnbondLockIn(delegatee, delegator.Address); + unbondingSet = repo.GetUnbondingSet(); + Assert.Equal(delegatorInitialBalance - delegatingFAV, delegatorBalance); + Assert.Equal(delegatingFAV, delegateeBalance); + Assert.Equal(share1 - undelegatingShare, share2); + Assert.Equal(delegatee.DelegationCurrency * 0, delegatee.TotalDelegated); + Assert.Equal(System.Numerics.BigInteger.Zero, delegatee.TotalShares); + Assert.Empty(delegator.Delegatees); + Assert.Empty(delegatee.Delegators); + Assert.Equal(unbondLockIn.Address, Assert.Single(unbondingSet.FlattenedUnbondingRefs).Address); + Assert.Equal(2, unbondLockIn.Entries.Count); + + unbondLockIn = unbondLockIn.Release(10L + delegatee.UnbondingPeriod - 1); + delegatorBalance = repo.World.GetBalance(delegator.Address, delegatee.DelegationCurrency); + delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + Assert.Equal(2, unbondLockIn.Entries.Count); + entriesByExpireHeight = unbondLockIn.Entries.ElementAt(0); + Assert.Equal(10L + delegatee.UnbondingPeriod, entriesByExpireHeight.Key); + entry = Assert.Single(entriesByExpireHeight.Value); + Assert.Equal(undelegatingFAV, entry.InitialUnbondingFAV); + Assert.Equal(undelegatingFAV, entry.UnbondingFAV); + Assert.Equal(10L, entry.CreationHeight); + Assert.Equal(10L + delegatee.UnbondingPeriod, entry.ExpireHeight); + entriesByExpireHeight = unbondLockIn.Entries.ElementAt(1); + Assert.Equal(12L + delegatee.UnbondingPeriod, entriesByExpireHeight.Key); + entry = Assert.Single(entriesByExpireHeight.Value); + Assert.Equal(undelegatingFAV2, entry.InitialUnbondingFAV); + Assert.Equal(undelegatingFAV2, entry.UnbondingFAV); + Assert.Equal(12L, entry.CreationHeight); + Assert.Equal(12L + delegatee.UnbondingPeriod, entry.ExpireHeight); + Assert.Equal(delegatorInitialBalance - delegatingFAV, delegatorBalance); + Assert.Equal(delegatingFAV, delegateeBalance); + + unbondLockIn = unbondLockIn.Release(10L + delegatee.UnbondingPeriod); + delegatorBalance = repo.World.GetBalance(delegator.Address, delegatee.DelegationCurrency); + delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + entriesByExpireHeight = Assert.Single(unbondLockIn.Entries); + Assert.Equal(12L + delegatee.UnbondingPeriod, entriesByExpireHeight.Key); + entry = Assert.Single(entriesByExpireHeight.Value); + Assert.Equal(undelegatingFAV2, entry.InitialUnbondingFAV); + Assert.Equal(undelegatingFAV2, entry.UnbondingFAV); + Assert.Equal(12L, entry.CreationHeight); + Assert.Equal(12L + delegatee.UnbondingPeriod, entry.ExpireHeight); + Assert.Equal(delegatorInitialBalance - delegatingFAV + undelegatingFAV, delegatorBalance); + Assert.Equal(delegatingFAV - undelegatingFAV, delegateeBalance); + + unbondLockIn = unbondLockIn.Release(12L + delegatee.UnbondingPeriod); + delegatorBalance = repo.World.GetBalance(delegator.Address, delegatee.DelegationCurrency); + delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + Assert.Empty(unbondLockIn.Entries); + Assert.Equal(delegatorInitialBalance, delegatorBalance); + Assert.Equal(delegatee.DelegationCurrency * 0, delegateeBalance); + } + + [Fact] + public void Redelegate() + { + var repo = _fixture.TestRepository; + var delegator = _fixture.TestDelegator1; + var delegatee1 = _fixture.TestDelegatee1; + var delegatee2 = _fixture.TestDelegatee2; + var delegatorInitialBalance = delegatee1.DelegationCurrency * 100; + repo.MintAsset(delegator.Address, delegatorInitialBalance); + var delegatingFAV = delegatee1.DelegationCurrency * 10; + delegator.Delegate(delegatee1, delegatingFAV, 1L); + Assert.Equal(delegatee1.Address, Assert.Single(delegator.Delegatees)); + var initialShare = repo.GetBond(delegatee1, delegator.Address).Share; + var redelegatingShare = initialShare / 3; + var redelegatingFAV = delegatee1.FAVFromShare(redelegatingShare); + var redelegatedDstShare = delegatee2.ShareFromFAV(redelegatingFAV); + delegator.Redelegate(delegatee1, delegatee2, redelegatingShare, 10L); + var delegatorBalance = repo.World.GetBalance(delegator.Address, delegatee1.DelegationCurrency); + var delegatee1Balance = repo.World.GetBalance(delegatee1.DelegationPoolAddress, delegatee1.DelegationCurrency); + var delegatee2Balance = repo.World.GetBalance(delegatee2.DelegationPoolAddress, delegatee2.DelegationCurrency); + var share1 = repo.GetBond(delegatee1, delegator.Address).Share; + var share2 = repo.GetBond(delegatee2, delegator.Address).Share; + var rebondGrace = repo.GetRebondGrace(delegatee1, delegator.Address); + var unbondingSet = repo.GetUnbondingSet(); + Assert.Equal(delegatorInitialBalance - delegatingFAV, delegatorBalance); + Assert.Equal(delegatingFAV, delegatee1Balance); + Assert.Equal(initialShare - redelegatingShare, share1); + Assert.Equal(initialShare - redelegatingShare, delegatee1.TotalShares); + Assert.Equal(redelegatedDstShare, share2); + Assert.Equal(redelegatedDstShare, delegatee2.TotalShares); + Assert.Equal(delegatingFAV - redelegatingFAV, delegatee1.TotalDelegated); + Assert.Equal(redelegatingFAV, delegatee2.TotalDelegated); + Assert.Equal(delegator.Address, Assert.Single(delegatee1.Delegators)); + Assert.Equal(delegator.Address, Assert.Single(delegatee2.Delegators)); + Assert.Equal(2, delegator.Delegatees.Count); + Assert.Equal(rebondGrace.Address, Assert.Single(unbondingSet.FlattenedUnbondingRefs).Address); + var entriesByExpireHeight = Assert.Single(rebondGrace.Entries); + Assert.Equal(10L + delegatee1.UnbondingPeriod, entriesByExpireHeight.Key); + var entry = Assert.Single(entriesByExpireHeight.Value); + Assert.Equal(delegatee2.Address, entry.UnbondeeAddress); + Assert.Equal(redelegatingFAV, entry.InitialUnbondingFAV); + Assert.Equal(redelegatingFAV, entry.UnbondingFAV); + Assert.Equal(10L, entry.CreationHeight); + Assert.Equal(10L + delegatee1.UnbondingPeriod, entry.ExpireHeight); + + var redelegatingShare2 = repo.GetBond(delegatee1, delegator.Address).Share; + var redelegatingFAV2 = delegatee1.FAVFromShare(redelegatingShare2); + var redelegatedDstShare2 = delegatee2.ShareFromFAV(redelegatingFAV2); + delegator.Redelegate(delegatee1, delegatee2, redelegatingShare2, 12L); + delegatorBalance = repo.World.GetBalance(delegator.Address, delegatee1.DelegationCurrency); + delegatee1Balance = repo.World.GetBalance(delegatee1.DelegationPoolAddress, delegatee1.DelegationCurrency); + delegatee2Balance = repo.World.GetBalance(delegatee2.DelegationPoolAddress, delegatee2.DelegationCurrency); + share1 = repo.GetBond(delegatee1, delegator.Address).Share; + share2 = repo.GetBond(delegatee2, delegator.Address).Share; + rebondGrace = repo.GetRebondGrace(delegatee1, delegator.Address); + unbondingSet = repo.GetUnbondingSet(); + Assert.Equal(delegatorInitialBalance - delegatingFAV, delegatorBalance); + Assert.Equal(delegatingFAV, delegatee1Balance); + Assert.Equal(initialShare - redelegatingShare - redelegatingShare2, share1); + Assert.Equal(initialShare - redelegatingShare - redelegatingShare2, delegatee1.TotalShares); + Assert.Equal(redelegatedDstShare + redelegatedDstShare2, share2); + Assert.Equal(redelegatedDstShare + redelegatedDstShare2, delegatee2.TotalShares); + Assert.Equal(delegatingFAV - redelegatingFAV - redelegatingFAV2, delegatee1.TotalDelegated); + Assert.Equal(redelegatingFAV + redelegatingFAV2, delegatee2.TotalDelegated); + Assert.Empty(delegatee1.Delegators); + Assert.Equal(delegator.Address, Assert.Single(delegatee2.Delegators)); + Assert.Equal(delegatee2.Address, Assert.Single(delegator.Delegatees)); + Assert.Equal(rebondGrace.Address, Assert.Single(unbondingSet.FlattenedUnbondingRefs).Address); + Assert.Equal(2, rebondGrace.Entries.Count); + + rebondGrace = rebondGrace.Release(10L + delegatee1.UnbondingPeriod - 1); + Assert.Equal(2, rebondGrace.Entries.Count); + entriesByExpireHeight = rebondGrace.Entries.ElementAt(0); + Assert.Equal(10L + delegatee1.UnbondingPeriod, entriesByExpireHeight.Key); + entry = Assert.Single(entriesByExpireHeight.Value); + Assert.Equal(delegatee2.Address, entry.UnbondeeAddress); + Assert.Equal(redelegatingFAV, entry.InitialUnbondingFAV); + Assert.Equal(redelegatingFAV, entry.UnbondingFAV); + Assert.Equal(10L, entry.CreationHeight); + Assert.Equal(10L + delegatee1.UnbondingPeriod, entry.ExpireHeight); + entriesByExpireHeight = rebondGrace.Entries.ElementAt(1); + Assert.Equal(12L + delegatee1.UnbondingPeriod, entriesByExpireHeight.Key); + entry = Assert.Single(entriesByExpireHeight.Value); + Assert.Equal(delegatee2.Address, entry.UnbondeeAddress); + Assert.Equal(redelegatingFAV2, entry.InitialUnbondingFAV); + Assert.Equal(redelegatingFAV2, entry.UnbondingFAV); + Assert.Equal(12L, entry.CreationHeight); + Assert.Equal(12L + delegatee1.UnbondingPeriod, entry.ExpireHeight); + + rebondGrace = rebondGrace.Release(10L + delegatee1.UnbondingPeriod); + entriesByExpireHeight = Assert.Single(rebondGrace.Entries); + Assert.Equal(12L + delegatee1.UnbondingPeriod, entriesByExpireHeight.Key); + entry = Assert.Single(entriesByExpireHeight.Value); + Assert.Equal(delegatee2.Address, entry.UnbondeeAddress); + Assert.Equal(redelegatingFAV2, entry.InitialUnbondingFAV); + Assert.Equal(redelegatingFAV2, entry.UnbondingFAV); + Assert.Equal(12L, entry.CreationHeight); + Assert.Equal(12L + delegatee1.UnbondingPeriod, entry.ExpireHeight); + + rebondGrace = rebondGrace.Release(12L + delegatee1.UnbondingPeriod); + Assert.Empty(rebondGrace.Entries); + } + + [Fact] + public void RewardOnDelegate() + { + var repo = _fixture.TestRepository; + var delegator1 = _fixture.TestDelegator1; + var delegator2 = _fixture.TestDelegator2; + var delegatee = _fixture.TestDelegatee1; + var delegatorInitialBalance = delegatee.DelegationCurrency * 100; + repo.MintAsset(delegator1.Address, delegatorInitialBalance); + repo.MintAsset(delegator2.Address, delegatorInitialBalance); + + var rewards = delegatee.RewardCurrencies.Select(r => r * 100); + foreach (var reward in rewards) + { + repo.MintAsset(delegatee.RewardPoolAddress, reward); + } + + // EndBlock after delegatee's reward + delegatee.CollectRewards(10L); + + var delegatingFAV1 = delegatee.DelegationCurrency * 10; + delegator1.Delegate(delegatee, delegatingFAV1, 10L); + var delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); + var delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + var share1 = repo.GetBond(delegatee, delegator1.Address).Share; + Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); + + var delegatingFAV2 = delegatee.DelegationCurrency * 20; + delegator2.Delegate(delegatee, delegatingFAV2, 10L); + var delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); + delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + var share2 = repo.GetBond(delegatee, delegator2.Address).Share; + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + + var totalShares = delegatee.TotalShares; + + foreach (var reward in rewards) + { + repo.MintAsset(delegatee.RewardPoolAddress, reward); + } + + // EndBlock after delegatee's reward + delegatee.CollectRewards(10L); + + delegatingFAV1 = delegatee.DelegationCurrency * 10; + delegator1.Delegate(delegatee, delegatingFAV1, 11L); + delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); + delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); + var delegator1RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator1.Address, c)); + var collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + + var rewards1 = rewards.Select(r => (r * share1).DivRem(totalShares, out _)); + Assert.Equal(delegatorInitialBalance - delegatingFAV1 * 2, delegator1Balance); + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + Assert.Equal(rewards1, delegator1RewardBalances); + Assert.Equal(rewards.Zip(rewards1, (f, s) => f * 2 - s), collectedRewards); + + delegator2.Delegate(delegatee, delegatingFAV2, 11L); + delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); + delegator1RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator1.Address, c)); + var delegator2RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator2.Address, c)); + collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + + var rewards2 = rewards.Select(r => (r * share2).DivRem(totalShares, out _)); + Assert.Equal(delegatorInitialBalance - delegatingFAV1 * 2, delegator1Balance); + Assert.Equal(delegatorInitialBalance - delegatingFAV2 * 2, delegator2Balance); + Assert.Equal(rewards1, delegator1RewardBalances); + Assert.Equal(rewards2, delegator2RewardBalances); + Assert.Equal(delegatee.RewardCurrencies.Select(c => c * 0), collectedRewards); + } + + [Fact] + public void RewardOnUndelegate() + { + var repo = _fixture.TestRepository; + var delegator1 = _fixture.TestDelegator1; + var delegator2 = _fixture.TestDelegator2; + var delegatee = _fixture.TestDelegatee1; + var delegatorInitialBalance = delegatee.DelegationCurrency * 100; + repo.MintAsset(delegator1.Address, delegatorInitialBalance); + repo.MintAsset(delegator2.Address, delegatorInitialBalance); + + var rewards = delegatee.RewardCurrencies.Select(r => r * 100); + foreach (var reward in rewards) + { + repo.MintAsset(delegatee.RewardPoolAddress, reward); + } + + // EndBlock after delegatee's reward + delegatee.CollectRewards(10L); + + var delegatingFAV1 = delegatee.DelegationCurrency * 10; + delegator1.Delegate(delegatee, delegatingFAV1, 10L); + var delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); + var delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + var share1 = repo.GetBond(delegatee, delegator1.Address).Share; + Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); + + var delegatingFAV2 = delegatee.DelegationCurrency * 20; + delegator2.Delegate(delegatee, delegatingFAV2, 10L); + var delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); + delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + var share2 = repo.GetBond(delegatee, delegator2.Address).Share; + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + + var totalShares = delegatee.TotalShares; + + foreach (var reward in rewards) + { + repo.MintAsset(delegatee.RewardPoolAddress, reward); + } + + // EndBlock after delegatee's reward + delegatee.CollectRewards(10L); + + var shareToUndelegate = repo.GetBond(delegatee, delegator1.Address).Share / 3; + delegator1.Undelegate(delegatee, shareToUndelegate, 11L); + delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); + var delegator1RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator1.Address, c)); + var collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + + var rewards1 = rewards.Select(r => (r * share1).DivRem(totalShares, out _)); + Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + Assert.Equal(rewards1, delegator1RewardBalances); + Assert.Equal(rewards.Zip(rewards1, (f, s) => f * 2 - s), collectedRewards); + + shareToUndelegate = repo.GetBond(delegatee, delegator2.Address).Share / 2; + delegator2.Undelegate(delegatee, shareToUndelegate, 11L); + delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); + delegator1RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator1.Address, c)); + var delegator2RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator2.Address, c)); + collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + + var rewards2 = rewards.Select(r => (r * share2).DivRem(totalShares, out _)); + Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + Assert.Equal(rewards1, delegator1RewardBalances); + Assert.Equal(rewards2, delegator2RewardBalances); + Assert.Equal(delegatee.RewardCurrencies.Select(c => c * 0), collectedRewards); + } + + [Fact] + public void RewardOnRedelegate() + { + var repo = _fixture.TestRepository; + var delegator1 = _fixture.TestDelegator1; + var delegator2 = _fixture.TestDelegator2; + var delegatee = _fixture.TestDelegatee1; + var dstDelegatee = _fixture.TestDelegatee2; + var delegatorInitialBalance = delegatee.DelegationCurrency * 100; + repo.MintAsset(delegator1.Address, delegatorInitialBalance); + repo.MintAsset(delegator2.Address, delegatorInitialBalance); + + var rewards = delegatee.RewardCurrencies.Select(r => r * 100); + foreach (var reward in rewards) + { + repo.MintAsset(delegatee.RewardPoolAddress, reward); + } + + // EndBlock after delegatee's reward + delegatee.CollectRewards(10L); + + var delegatingFAV1 = delegatee.DelegationCurrency * 10; + delegator1.Delegate(delegatee, delegatingFAV1, 10L); + var delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); + var delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + var share1 = repo.GetBond(delegatee, delegator1.Address).Share; + Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); + + var delegatingFAV2 = delegatee.DelegationCurrency * 20; + delegator2.Delegate(delegatee, delegatingFAV2, 10L); + var delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); + delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + var share2 = repo.GetBond(delegatee, delegator2.Address).Share; + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + + var totalShares = delegatee.TotalShares; + + foreach (var reward in rewards) + { + repo.MintAsset(delegatee.RewardPoolAddress, reward); + } + + // EndBlock after delegatee's reward + delegatee.CollectRewards(10L); + + var shareToRedelegate = repo.GetBond(delegatee, delegator1.Address).Share / 3; + delegator1.Redelegate(delegatee, dstDelegatee, shareToRedelegate, 11L); + delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); + var delegator1RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator1.Address, c)); + var collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + + var rewards1 = rewards.Select(r => (r * share1).DivRem(totalShares, out _)); + Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + Assert.Equal(rewards1, delegator1RewardBalances); + Assert.Equal(rewards.Zip(rewards1, (f, s) => f * 2 - s), collectedRewards); + + shareToRedelegate = repo.GetBond(delegatee, delegator2.Address).Share / 2; + delegator2.Redelegate(delegatee, dstDelegatee, shareToRedelegate, 11L); + delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); + delegator1RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator1.Address, c)); + var delegator2RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator2.Address, c)); + collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + + var rewards2 = rewards.Select(r => (r * share2).DivRem(totalShares, out _)); + Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + Assert.Equal(rewards1, delegator1RewardBalances); + Assert.Equal(rewards2, delegator2RewardBalances); + Assert.Equal(delegatee.RewardCurrencies.Select(c => c * 0), collectedRewards); + } + + [Fact] + public void RewardOnClaim() + { + var repo = _fixture.TestRepository; + var delegator1 = _fixture.TestDelegator1; + var delegator2 = _fixture.TestDelegator2; + var delegatee = _fixture.TestDelegatee1; + var dstDelegatee = _fixture.TestDelegatee2; + var delegatorInitialBalance = delegatee.DelegationCurrency * 100; + repo.MintAsset(delegator1.Address, delegatorInitialBalance); + repo.MintAsset(delegator2.Address, delegatorInitialBalance); + + var rewards = delegatee.RewardCurrencies.Select(r => r * 100); + foreach (var reward in rewards) + { + repo.MintAsset(delegatee.RewardPoolAddress, reward); + } + + // EndBlock after delegatee's reward + delegatee.CollectRewards(10L); + + var delegatingFAV1 = delegatee.DelegationCurrency * 10; + delegator1.Delegate(delegatee, delegatingFAV1, 10L); + var delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); + var delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + var share1 = repo.GetBond(delegatee, delegator1.Address).Share; + Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); + + var delegatingFAV2 = delegatee.DelegationCurrency * 20; + delegator2.Delegate(delegatee, delegatingFAV2, 10L); + var delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); + delegateeBalance = repo.World.GetBalance(delegatee.DelegationPoolAddress, delegatee.DelegationCurrency); + var share2 = repo.GetBond(delegatee, delegator2.Address).Share; + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + + var totalShares = delegatee.TotalShares; + + foreach (var reward in rewards) + { + repo.MintAsset(delegatee.RewardPoolAddress, reward); + } + + // EndBlock after delegatee's reward + delegatee.CollectRewards(10L); + + var shareToRedelegate = repo.GetBond(delegatee, delegator1.Address).Share / 3; + delegator1.ClaimReward(delegatee, 11L); + delegator1Balance = repo.World.GetBalance(delegator1.Address, delegatee.DelegationCurrency); + var delegator1RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator1.Address, c)); + var collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + + var rewards1 = rewards.Select(r => (r * share1).DivRem(totalShares, out _)); + Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + Assert.Equal(rewards1, delegator1RewardBalances); + Assert.Equal(rewards.Zip(rewards1, (f, s) => f * 2 - s), collectedRewards); + + shareToRedelegate = repo.GetBond(delegatee, delegator2.Address).Share / 2; + delegator2.ClaimReward(delegatee, 11L); + delegator2Balance = repo.World.GetBalance(delegator2.Address, delegatee.DelegationCurrency); + delegator1RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator1.Address, c)); + var delegator2RewardBalances = delegatee.RewardCurrencies.Select( + c => repo.World.GetBalance(delegator2.Address, c)); + collectedRewards = DelegationFixture.TotalRewardsOfRecords(delegatee, repo); + + var rewards2 = rewards.Select(r => (r * share2).DivRem(totalShares, out _)); + Assert.Equal(delegatorInitialBalance - delegatingFAV1, delegator1Balance); + Assert.Equal(delegatorInitialBalance - delegatingFAV2, delegator2Balance); + Assert.Equal(rewards1, delegator1RewardBalances); + Assert.Equal(rewards2, delegator2RewardBalances); + Assert.Equal(delegatee.RewardCurrencies.Select(c => c * 0), collectedRewards); + } + } +} diff --git a/.Lib9c.Tests/Delegation/DummyDelegatee.cs b/.Lib9c.Tests/Delegation/DummyDelegatee.cs new file mode 100644 index 0000000000..1ffaa83a82 --- /dev/null +++ b/.Lib9c.Tests/Delegation/DummyDelegatee.cs @@ -0,0 +1,29 @@ +using Lib9c.Tests.Delegation; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Delegation; + +public sealed class DummyDelegatee : Delegatee +{ + public DummyDelegatee(Address address, IDelegationRepository repository) + : base(address, repository) + { + } + + public DummyDelegatee(Address address, Address accountAddress, DummyRepository repository) + : base( + address, + accountAddress, + DelegationFixture.TestDelegationCurrency, + new Currency[] { DelegationFixture.TestRewardCurrency }, + DelegationAddress.DelegationPoolAddress(address, accountAddress), + DelegationAddress.RewardPoolAddress(address, accountAddress), + DelegationFixture.FixedPoolAddress, + DelegationFixture.FixedPoolAddress, + 3, + 5, + 5, + repository) + { + } +} diff --git a/.Lib9c.Tests/Delegation/DummyDelegator.cs b/.Lib9c.Tests/Delegation/DummyDelegator.cs new file mode 100644 index 0000000000..d7b0223f56 --- /dev/null +++ b/.Lib9c.Tests/Delegation/DummyDelegator.cs @@ -0,0 +1,15 @@ +using Libplanet.Crypto; +using Nekoyume.Delegation; + +public sealed class DummyDelegator : Delegator +{ + public DummyDelegator(Address address, IDelegationRepository repository) + : base(address, repository) + { + } + + public DummyDelegator(Address address, Address accountAddress, DummyRepository repo) + : base(address, accountAddress, address, address, repo) + { + } +} diff --git a/.Lib9c.Tests/Delegation/DummyRepository.cs b/.Lib9c.Tests/Delegation/DummyRepository.cs new file mode 100644 index 0000000000..85332a4785 --- /dev/null +++ b/.Lib9c.Tests/Delegation/DummyRepository.cs @@ -0,0 +1,58 @@ +#nullable enable +namespace Nekoyume.Delegation +{ + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Action; + + public class DummyRepository : DelegationRepository + { + public DummyRepository(IWorld world, IActionContext context) + : base( + world: world, + actionContext: context, + delegateeAccountAddress: new Address("1000000000000000000000000000000000000000"), + delegatorAccountAddress: new Address("1000000000000000000000000000000000000001"), + delegateeMetadataAccountAddress: new Address("0000000000000000000000000000000000000002"), + delegatorMetadataAccountAddress: new Address("0000000000000000000000000000000000000003"), + bondAccountAddress: new Address("0000000000000000000000000000000000000004"), + unbondLockInAccountAddress: new Address("0000000000000000000000000000000000000005"), + rebondGraceAccountAddress: new Address("0000000000000000000000000000000000000006"), + unbondingSetAccountAddress: new Address("0000000000000000000000000000000000000007"), + lumpSumRewardRecordAccountAddress: new Address("0000000000000000000000000000000000000008")) + { + } + + public override DummyDelegatee GetDelegatee(Address address) + { + try + { + return new DummyDelegatee(address, this); + } + catch (FailedLoadStateException) + { + return new DummyDelegatee(address, DelegateeAccountAddress, this); + } + } + + public override DummyDelegator GetDelegator(Address address) + { + try + { + return new DummyDelegator(address, this); + } + catch (FailedLoadStateException) + { + return new DummyDelegator(address, DelegatorAccountAddress, this); + } + } + + public override void SetDelegatee(IDelegatee delegatee) + => SetDelegateeMetadata(((DummyDelegatee)delegatee).Metadata); + + public override void SetDelegator(IDelegator delegator) + => SetDelegatorMetadata(((DummyDelegator)delegator).Metadata); + } +} diff --git a/.Lib9c.Tests/Delegation/TestDelegatee.cs b/.Lib9c.Tests/Delegation/TestDelegatee.cs new file mode 100644 index 0000000000..f1cfc67963 --- /dev/null +++ b/.Lib9c.Tests/Delegation/TestDelegatee.cs @@ -0,0 +1,31 @@ +namespace Lib9c.Tests.Delegation +{ + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Delegation; + + public sealed class TestDelegatee : Delegatee + { + public TestDelegatee(Address address, TestRepository repository) + : base(address, repository) + { + } + + public TestDelegatee(Address address, Address accountAddress, TestRepository repository) + : base( + address, + accountAddress, + DelegationFixture.TestDelegationCurrency, + new Currency[] { DelegationFixture.TestRewardCurrency }, + DelegationAddress.DelegationPoolAddress(address, accountAddress), + DelegationAddress.RewardPoolAddress(address, accountAddress), + DelegationFixture.FixedPoolAddress, + DelegationFixture.FixedPoolAddress, + 3, + 5, + 5, + repository) + { + } + } +} diff --git a/.Lib9c.Tests/Delegation/TestDelegator.cs b/.Lib9c.Tests/Delegation/TestDelegator.cs new file mode 100644 index 0000000000..fa1c4c2e99 --- /dev/null +++ b/.Lib9c.Tests/Delegation/TestDelegator.cs @@ -0,0 +1,18 @@ +namespace Lib9c.Tests.Delegation +{ + using Libplanet.Crypto; + using Nekoyume.Delegation; + + public sealed class TestDelegator : Delegator + { + public TestDelegator(Address address, TestRepository repo) + : base(address, repo) + { + } + + public TestDelegator(Address address, Address accountAddress, TestRepository repo) + : base(address, accountAddress, address, address, repo) + { + } + } +} diff --git a/.Lib9c.Tests/Delegation/TestRepository.cs b/.Lib9c.Tests/Delegation/TestRepository.cs new file mode 100644 index 0000000000..b072d9ce86 --- /dev/null +++ b/.Lib9c.Tests/Delegation/TestRepository.cs @@ -0,0 +1,65 @@ +#nullable enable +namespace Nekoyume.Delegation +{ + using Lib9c.Tests.Delegation; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Action; + + public class TestRepository : DelegationRepository + { + private readonly IActionContext _context; + + public TestRepository(IWorld world, IActionContext context) + : base( + world: world, + actionContext: context, + delegateeAccountAddress: new Address("0000000000000000000000000000000000000000"), + delegatorAccountAddress: new Address("0000000000000000000000000000000000000001"), + delegateeMetadataAccountAddress: new Address("0000000000000000000000000000000000000002"), + delegatorMetadataAccountAddress: new Address("0000000000000000000000000000000000000003"), + bondAccountAddress: new Address("0000000000000000000000000000000000000004"), + unbondLockInAccountAddress: new Address("0000000000000000000000000000000000000005"), + rebondGraceAccountAddress: new Address("0000000000000000000000000000000000000006"), + unbondingSetAccountAddress: new Address("0000000000000000000000000000000000000007"), + lumpSumRewardRecordAccountAddress: new Address("0000000000000000000000000000000000000008")) + { + _context = context; + } + + public override TestDelegatee GetDelegatee(Address address) + { + try + { + return new TestDelegatee(address, this); + } + catch (FailedLoadStateException) + { + return new TestDelegatee(address, DelegateeAccountAddress, this); + } + } + + public override TestDelegator GetDelegator(Address address) + { + try + { + return new TestDelegator(address, this); + } + catch (FailedLoadStateException) + { + return new TestDelegator(address, DelegatorAccountAddress, this); + } + } + + public override void SetDelegatee(IDelegatee delegatee) + => SetDelegateeMetadata(((TestDelegatee)delegatee).Metadata); + + public override void SetDelegator(IDelegator delegator) + => SetDelegatorMetadata(((TestDelegator)delegator).Metadata); + + public void MintAsset(Address recipient, FungibleAssetValue value) + => previousWorld = previousWorld.MintAsset(_context, recipient, value); + } +} diff --git a/.Lib9c.Tests/Model/Guild/GuildApplicationTest.cs b/.Lib9c.Tests/Model/Guild/GuildApplicationTest.cs deleted file mode 100644 index 3879932184..0000000000 --- a/.Lib9c.Tests/Model/Guild/GuildApplicationTest.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Lib9c.Tests.Model.Guild -{ - using System.Threading.Tasks; - using Lib9c.Tests.Util; - using Nekoyume.TypedAddress; - using VerifyTests; - using VerifyXunit; - using Xunit; - - [UsesVerify] - public class GuildApplicationTest - { - public GuildApplicationTest() - { - VerifierSettings.SortPropertiesAlphabetically(); - } - - [Fact] - public Task Snapshot() - { - var guildApplication = new Nekoyume.Model.Guild.GuildApplication( - new GuildAddress("0xd928ae87311dead490c986c24cc23c37eff892f2")); - - return Verifier.Verify(guildApplication.Bencoded); - } - - [Fact] - public void Serialization() - { - var guildApplication = new Nekoyume.Model.Guild.GuildApplication( - AddressUtil.CreateGuildAddress()); - var newGuildApplication = - new Nekoyume.Model.Guild.GuildApplication(guildApplication.Bencoded); - - Assert.Equal(guildApplication.GuildAddress, newGuildApplication.GuildAddress); - } - } -} diff --git a/.Lib9c.Tests/Model/Guild/GuildParticipantTest.Snapshot.verified.txt b/.Lib9c.Tests/Model/Guild/GuildParticipantTest.Snapshot.verified.txt index f135b22392..d6de105e44 100644 --- a/.Lib9c.Tests/Model/Guild/GuildParticipantTest.Snapshot.verified.txt +++ b/.Lib9c.Tests/Model/Guild/GuildParticipantTest.Snapshot.verified.txt @@ -1,4 +1,4 @@ -[ +[ { EncodingLength: 21, Kind: Text, @@ -7,7 +7,7 @@ { EncodingLength: 3, Kind: Integer, - Value: 1 + Value: 2 }, [ 217, diff --git a/.Lib9c.Tests/Model/Guild/GuildParticipantTest.cs b/.Lib9c.Tests/Model/Guild/GuildParticipantTest.cs index 2a4e868f52..ca96122f0f 100644 --- a/.Lib9c.Tests/Model/Guild/GuildParticipantTest.cs +++ b/.Lib9c.Tests/Model/Guild/GuildParticipantTest.cs @@ -1,7 +1,10 @@ namespace Lib9c.Tests.Model.Guild { using System.Threading.Tasks; + using Lib9c.Tests.Action; using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Mocks; using Nekoyume.TypedAddress; using VerifyTests; using VerifyXunit; @@ -18,8 +21,12 @@ public GuildParticipantTest() [Fact] public Task Snapshot() { + IWorld world = new World(MockUtil.MockModernWorldState); + var repository = new Nekoyume.Model.Guild.GuildRepository(world, new ActionContext()); var guild = new Nekoyume.Model.Guild.GuildParticipant( - new GuildAddress("0xd928ae87311dead490c986c24cc23c37eff892f2")); + new AgentAddress("0xB52B7F66B8464986f56053d82F0D80cA412A6F33"), + new GuildAddress("0xd928ae87311dead490c986c24cc23c37eff892f2"), + repository); return Verifier.Verify(guild.Bencoded); } @@ -27,10 +34,16 @@ public Task Snapshot() [Fact] public void Serialization() { + IWorld world = new World(MockUtil.MockModernWorldState); + var repository = new Nekoyume.Model.Guild.GuildRepository(world, new ActionContext()); var guildParticipant = new Nekoyume.Model.Guild.GuildParticipant( - AddressUtil.CreateGuildAddress()); + AddressUtil.CreateAgentAddress(), + AddressUtil.CreateGuildAddress(), + repository); + repository.SetGuildParticipant(guildParticipant); var newGuildParticipant = - new Nekoyume.Model.Guild.GuildParticipant(guildParticipant.Bencoded); + new Nekoyume.Model.Guild.GuildParticipant( + guildParticipant.Address, guildParticipant.Bencoded, repository); Assert.Equal(guildParticipant.GuildAddress, newGuildParticipant.GuildAddress); } diff --git a/.Lib9c.Tests/Model/Guild/GuildTest.Snapshot.verified.txt b/.Lib9c.Tests/Model/Guild/GuildTest.Snapshot.verified.txt index 0267c82cfe..72cb503bb0 100644 --- a/.Lib9c.Tests/Model/Guild/GuildTest.Snapshot.verified.txt +++ b/.Lib9c.Tests/Model/Guild/GuildTest.Snapshot.verified.txt @@ -1,4 +1,4 @@ -[ +[ { EncodingLength: 8, Kind: Text, @@ -7,7 +7,7 @@ { EncodingLength: 3, Kind: Integer, - Value: 1 + Value: 2 }, [ 217, @@ -30,5 +30,27 @@ 248, 146, 242 + ], + [ + 97, + 75, + 191, + 60, + 231, + 134, + 87, + 182, + 225, + 103, + 60, + 167, + 121, + 151, + 173, + 223, + 81, + 5, + 56, + 223 ] ] diff --git a/.Lib9c.Tests/Model/Guild/GuildTest.cs b/.Lib9c.Tests/Model/Guild/GuildTest.cs index 9fc1ce19e2..890b1a60b0 100644 --- a/.Lib9c.Tests/Model/Guild/GuildTest.cs +++ b/.Lib9c.Tests/Model/Guild/GuildTest.cs @@ -1,7 +1,12 @@ namespace Lib9c.Tests.Model.Guild { using System.Threading.Tasks; + using Lib9c.Tests.Action; using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Mocks; + using Libplanet.Types.Assets; using Nekoyume.TypedAddress; using VerifyTests; using VerifyXunit; @@ -18,8 +23,14 @@ public GuildTest() [Fact] public Task Snapshot() { + IWorld world = new World(MockUtil.MockModernWorldState); + var repository = new Nekoyume.Model.Guild.GuildRepository(world, new ActionContext()); + var validatorAddress = new Address("0x614bBf3cE78657b6E1673cA77997adDf510538Df"); var guild = new Nekoyume.Model.Guild.Guild( - new AgentAddress("0xd928ae87311dead490c986c24cc23c37eff892f2")); + AddressUtil.CreateGuildAddress(), + new AgentAddress("0xd928ae87311dead490c986c24cc23c37eff892f2"), + validatorAddress, + repository); return Verifier.Verify(guild.Bencoded); } @@ -27,9 +38,20 @@ public Task Snapshot() [Fact] public void Serialization() { + IWorld world = new World(MockUtil.MockModernWorldState); + var repository = new Nekoyume.Model.Guild.GuildRepository(world, new ActionContext()); + var guildAddress = AddressUtil.CreateGuildAddress(); + var validatorAddress = new PrivateKey().Address; var guild = new Nekoyume.Model.Guild.Guild( - AddressUtil.CreateAgentAddress()); - var newGuild = new Nekoyume.Model.Guild.Guild(guild.Bencoded); + guildAddress, + AddressUtil.CreateAgentAddress(), + validatorAddress, + repository); + repository.SetGuild(guild); + var newGuild = new Nekoyume.Model.Guild.Guild( + guildAddress, + guild.Bencoded, + repository); Assert.Equal(guild.GuildMasterAddress, newGuild.GuildMasterAddress); } diff --git a/.Lib9c.Tests/Model/Stake/StakeStateV2Test.cs b/.Lib9c.Tests/Model/Stake/StakeStateTest.cs similarity index 80% rename from .Lib9c.Tests/Model/Stake/StakeStateV2Test.cs rename to .Lib9c.Tests/Model/Stake/StakeStateTest.cs index e2470170eb..dfb2e67268 100644 --- a/.Lib9c.Tests/Model/Stake/StakeStateV2Test.cs +++ b/.Lib9c.Tests/Model/Stake/StakeStateTest.cs @@ -6,14 +6,14 @@ namespace Lib9c.Tests.Model.Stake using Nekoyume.Model.State; using Xunit; - public class StakeStateV2Test + public class StakeStateTest { [Fact] public void DeriveAddress() { var agentAddr = new PrivateKey().Address; - var expectedStakeStateAddr = StakeState.DeriveAddress(agentAddr); - Assert.Equal(expectedStakeStateAddr, StakeStateV2.DeriveAddress(agentAddr)); + var expectedStakeStateAddr = LegacyStakeState.DeriveAddress(agentAddr); + Assert.Equal(expectedStakeStateAddr, StakeState.DeriveAddress(agentAddr)); } [Theory] @@ -26,7 +26,7 @@ public void Constructor(long startedBlockIndex, long receivedBlockIndex) Contract.StakeRegularRewardSheetPrefix, 1, 1); - var state = new StakeStateV2(contract, startedBlockIndex, receivedBlockIndex); + var state = new StakeState(contract, startedBlockIndex, receivedBlockIndex); Assert.Equal(contract, state.Contract); Assert.Equal(startedBlockIndex, state.StartedBlockIndex); Assert.Equal(receivedBlockIndex, state.ReceivedBlockIndex); @@ -40,7 +40,7 @@ public void Constructor_Throw_ArgumentNullException( long receivedBlockIndex) { Assert.Throws(() => - new StakeStateV2(null, startedBlockIndex, receivedBlockIndex)); + new StakeState(null, startedBlockIndex, receivedBlockIndex)); } [Theory] @@ -52,7 +52,7 @@ public void Constructor_Throw_ArgumentOutOfRangeException( long receivedBlockIndex) { Assert.Throws(() => - new StakeStateV2(null, startedBlockIndex, receivedBlockIndex)); + new StakeState(null, startedBlockIndex, receivedBlockIndex)); } [Theory] @@ -62,7 +62,7 @@ public void Constructor_Throw_ArgumentOutOfRangeException( [InlineData(long.MaxValue, long.MaxValue)] public void Constructor_With_StakeState(long startedBlockIndex, long? receivedBlockIndex) { - var stakeState = new StakeState( + var stakeState = new LegacyStakeState( new PrivateKey().Address, startedBlockIndex); if (receivedBlockIndex.HasValue) @@ -75,7 +75,7 @@ public void Constructor_With_StakeState(long startedBlockIndex, long? receivedBl Contract.StakeRegularRewardSheetPrefix, 1, 1); - var stakeStateV2 = new StakeStateV2(stakeState, contract); + var stakeStateV2 = new StakeState(stakeState, contract); Assert.Equal(contract, stakeStateV2.Contract); Assert.Equal(stakeState.StartedBlockIndex, stakeStateV2.StartedBlockIndex); Assert.Equal(stakeState.ReceivedBlockIndex, stakeStateV2.ReceivedBlockIndex); @@ -84,14 +84,14 @@ public void Constructor_With_StakeState(long startedBlockIndex, long? receivedBl [Fact] public void Constructor_With_StakeState_Throw_ArgumentNullException() { - var stakeState = new StakeState(new PrivateKey().Address, 0); + var stakeState = new LegacyStakeState(new PrivateKey().Address, 0); var contract = new Contract( Contract.StakeRegularFixedRewardSheetPrefix, Contract.StakeRegularRewardSheetPrefix, 1, 1); - Assert.Throws(() => new StakeStateV2(null, contract)); - Assert.Throws(() => new StakeStateV2(stakeState, null)); + Assert.Throws(() => new StakeState(null, contract)); + Assert.Throws(() => new StakeState(stakeState, null)); } [Theory] @@ -104,9 +104,9 @@ public void Serde(long startedBlockIndex, long receivedBlockIndex) Contract.StakeRegularRewardSheetPrefix, 1, 1); - var state = new StakeStateV2(contract, startedBlockIndex, receivedBlockIndex); + var state = new StakeState(contract, startedBlockIndex, receivedBlockIndex); var ser = state.Serialize(); - var des = new StakeStateV2(ser); + var des = new StakeState(ser); Assert.Equal(state.Contract, des.Contract); Assert.Equal(state.StartedBlockIndex, des.StartedBlockIndex); Assert.Equal(state.ReceivedBlockIndex, des.ReceivedBlockIndex); @@ -122,11 +122,11 @@ public void Compare() Contract.StakeRegularRewardSheetPrefix, 1, 1); - var stateL = new StakeStateV2(contract, 0); - var stateR = new StakeStateV2(contract, 0); + var stateL = new StakeState(contract, 0); + var stateR = new StakeState(contract, 0); Assert.Equal(stateL, stateR); Assert.True(stateL == stateR); - stateR = new StakeStateV2(contract, 1); + stateR = new StakeState(contract, 1); Assert.NotEqual(stateL, stateR); Assert.True(stateL != stateR); } diff --git a/.Lib9c.Tests/Model/Stake/StakeStateUtilsTest.cs b/.Lib9c.Tests/Model/Stake/StakeStateUtilsTest.cs index 37626bee61..a391d01237 100644 --- a/.Lib9c.Tests/Model/Stake/StakeStateUtilsTest.cs +++ b/.Lib9c.Tests/Model/Stake/StakeStateUtilsTest.cs @@ -20,20 +20,20 @@ public class StakeStateUtilsTest public void TryMigrate_Throw_NullReferenceException_When_IAccountDelta_Null() { Assert.Throws(() => - StakeStateUtils.TryMigrate((IWorld)null, default, out _)); + StakeStateUtils.TryMigrateV1ToV2((IWorld)null, default, out _)); } [Fact] public void TryMigrate_Return_False_When_IValue_Null() { - Assert.False(StakeStateUtils.TryMigrate((IValue)null, default, out _)); + Assert.False(StakeStateUtils.TryMigrateV1ToV2((IValue)null, default, out _)); } [Fact] public void TryMigrate_Return_False_When_Staking_State_Null() { var state = new World(MockUtil.MockModernWorldState); - Assert.False(StakeStateUtils.TryMigrate(state, new PrivateKey().Address, out _)); + Assert.False(StakeStateUtils.TryMigrateV1ToV2(state, new PrivateKey().Address, out _)); } [Theory] @@ -113,14 +113,14 @@ public void TryMigrate_Return_True_With_StakeState( Addresses.GameConfig, new GameConfigState(GameConfigSheetFixtures.Default).Serialize()); var stakeAddr = new PrivateKey().Address; - var stakeState = new StakeState(stakeAddr, startedBlockIndex); + var stakeState = new LegacyStakeState(stakeAddr, startedBlockIndex); if (receivedBlockIndex is not null) { stakeState.Claim(receivedBlockIndex.Value); } state = state.SetLegacyState(stakeAddr, stakeState.Serialize()); - Assert.True(StakeStateUtils.TryMigrate(state, stakeAddr, out var stakeStateV2)); + Assert.True(StakeStateUtils.TryMigrateV1ToV2(state, stakeAddr, out var stakeStateV2)); Assert.Equal( stakeRegularFixedRewardSheetTableName, stakeStateV2.Contract.StakeRegularFixedRewardSheetTableName); @@ -149,11 +149,11 @@ public void TryMigrate_Return_True_With_StakeStateV2( stakePolicySheet.Set(StakePolicySheetFixtures.V2); var contract = new Contract(stakePolicySheet); var stakeStateV2 = receivedBlockIndex is null - ? new StakeStateV2(contract, startedBlockIndex) - : new StakeStateV2(contract, receivedBlockIndex.Value); + ? new StakeState(contract, startedBlockIndex) + : new StakeState(contract, receivedBlockIndex.Value); state = state.SetLegacyState(stakeAddr, stakeStateV2.Serialize()); - Assert.True(StakeStateUtils.TryMigrate(state, stakeAddr, out var result)); + Assert.True(StakeStateUtils.TryMigrateV1ToV2(state, stakeAddr, out var result)); Assert.Equal(stakeStateV2.Contract, result.Contract); Assert.Equal(stakeStateV2.StartedBlockIndex, result.StartedBlockIndex); Assert.Equal(stakeStateV2.ReceivedBlockIndex, result.ReceivedBlockIndex); diff --git a/.Lib9c.Tests/Model/State/StakeStateTest.cs b/.Lib9c.Tests/Model/State/LegacyStakeStateTest.cs similarity index 91% rename from .Lib9c.Tests/Model/State/StakeStateTest.cs rename to .Lib9c.Tests/Model/State/LegacyStakeStateTest.cs index 82cb4366ef..85c236baa8 100644 --- a/.Lib9c.Tests/Model/State/StakeStateTest.cs +++ b/.Lib9c.Tests/Model/State/LegacyStakeStateTest.cs @@ -6,21 +6,21 @@ namespace Lib9c.Tests.Model.State using Nekoyume.Action; using Nekoyume.Model.State; using Xunit; - using static Nekoyume.Model.State.StakeState; + using static Nekoyume.Model.State.LegacyStakeState; - public class StakeStateTest + public class LegacyStakeStateTest { [Fact] public void IsClaimable() { - Assert.False(new StakeState( + Assert.False(new LegacyStakeState( default, 0, RewardInterval + 1, LockupInterval, new StakeAchievements()) .IsClaimable(RewardInterval * 2)); - Assert.True(new StakeState( + Assert.True(new LegacyStakeState( default, ActionObsoleteConfig.V100290ObsoleteIndex - 100, ActionObsoleteConfig.V100290ObsoleteIndex - 100 + RewardInterval + 1, @@ -32,10 +32,10 @@ public void IsClaimable() [Fact] public void Serialize() { - var state = new StakeState(default, 100); + var state = new LegacyStakeState(default, 100); var serialized = (Dictionary)state.Serialize(); - var deserialized = new StakeState(serialized); + var deserialized = new LegacyStakeState(serialized); Assert.Equal(state.address, deserialized.address); Assert.Equal(state.StartedBlockIndex, deserialized.StartedBlockIndex); @@ -46,10 +46,10 @@ public void Serialize() [Fact] public void SerializeV2() { - var state = new StakeState(default, 100); + var state = new LegacyStakeState(default, 100); var serialized = (Dictionary)state.SerializeV2(); - var deserialized = new StakeState(serialized); + var deserialized = new LegacyStakeState(serialized); Assert.Equal(state.address, deserialized.address); Assert.Equal(state.StartedBlockIndex, deserialized.StartedBlockIndex); @@ -63,7 +63,7 @@ public void SerializeV2() [InlineData(long.MinValue)] public void Claim(long blockIndex) { - var stakeState = new StakeState(new PrivateKey().Address, 0L); + var stakeState = new LegacyStakeState(new PrivateKey().Address, 0L); stakeState.Claim(blockIndex); Assert.Equal(blockIndex, stakeState.ReceivedBlockIndex); } @@ -78,7 +78,7 @@ public void CalculateAccumulateRuneRewards( long blockIndex, int expected) { - var state = new StakeState(default, startedBlockIndex); + var state = new LegacyStakeState(default, startedBlockIndex); Assert.Equal(expected, state.CalculateAccumulatedRuneRewards(blockIndex)); } @@ -120,7 +120,7 @@ public void GetRewardStepV1( long? rewardStartBlockIndex, int expectedStep) { - var stakeState = new StakeState( + var stakeState = new LegacyStakeState( new PrivateKey().Address, startedBlockIndex); stakeState.Claim(receivedBlockIndex); @@ -166,7 +166,7 @@ public void GetRewardStep( long? rewardStartBlockIndex, int expectedStep) { - var stakeState = new StakeState( + var stakeState = new LegacyStakeState( new PrivateKey().Address, startedBlockIndex); stakeState.Claim(receivedBlockIndex); diff --git a/.Lib9c.Tests/Policy/BlockPolicyTest.cs b/.Lib9c.Tests/Policy/BlockPolicyTest.cs index 7283abe1c1..43e0da19a8 100644 --- a/.Lib9c.Tests/Policy/BlockPolicyTest.cs +++ b/.Lib9c.Tests/Policy/BlockPolicyTest.cs @@ -24,24 +24,20 @@ namespace Lib9c.Tests using Nekoyume; using Nekoyume.Action; using Nekoyume.Action.Loader; + using Nekoyume.Action.ValidatorDelegation; using Nekoyume.Blockchain.Policy; using Nekoyume.Model; using Nekoyume.Model.State; - using Nekoyume.Module; + using Nekoyume.ValidatorDelegation; using Xunit; public class BlockPolicyTest { private readonly PrivateKey _privateKey; - private readonly Currency _currency; public BlockPolicyTest() { _privateKey = new PrivateKey(); -#pragma warning disable CS0618 - // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 - _currency = Currency.Legacy("NCG", 2, _privateKey.Address); -#pragma warning restore CS0618 } [Fact] @@ -70,10 +66,10 @@ public void ValidateNextBlockTx_Mead() }, }; Block genesis = MakeGenesisBlock( + new ValidatorSet( + new List { new (adminPrivateKey.PublicKey, 10_000_000_000_000_000_000) }), adminAddress, ImmutableHashSet
.Empty, - initialValidators: new Dictionary - { { adminPrivateKey.PublicKey, BigInteger.One } }, actionBases: new[] { mint, mint2 }, privateKey: adminPrivateKey ); @@ -94,7 +90,12 @@ public void ValidateNextBlockTx_Mead() ); Block block = blockChain.ProposeBlock(adminPrivateKey); - blockChain.Append(block, GenerateBlockCommit(block, adminPrivateKey)); + blockChain.Append( + block, + GenerateBlockCommit( + block, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey })); Assert.Equal( 1 * Currencies.Mead, @@ -163,10 +164,10 @@ public void BlockCommitFromNonValidator() IBlockPolicy policy = blockPolicySource.GetPolicy(null, null, null, null); IStagePolicy stagePolicy = new VolatileStagePolicy(); Block genesis = MakeGenesisBlock( + new ValidatorSet( + new List { new (adminPrivateKey.PublicKey, 10_000_000_000_000_000_000) }), adminAddress, - ImmutableHashSet.Create(adminAddress), - initialValidators: new Dictionary - { { adminPrivateKey.PublicKey, BigInteger.One } } + ImmutableHashSet.Create(adminAddress) ); using var store = new DefaultStore(null); using var stateStore = new TrieStateStore(new DefaultKeyValueStore(null)); @@ -184,8 +185,26 @@ public void BlockCommitFromNonValidator() renderers: new[] { new BlockRenderer() } ); Block block1 = blockChain.ProposeBlock(adminPrivateKey); + var invalidBlockCommit = new BlockCommit( + block1.Index, + 0, + block1.Hash, + new[] + { + new VoteMetadata( + block1.Index, + 0, + block1.Hash, + DateTimeOffset.UtcNow, + nonValidator.PublicKey, + 1000, + VoteFlag.PreCommit + ).Sign(nonValidator), + }.ToImmutableArray()); Assert.Throws( - () => blockChain.Append(block1, GenerateBlockCommit(block1, nonValidator))); + () => blockChain.Append( + block1, + invalidBlockCommit)); } [Fact] @@ -208,6 +227,8 @@ public void MustNotIncludeBlockActionAtTransaction() maxTransactionsPerSignerPerBlockPolicy: null); IStagePolicy stagePolicy = new VolatileStagePolicy(); Block genesis = MakeGenesisBlock( + new ValidatorSet( + new List { new (adminPrivateKey.PublicKey, 10_000_000_000_000_000_000) }), adminAddress, ImmutableHashSet.Create(adminAddress), new AuthorizedMinersState( @@ -241,7 +262,7 @@ public void MustNotIncludeBlockActionAtTransaction() } [Fact] - public void EarnMiningGoldWhenSuccessMining() + public void EarnMiningMeadWhenSuccessMining() { var adminPrivateKey = new PrivateKey(); var adminAddress = adminPrivateKey.Address; @@ -260,6 +281,8 @@ public void EarnMiningGoldWhenSuccessMining() maxTransactionsPerSignerPerBlockPolicy: null); IStagePolicy stagePolicy = new VolatileStagePolicy(); Block genesis = MakeGenesisBlock( + new ValidatorSet( + new List { new (adminPrivateKey.PublicKey, 10_000_000_000_000_000_000) }), adminAddress, ImmutableHashSet.Create(adminAddress), new AuthorizedMinersState( @@ -267,7 +290,6 @@ public void EarnMiningGoldWhenSuccessMining() 5, 10 ), - new Dictionary { { adminPrivateKey.PublicKey, BigInteger.One } }, pendingActivations: new[] { ps } ); @@ -293,12 +315,92 @@ public void EarnMiningGoldWhenSuccessMining() ); Block block = blockChain.ProposeBlock(adminPrivateKey); - blockChain.Append(block, GenerateBlockCommit(block, adminPrivateKey)); - FungibleAssetValue actualBalance = blockChain + BigInteger power = blockChain.GetNextWorldState().GetValidatorSet().GetValidator(adminPrivateKey.PublicKey).Power; + BlockCommit commit = GenerateBlockCommit( + block, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey }); + // Since it's a block right after the Genesis, the reward is 0. + blockChain.Append(block, commit); + + var mintAmount = 10 * Currencies.Mead; + var mint = new PrepareRewardAssets + { + RewardPoolAddress = adminAddress, + Assets = new List + { + mintAmount, + }, + }; + + blockChain.MakeTransaction( + adminPrivateKey, + new ActionBase[] { mint, } + ); + block = blockChain.ProposeBlock(adminPrivateKey, commit); + power = blockChain.GetNextWorldState().GetValidatorSet().GetValidator(adminPrivateKey.PublicKey).Power; + commit = GenerateBlockCommit( + block, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey }); + // First Reward : Proposer base reward 5 * 0.01, proposer bonus reward 5 * 0.04, Commission 4.75 * 0.1 + // Total 10 + 0.05 + 0.2 + 0.475 = 10.725 + blockChain.Append(block, commit); + + var rewardCurrency = Currencies.Mead; + var actualBalance = blockChain + .GetNextWorldState() + .GetBalance(adminAddress, rewardCurrency); + var expectedBalance = mintAmount + new FungibleAssetValue(rewardCurrency, 0, 725000000000000000); + Assert.Equal(expectedBalance, actualBalance); + + var ssss = blockChain + .GetNextWorldState() + .GetBalance(Addresses.RewardPool, rewardCurrency); + + // After claimed, mead have to be used? + blockChain.MakeTransaction( + adminPrivateKey, + new ActionBase[] { new ClaimValidatorRewardSelf(), }, + gasLimit: 1, + maxGasPrice: Currencies.Mead * 1 + ); + + block = blockChain.ProposeBlock(adminPrivateKey, commit); + power = blockChain.GetNextWorldState().GetValidatorSet().GetValidator(adminPrivateKey.PublicKey).Power; + commit = GenerateBlockCommit( + block, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey }); + // First + Second Reward : Total reward of two blocks : 10 * 2 = 20 + // Base reward: 0.05 + 0.2 + 0.475 = 0.725 + // Total reward: 4.275 + 4.275 (two blocks) + // Used gas: 1 + // Total 10.725 + 0.725 + 4.275 + 4.275 - 1 = 19 + blockChain.Append(block, commit); + + actualBalance = blockChain + .GetNextWorldState() + .GetBalance(adminAddress, rewardCurrency); + expectedBalance = rewardCurrency * 19; + Assert.Equal(expectedBalance, actualBalance); + + block = blockChain.ProposeBlock(adminPrivateKey, commit); + power = blockChain.GetNextWorldState().GetValidatorSet().GetValidator(adminPrivateKey.PublicKey).Power; + commit = GenerateBlockCommit( + block, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey }); + // Mining reward: 5 + 1 / 2 = 5.5 + // Proposer base reward 5.5 * 0.01, proposer bonus reward 5.5 * 0.04, Commission (5.5 - 0.275) * 0.1 + // Base reward: (5.5 * 0.01 + 5.5 * 0.04) + (5.5 - 0.275) * 0.1 = 0.7975 + // Total 19 + 0.7975 = 19.7975 + blockChain.Append(block, commit); + actualBalance = blockChain .GetNextWorldState() - .GetBalance(adminAddress, _currency); - FungibleAssetValue expectedBalance = new FungibleAssetValue(_currency, 10, 0); - Assert.True(expectedBalance.Equals(actualBalance)); + .GetBalance(adminAddress, rewardCurrency); + expectedBalance = new FungibleAssetValue(rewardCurrency, 19, 797500000000000000); + Assert.Equal(expectedBalance, actualBalance); } [Fact] @@ -317,10 +419,10 @@ public void ValidateNextBlockWithManyTransactions() IStagePolicy stagePolicy = new VolatileStagePolicy(); Block genesis = MakeGenesisBlock( + validators: new ValidatorSet( + new List { new (adminPublicKey, 10_000_000_000_000_000_000) }), adminPublicKey.Address, - ImmutableHashSet
.Empty, - initialValidators: new Dictionary - { { adminPrivateKey.PublicKey, BigInteger.One } }); + ImmutableHashSet
.Empty); using var store = new DefaultStore(null); var stateStore = new TrieStateStore(new MemoryKeyValueStore()); @@ -373,10 +475,17 @@ List GenerateTransactions(int count) evidence: evs).Propose(); var stateRootHash = blockChain.DetermineNextBlockStateRootHash(blockChain.Tip, out _); Block block1 = EvaluateAndSign(stateRootHash, preEvalBlock1, adminPrivateKey); - blockChain.Append(block1, GenerateBlockCommit(block1, adminPrivateKey)); + blockChain.Append(block1, GenerateBlockCommit( + block1, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey })); Assert.Equal(2, blockChain.Count); Assert.True(blockChain.ContainsBlock(block1.Hash)); txs = GenerateTransactions(10).OrderBy(tx => tx.Id).ToList(); + var blockCommit = GenerateBlockCommit( + blockChain.Tip, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey }); PreEvaluationBlock preEvalBlock2 = new BlockContent( new BlockMetadata( index: 2, @@ -384,16 +493,25 @@ List GenerateTransactions(int count) publicKey: adminPublicKey, previousHash: blockChain.Tip.Hash, txHash: BlockContent.DeriveTxHash(txs), - lastCommit: GenerateBlockCommit(blockChain.Tip, adminPrivateKey), + lastCommit: blockCommit, evidenceHash: BlockContent.DeriveEvidenceHash(evs)), transactions: txs, evidence: evs).Propose(); stateRootHash = blockChain.DetermineNextBlockStateRootHash(blockChain.Tip, out _); Block block2 = EvaluateAndSign(stateRootHash, preEvalBlock2, adminPrivateKey); - blockChain.Append(block2, GenerateBlockCommit(block2, adminPrivateKey)); + blockChain.Append( + block2, + GenerateBlockCommit( + block2, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey })); Assert.Equal(3, blockChain.Count); Assert.True(blockChain.ContainsBlock(block2.Hash)); txs = GenerateTransactions(11).OrderBy(tx => tx.Id).ToList(); + blockCommit = GenerateBlockCommit( + blockChain.Tip, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey }); PreEvaluationBlock preEvalBlock3 = new BlockContent( new BlockMetadata( index: 3, @@ -401,14 +519,19 @@ List GenerateTransactions(int count) publicKey: adminPublicKey, previousHash: blockChain.Tip.Hash, txHash: BlockContent.DeriveTxHash(txs), - lastCommit: GenerateBlockCommit(blockChain.Tip, adminPrivateKey), + lastCommit: blockCommit, evidenceHash: BlockContent.DeriveEvidenceHash(evs)), transactions: txs, evidence: evs).Propose(); stateRootHash = blockChain.DetermineNextBlockStateRootHash(blockChain.Tip, out _); Block block3 = EvaluateAndSign(stateRootHash, preEvalBlock3, adminPrivateKey); Assert.Throws( - () => blockChain.Append(block3, GenerateBlockCommit(block3, adminPrivateKey))); + () => blockChain.Append( + block3, + GenerateBlockCommit( + block3, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey }))); Assert.Equal(3, blockChain.Count); Assert.False(blockChain.ContainsBlock(block3.Hash)); } @@ -429,12 +552,13 @@ public void ValidateNextBlockWithManyTransactionsPerSigner() .Default .Add(new SpannedSubPolicy(2, null, null, 5))); IStagePolicy stagePolicy = new VolatileStagePolicy(); + var validatorSet = new ValidatorSet( + new List { new (adminPrivateKey.PublicKey, 10_000_000_000_000_000_000) }); Block genesis = MakeGenesisBlock( + validatorSet, adminPublicKey.Address, - ImmutableHashSet
.Empty, - initialValidators: new Dictionary - { { adminPrivateKey.PublicKey, BigInteger.One } }); + ImmutableHashSet
.Empty); using var store = new DefaultStore(null); var stateStore = new TrieStateStore(new MemoryKeyValueStore()); @@ -489,11 +613,20 @@ List GenerateTransactions(int count) Block block1 = EvaluateAndSign(stateRootHash, preEvalBlock1, adminPrivateKey); // Should be fine since policy hasn't kicked in yet. - blockChain.Append(block1, GenerateBlockCommit(block1, adminPrivateKey)); + blockChain.Append( + block1, + GenerateBlockCommit( + block1, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey })); Assert.Equal(2, blockChain.Count); Assert.True(blockChain.ContainsBlock(block1.Hash)); txs = GenerateTransactions(10).OrderBy(tx => tx.Id).ToList(); + var blockCommit = GenerateBlockCommit( + blockChain.Tip, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey }); PreEvaluationBlock preEvalBlock2 = new BlockContent( new BlockMetadata( index: 2, @@ -501,7 +634,7 @@ List GenerateTransactions(int count) publicKey: adminPublicKey, previousHash: blockChain.Tip.Hash, txHash: BlockContent.DeriveTxHash(txs), - lastCommit: GenerateBlockCommit(blockChain.Tip, adminPrivateKey), + lastCommit: blockCommit, evidenceHash: BlockContent.DeriveEvidenceHash(evs)), transactions: txs, evidence: evs).Propose(); @@ -510,7 +643,12 @@ List GenerateTransactions(int count) // Subpolicy kicks in. Assert.Throws( - () => blockChain.Append(block2, GenerateBlockCommit(block2, adminPrivateKey))); + () => blockChain.Append( + block2, + GenerateBlockCommit( + block2, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey }))); Assert.Equal(2, blockChain.Count); Assert.False(blockChain.ContainsBlock(block2.Hash)); // Since failed, roll back nonce. @@ -518,6 +656,10 @@ List GenerateTransactions(int count) // Limit should also pass. txs = GenerateTransactions(5).OrderBy(tx => tx.Id).ToList(); + blockCommit = GenerateBlockCommit( + blockChain.Tip, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey }); PreEvaluationBlock preEvalBlock3 = new BlockContent( new BlockMetadata( index: 2, @@ -525,40 +667,45 @@ List GenerateTransactions(int count) publicKey: adminPublicKey, previousHash: blockChain.Tip.Hash, txHash: BlockContent.DeriveTxHash(txs), - lastCommit: GenerateBlockCommit(blockChain.Tip, adminPrivateKey), + lastCommit: blockCommit, evidenceHash: BlockContent.DeriveEvidenceHash(evs)), transactions: txs, evidence: evs).Propose(); Block block3 = EvaluateAndSign(stateRootHash, preEvalBlock3, adminPrivateKey); - blockChain.Append(block3, GenerateBlockCommit(block3, adminPrivateKey)); + blockChain.Append( + block3, + GenerateBlockCommit( + block3, + blockChain.GetNextWorldState().GetValidatorSet(), + new PrivateKey[] { adminPrivateKey })); Assert.Equal(3, blockChain.Count); Assert.True(blockChain.ContainsBlock(block3.Hash)); } - private BlockCommit GenerateBlockCommit(Block block, PrivateKey key) + private BlockCommit GenerateBlockCommit( + Block block, ValidatorSet validatorSet, IEnumerable validatorPrivateKeys) { - PrivateKey privateKey = key; return block.Index != 0 ? new BlockCommit( block.Index, 0, block.Hash, - ImmutableArray.Empty.Add(new VoteMetadata( + validatorPrivateKeys.Select(k => new VoteMetadata( block.Index, 0, block.Hash, DateTimeOffset.UtcNow, - privateKey.PublicKey, - null, - VoteFlag.PreCommit).Sign(privateKey))) + k.PublicKey, + validatorSet.GetValidator(k.PublicKey).Power, + VoteFlag.PreCommit).Sign(k)).ToImmutableArray()) : null; } private Block MakeGenesisBlock( + ValidatorSet validators, Address adminAddress, IImmutableSet
activatedAddresses, AuthorizedMinersState authorizedMinersState = null, - Dictionary initialValidators = null, DateTimeOffset? timestamp = null, PendingActivationState[] pendingActivations = null, IEnumerable actionBases = null, @@ -574,13 +721,13 @@ private Block MakeGenesisBlock( var sheets = TableSheetsImporter.ImportSheets(); return BlockHelper.ProposeGenesisBlock( + validators, sheets, new GoldDistribution[0], pendingActivations, new AdminState(adminAddress, 1500000), authorizedMinersState: authorizedMinersState, activatedAccounts: activatedAddresses, - initialValidators: initialValidators, isActivateAdminAddress: false, credits: null, privateKey: privateKey ?? _privateKey, diff --git a/.Lib9c.Tests/PolicyAction/Tx/Begin/AutoJoinGuildTest.cs b/.Lib9c.Tests/PolicyAction/Tx/Begin/AutoJoinGuildTest.cs deleted file mode 100644 index d88fca8fde..0000000000 --- a/.Lib9c.Tests/PolicyAction/Tx/Begin/AutoJoinGuildTest.cs +++ /dev/null @@ -1,106 +0,0 @@ -namespace Lib9c.Tests.PolicyAction.Tx.Begin -{ - using System; - using Bencodex.Types; - using Lib9c.Tests.Action; - using Lib9c.Tests.Util; - using Libplanet.Action.State; - using Libplanet.Mocks; - using Nekoyume; - using Nekoyume.Action; - using Nekoyume.Action.Guild; - using Nekoyume.Extensions; - using Nekoyume.Model.State; - using Nekoyume.Module; - using Nekoyume.Module.Guild; - using Nekoyume.PolicyAction.Tx.Begin; - using Nekoyume.TypedAddress; - using Xunit; - - public class AutoJoinGuildTest - { - [Fact] - public void RunAsPolicyActionOnly() - { - Assert.Throws(() => new AutoJoinGuild().Execute( - new ActionContext - { - IsPolicyAction = false, - })); - } - - [Fact] - public void Execute_When_WithPledgeContract() - { - var guildMasterAddress = GuildConfig.PlanetariumGuildOwner; - var guildAddress = AddressUtil.CreateGuildAddress(); - var agentAddress = AddressUtil.CreateAgentAddress(); - var pledgeAddress = agentAddress.GetPledgeAddress(); - var world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMasterAddress) - .SetLegacyState(pledgeAddress, new List( - MeadConfig.PatronAddress.Serialize(), - true.Serialize(), - RequestPledge.DefaultRefillMead.Serialize())); - - Assert.Null(world.GetJoinedGuild(agentAddress)); - var action = new AutoJoinGuild(); - world = action.Execute(new ActionContext - { - PreviousState = world, - Signer = agentAddress, - IsPolicyAction = true, - }); - - var joinedGuildAddress = Assert.IsType(world.GetJoinedGuild(agentAddress)); - Assert.True(world.TryGetGuild(joinedGuildAddress, out var guild)); - Assert.Equal(GuildConfig.PlanetariumGuildOwner, guild.GuildMasterAddress); - } - - [Fact] - public void Execute_When_WithoutPledgeContract() - { - var guildMasterAddress = GuildConfig.PlanetariumGuildOwner; - var guildAddress = AddressUtil.CreateGuildAddress(); - var agentAddress = AddressUtil.CreateAgentAddress(); - var world = new World(MockUtil.MockModernWorldState) - .MakeGuild(guildAddress, guildMasterAddress) - .JoinGuild(guildAddress, guildMasterAddress); - - Assert.Null(world.GetJoinedGuild(agentAddress)); - var action = new AutoJoinGuild(); - world = action.Execute(new ActionContext - { - PreviousState = world, - Signer = agentAddress, - IsPolicyAction = true, - }); - - Assert.Null(world.GetJoinedGuild(agentAddress)); - } - - [Fact] - public void Execute_When_WithoutGuildYet() - { - var agentAddress = AddressUtil.CreateAgentAddress(); - var pledgeAddress = agentAddress.GetPledgeAddress(); - var world = new World(MockUtil.MockModernWorldState) - .SetLegacyState(pledgeAddress, new List( - MeadConfig.PatronAddress.Serialize(), - true.Serialize(), - RequestPledge.DefaultRefillMead.Serialize())); - - Assert.Null(world.GetJoinedGuild(agentAddress)); - var action = new AutoJoinGuild(); - world = action.Execute(new ActionContext - { - PreviousState = world, - Signer = agentAddress, - IsPolicyAction = true, - }); - - Assert.Null(world.GetJoinedGuild(agentAddress)); - } - } -} diff --git a/.Lib9c.Tests/TestHelper/BlockChainHelper.cs b/.Lib9c.Tests/TestHelper/BlockChainHelper.cs index e119d8df0c..9957d84982 100644 --- a/.Lib9c.Tests/TestHelper/BlockChainHelper.cs +++ b/.Lib9c.Tests/TestHelper/BlockChainHelper.cs @@ -4,6 +4,7 @@ namespace Lib9c.Tests.TestHelper using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; + using System.Numerics; using Lib9c.DevExtensions.Action; using Lib9c.Renderers; using Lib9c.Tests.Action; @@ -17,6 +18,7 @@ namespace Lib9c.Tests.TestHelper using Libplanet.Store.Trie; using Libplanet.Types.Assets; using Libplanet.Types.Blocks; + using Libplanet.Types.Consensus; using Nekoyume; using Nekoyume.Action; using Nekoyume.Action.Loader; @@ -74,6 +76,7 @@ public static Block MakeGenesisBlock( var sheets = TableSheetsImporter.ImportSheets(); return BlockHelper.ProposeGenesisBlock( + new ValidatorSet(new List { new (privateKey.PublicKey, BigInteger.One) }), sheets, new GoldDistribution[0], pendingActivations, diff --git a/.Lib9c.Tests/Util/DelegationUtil.cs b/.Lib9c.Tests/Util/DelegationUtil.cs new file mode 100644 index 0000000000..292fc9759f --- /dev/null +++ b/.Lib9c.Tests/Util/DelegationUtil.cs @@ -0,0 +1,113 @@ +namespace Lib9c.Tests.Util +{ + using System; + using Lib9c.Tests.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Action.Guild; + using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.Model.Stake; + using Nekoyume.TableData.Stake; + + public static class DelegationUtil + { + public static IWorld MintGuildGold( + IWorld world, Address address, FungibleAssetValue amount, long blockHeight) + { + if (!amount.Currency.Equals(Currencies.GuildGold)) + { + throw new ArgumentException( + $"The currency of the amount must be {Currencies.GuildGold}.", + nameof(amount) + ); + } + + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + }; + var poolAddress = StakeState.DeriveAddress(address); + return world.MintAsset(actionContext, poolAddress, amount); + } + + public static IWorld PromoteValidator( + IWorld world, + PublicKey validatorPublicKey, + FungibleAssetValue amount, + long blockHeight) + { + var promoteValidator = new PromoteValidator(validatorPublicKey, amount); + + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorPublicKey.Address, + BlockIndex = blockHeight, + }; + return promoteValidator.ExecutePublic(actionContext); + } + + public static IWorld MakeGuild( + IWorld world, Address guildMasterAddress, Address validatorAddress, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Signer = guildMasterAddress, + RandomSeed = Random.Shared.Next(), + }; + var makeGuild = new MakeGuild(validatorAddress); + return makeGuild.ExecutePublic(actionContext); + } + + public static FungibleAssetValue GetGuildCoinFromNCG(FungibleAssetValue balance) + { + return FungibleAssetValue.Parse(Currencies.GuildGold, balance.GetQuantityString(true)); + } + + public static IWorld EnsureValidatorPromotionReady( + IWorld world, PublicKey validatorPublicKey, long blockHeight) + { + world = MintGuildGold(world, validatorPublicKey.Address, Currencies.GuildGold * 10, blockHeight); + world = PromoteValidator(world, validatorPublicKey, Currencies.GuildGold * 10, blockHeight); + return world; + } + + public static IWorld EnsureGuildParticipentIsStaked( + IWorld world, + Address agentAddress, + FungibleAssetValue ncg, + StakePolicySheet stakePolicySheet, + long blockHeight) + { + return world; + } + + public static IWorld EnsureStakeReleased( + IWorld world, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + // TODO : [GuildMigration] Revive below code when the migration is done. + // var actionContext = new ActionContext + // { + // PreviousState = world, + // BlockIndex = blockHeight, + // }; + // var releaseValidatorUnbondings = new ReleaseValidatorUnbondings(); + // return releaseValidatorUnbondings.Execute(actionContext); + return world; + } + } +} diff --git a/.Libplanet b/.Libplanet index 46cff67d3e..2740bf1c24 160000 --- a/.Libplanet +++ b/.Libplanet @@ -1 +1 @@ -Subproject commit 46cff67d3e5e0be88e23aa0924e4b5f88e20c672 +Subproject commit 2740bf1c2461d547532796edfd310f7fe71677f7 diff --git a/@planetarium/lib9c/src/actions/make_guild.ts b/@planetarium/lib9c/src/actions/make_guild.ts index ca0ffd097e..e963fcb825 100644 --- a/@planetarium/lib9c/src/actions/make_guild.ts +++ b/@planetarium/lib9c/src/actions/make_guild.ts @@ -1,10 +1,27 @@ -import type { Value } from "@planetarium/bencodex"; +import type { Address } from "@planetarium/account"; +import { BencodexDictionary, type Value } from "@planetarium/bencodex"; import { PolymorphicAction } from "./common.js"; +export type MakeGuildArgs = { + validatorAddress: Address; +}; + export class MakeGuild extends PolymorphicAction { protected readonly type_id: string = "make_guild"; + private readonly validatorAddress: Address; + + constructor({ validatorAddress }: MakeGuildArgs) { + super(); + + this.validatorAddress = validatorAddress; + } + protected plain_value(): Value { - return null; + const validatorAddressKey = "va" as const; + + return new BencodexDictionary([ + [validatorAddressKey, this.validatorAddress.toBytes()], + ]); } } diff --git a/@planetarium/lib9c/src/actions/migrate_pledge_to_guild.ts b/@planetarium/lib9c/src/actions/migrate_delegation.ts similarity index 65% rename from @planetarium/lib9c/src/actions/migrate_pledge_to_guild.ts rename to @planetarium/lib9c/src/actions/migrate_delegation.ts index 5185cd04cc..4eec55ee3b 100644 --- a/@planetarium/lib9c/src/actions/migrate_pledge_to_guild.ts +++ b/@planetarium/lib9c/src/actions/migrate_delegation.ts @@ -2,16 +2,16 @@ import type { Address } from "@planetarium/account"; import { BencodexDictionary, type Value } from "@planetarium/bencodex"; import { PolymorphicAction } from "./common.js"; -export type MigratePledgeToGuildArgs = { +export type MigrateDelegationArgs = { target: Address; }; -export class MigratePledgeToGuild extends PolymorphicAction { - protected readonly type_id: string = "migrate_pledge_to_guild"; +export class MigrateDelegation extends PolymorphicAction { + protected readonly type_id: string = "migrate_delegation"; private readonly target: Address; - constructor({ target }: MigratePledgeToGuildArgs) { + constructor({ target }: MigrateDelegationArgs) { super(); this.target = target; diff --git a/@planetarium/lib9c/src/actions/migrate_planetarium_guild.ts b/@planetarium/lib9c/src/actions/migrate_planetarium_guild.ts new file mode 100644 index 0000000000..f8a772c07e --- /dev/null +++ b/@planetarium/lib9c/src/actions/migrate_planetarium_guild.ts @@ -0,0 +1,10 @@ +import type { Value } from "@planetarium/bencodex"; +import { PolymorphicAction } from "./common.js"; + +export class MigratePlanetariumGuild extends PolymorphicAction { + protected readonly type_id: string = "migrate_planetarium_guild"; + + protected plain_value(): Value { + return null; + } +} diff --git a/@planetarium/lib9c/src/index.ts b/@planetarium/lib9c/src/index.ts index bf11110b53..80c336a447 100644 --- a/@planetarium/lib9c/src/index.ts +++ b/@planetarium/lib9c/src/index.ts @@ -53,7 +53,11 @@ export { type ApprovePledgeArgs, } from "./actions/approve_pledge.js"; export { - MigratePledgeToGuild, - type MigratePledgeToGuildArgs, -} from "./actions/migrate_pledge_to_guild.js"; -export { MakeGuild } from "./actions/make_guild.js"; + MakeGuild, + type MakeGuildArgs, +} from "./actions/make_guild.js"; +export { MigratePlanetariumGuild } from "./actions/migrate_planetarium_guild.js"; +export { + MigrateDelegation, + type MigrateDelegationArgs, +} from "./actions/migrate_delegation.js"; diff --git a/@planetarium/lib9c/tests/actions/fixtures.ts b/@planetarium/lib9c/tests/actions/fixtures.ts index 689399033d..d3622b7474 100644 --- a/@planetarium/lib9c/tests/actions/fixtures.ts +++ b/@planetarium/lib9c/tests/actions/fixtures.ts @@ -17,3 +17,7 @@ export const fungibleIdA = HashDigest.fromHex( export const patronAddress = Address.fromHex( "0xc64c7cBf29BF062acC26024D5b9D1648E8f8D2e1", ); + +export const validatorAddress = Address.fromHex( + "0xc2Ef377d9F526E4b480518863b94d79FCEABB2e1", +); diff --git a/@planetarium/lib9c/tests/actions/make_guild.test.ts b/@planetarium/lib9c/tests/actions/make_guild.test.ts index 90eaf2830e..ba071be146 100644 --- a/@planetarium/lib9c/tests/actions/make_guild.test.ts +++ b/@planetarium/lib9c/tests/actions/make_guild.test.ts @@ -1,7 +1,12 @@ import { describe } from "vitest"; import { MakeGuild } from "../../src/index.js"; import { runTests } from "./common.js"; +import { validatorAddress } from "./fixtures.js"; describe("MakeGuild", () => { - runTests("valid case", [new MakeGuild()]); + runTests("valid case", [ + new MakeGuild({ + validatorAddress: validatorAddress, + }), + ]); }); diff --git a/@planetarium/lib9c/tests/actions/migrate_delegation.test.ts b/@planetarium/lib9c/tests/actions/migrate_delegation.test.ts new file mode 100644 index 0000000000..49ab47e9bb --- /dev/null +++ b/@planetarium/lib9c/tests/actions/migrate_delegation.test.ts @@ -0,0 +1,12 @@ +import { describe } from "vitest"; +import { MigrateDelegation } from "../../src/index.js"; +import { runTests } from "./common.js"; +import { agentAddress } from "./fixtures.js"; + +describe("MigrateDelegation", () => { + runTests("valid case", [ + new MigrateDelegation({ + target: agentAddress, + }), + ]); +}); diff --git a/@planetarium/lib9c/tests/actions/migrate_planetarium_guild.test.ts b/@planetarium/lib9c/tests/actions/migrate_planetarium_guild.test.ts new file mode 100644 index 0000000000..67d0e243cb --- /dev/null +++ b/@planetarium/lib9c/tests/actions/migrate_planetarium_guild.test.ts @@ -0,0 +1,7 @@ +import { describe } from "vitest"; +import { MigratePlanetariumGuild } from "../../src/index.js"; +import { runTests } from "./common.js"; + +describe("MigratePlanetariumGuild", () => { + runTests("valid case", [new MigratePlanetariumGuild()]); +}); diff --git a/@planetarium/lib9c/tests/actions/migrate_pledge_to_guild.test.ts b/@planetarium/lib9c/tests/actions/migrate_pledge_to_guild.test.ts deleted file mode 100644 index 7a9bd383da..0000000000 --- a/@planetarium/lib9c/tests/actions/migrate_pledge_to_guild.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe } from "vitest"; -import { MigratePledgeToGuild } from "../../src/index.js"; -import { runTests } from "./common.js"; -import { agentAddress } from "./fixtures.js"; - -describe("MigratePledgeToGuild", () => { - describe("odin", () => { - runTests("valid case", [ - new MigratePledgeToGuild({ - target: agentAddress, - }), - ]); - }); - describe("heimdall", () => { - runTests("valid case", [ - new MigratePledgeToGuild({ - target: agentAddress, - }), - ]); - }); -}); diff --git a/Lib9c.DevExtensions/Action/Craft/UnlockCraftAction.cs b/Lib9c.DevExtensions/Action/Craft/UnlockCraftAction.cs index cdb8abace7..eb4632913e 100644 --- a/Lib9c.DevExtensions/Action/Craft/UnlockCraftAction.cs +++ b/Lib9c.DevExtensions/Action/Craft/UnlockCraftAction.cs @@ -24,7 +24,7 @@ public class UnlockCraftAction : GameAction public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; int targetStage; diff --git a/Lib9c.DevExtensions/Action/Craft/UnlockRecipe.cs b/Lib9c.DevExtensions/Action/Craft/UnlockRecipe.cs index 4f0d118a70..c6e6136917 100644 --- a/Lib9c.DevExtensions/Action/Craft/UnlockRecipe.cs +++ b/Lib9c.DevExtensions/Action/Craft/UnlockRecipe.cs @@ -20,7 +20,7 @@ public class UnlockRecipe : GameAction public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var recipeIdList = List.Empty; for (var i = 1; i <= TargetStage; i++) diff --git a/Lib9c.DevExtensions/Action/CreateArenaDummy.cs b/Lib9c.DevExtensions/Action/CreateArenaDummy.cs index 8e85d7892f..18b24d412b 100644 --- a/Lib9c.DevExtensions/Action/CreateArenaDummy.cs +++ b/Lib9c.DevExtensions/Action/CreateArenaDummy.cs @@ -53,7 +53,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; for (var i = 0; i < accountCount; i++) diff --git a/Lib9c.DevExtensions/Action/CreateOrReplaceAvatar.cs b/Lib9c.DevExtensions/Action/CreateOrReplaceAvatar.cs index 8f4b5bf291..f911ca519e 100644 --- a/Lib9c.DevExtensions/Action/CreateOrReplaceAvatar.cs +++ b/Lib9c.DevExtensions/Action/CreateOrReplaceAvatar.cs @@ -365,7 +365,7 @@ public CreateOrReplaceAvatar( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var random = context.GetRandom(); return Execute( context.PreviousState, diff --git a/Lib9c.DevExtensions/Action/CreateTestbed.cs b/Lib9c.DevExtensions/Action/CreateTestbed.cs index cfd770563e..0c191cb1e6 100644 --- a/Lib9c.DevExtensions/Action/CreateTestbed.cs +++ b/Lib9c.DevExtensions/Action/CreateTestbed.cs @@ -70,7 +70,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var sellData = TestbedHelper.LoadData("TestbedSell"); var random = context.GetRandom(); var addedItemInfos = sellData.Items diff --git a/Lib9c.DevExtensions/Action/FaucetCurrency.cs b/Lib9c.DevExtensions/Action/FaucetCurrency.cs index b49ac362a6..dc11b7051f 100644 --- a/Lib9c.DevExtensions/Action/FaucetCurrency.cs +++ b/Lib9c.DevExtensions/Action/FaucetCurrency.cs @@ -23,7 +23,7 @@ public class FaucetCurrency : GameAction, IFaucetCurrency public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; if (FaucetNcg > 0) { diff --git a/Lib9c.DevExtensions/Action/FaucetRune.cs b/Lib9c.DevExtensions/Action/FaucetRune.cs index 8f936ff013..7532495963 100644 --- a/Lib9c.DevExtensions/Action/FaucetRune.cs +++ b/Lib9c.DevExtensions/Action/FaucetRune.cs @@ -25,7 +25,7 @@ public class FaucetRune : GameAction, IFaucetRune public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; if (!(FaucetRuneInfos is null)) { diff --git a/Lib9c.DevExtensions/Action/ManipulateState.cs b/Lib9c.DevExtensions/Action/ManipulateState.cs index 8da52b03f7..8fe0d41391 100644 --- a/Lib9c.DevExtensions/Action/ManipulateState.cs +++ b/Lib9c.DevExtensions/Action/ManipulateState.cs @@ -47,7 +47,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); return Execute(context, context.PreviousState, StateList, BalanceList); } diff --git a/Lib9c.DevExtensions/Action/Stage/ClearStage.cs b/Lib9c.DevExtensions/Action/Stage/ClearStage.cs index 68e2bbdc64..2077686d6c 100644 --- a/Lib9c.DevExtensions/Action/Stage/ClearStage.cs +++ b/Lib9c.DevExtensions/Action/Stage/ClearStage.cs @@ -22,7 +22,7 @@ public class ClearStage : GameAction public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var avatarState = states.GetAvatarState(AvatarAddress); if (avatarState is null || !avatarState.agentAddress.Equals(context.Signer)) diff --git a/Lib9c.Policy/Policy/BlockPolicySource.cs b/Lib9c.Policy/Policy/BlockPolicySource.cs index 67b1a0ca14..5ab6802df1 100644 --- a/Lib9c.Policy/Policy/BlockPolicySource.cs +++ b/Lib9c.Policy/Policy/BlockPolicySource.cs @@ -1,22 +1,16 @@ using System; using System.Collections.Immutable; -using System.Linq; -using Bencodex.Types; -using Lib9c.Abstractions; +using Lib9c; using Libplanet.Action; using Libplanet.Action.Loader; +using Libplanet.Action.State; using Libplanet.Blockchain; using Libplanet.Blockchain.Policies; -using Nekoyume.Action; -using Nekoyume.Action.Loader; -using Nekoyume.Model; -using Nekoyume.Model.State; -using Lib9c; -using Libplanet.Action.State; -using Libplanet.Crypto; using Libplanet.Types.Blocks; using Libplanet.Types.Tx; -using Nekoyume.PolicyAction.Tx.Begin; +using Nekoyume.Action; +using Nekoyume.Action.Loader; +using Nekoyume.Action.ValidatorDelegation; #if UNITY_EDITOR || UNITY_STANDALONE using UniRx; @@ -139,10 +133,23 @@ internal IBlockPolicy GetPolicy( // FIXME: Slight inconsistency due to pre-existing delegate. return new BlockPolicy( policyActionsRegistry: new PolicyActionsRegistry( - beginBlockActions: ImmutableArray.Empty, - endBlockActions: new IAction[] { new RewardGold() }.ToImmutableArray(), - beginTxActions: ImmutableArray.Empty, - endTxActions: ImmutableArray.Empty), + beginBlockActions: new IAction[] { + new SlashValidator(), + new AllocateGuildReward(), + new AllocateReward(), + }.ToImmutableArray(), + endBlockActions: new IAction[] { + new UpdateValidators(), + new RecordProposer(), + new RewardGold(), + new ReleaseValidatorUnbondings(), + }.ToImmutableArray(), + beginTxActions: new IAction[] { + new Mortgage(), + }.ToImmutableArray(), + endTxActions: new IAction[] { + new Reward(), new Refund(), + }.ToImmutableArray()), blockInterval: BlockInterval, validateNextBlockTx: validateNextBlockTx, validateNextBlock: validateNextBlock, @@ -160,7 +167,7 @@ internal IBlockPolicy GetPolicy( Transaction transaction) { // Avoid NRE when genesis block appended - long index = blockChain.Count > 0 ? blockChain.Tip.Index + 1: 0; + long index = blockChain.Count > 0 ? blockChain.Tip.Index + 1 : 0; if (transaction.Actions?.Count > 1) { diff --git a/Lib9c.Utils/BlockHelper.cs b/Lib9c.Utils/BlockHelper.cs index 58c0c58986..b2e58007e1 100644 --- a/Lib9c.Utils/BlockHelper.cs +++ b/Lib9c.Utils/BlockHelper.cs @@ -2,10 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Numerics; -using Bencodex.Types; using Libplanet.Action; -using Libplanet.Action.Sys; using Libplanet.Blockchain; using Libplanet.Crypto; using Libplanet.Store; @@ -25,13 +22,13 @@ namespace Nekoyume public static class BlockHelper { public static Block ProposeGenesisBlock( + ValidatorSet validatorSet, IDictionary tableSheets, GoldDistribution[] goldDistributions, PendingActivationState[] pendingActivationStates, AdminState? adminState = null, AuthorizedMinersState? authorizedMinersState = null, IImmutableSet
? activatedAccounts = null, - Dictionary? initialValidators = null, bool isActivateAdminAddress = false, IEnumerable? credits = null, PrivateKey? privateKey = null, @@ -50,7 +47,6 @@ public static Block ProposeGenesisBlock( redeemCodeListSheet.Set(tableSheets[nameof(RedeemCodeListSheet)]); privateKey ??= new PrivateKey(); - initialValidators ??= new Dictionary(); activatedAccounts ??= ImmutableHashSet
.Empty; #pragma warning disable CS0618 // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 @@ -59,6 +55,7 @@ public static Block ProposeGenesisBlock( var initialStatesAction = new InitializeStates ( + validatorSet: validatorSet, rankingState: new RankingState0(), shopState: new ShopState(), tableSheets: (Dictionary) tableSheets, @@ -80,16 +77,7 @@ public static Block ProposeGenesisBlock( { initialStatesAction, }; - IEnumerable systemActions = new IAction[] - { - new Initialize( - states: ImmutableDictionary.Create(), - validatorSet: new ValidatorSet( - initialValidators.Select(validator => - new Validator(validator.Key, validator.Value)).ToList() - ) - ), - }; + IEnumerable systemActions = new IAction[] { }; if (!(actionBases is null)) { actions.AddRange(actionBases); diff --git a/Lib9c/Action/ActivateCollection.cs b/Lib9c/Action/ActivateCollection.cs index a1fc454ff9..b77f6fb992 100644 --- a/Lib9c/Action/ActivateCollection.cs +++ b/Lib9c/Action/ActivateCollection.cs @@ -25,7 +25,7 @@ public class ActivateCollection : GameAction public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); if (CollectionData.Count > MaxCollectionDataCount) { throw new ArgumentOutOfRangeException( diff --git a/Lib9c/Action/AddRedeemCode.cs b/Lib9c/Action/AddRedeemCode.cs index cd2b5411f5..7df674eac4 100644 --- a/Lib9c/Action/AddRedeemCode.cs +++ b/Lib9c/Action/AddRedeemCode.cs @@ -20,7 +20,7 @@ public class AddRedeemCode : GameAction, IAddRedeemCodeV1 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; CheckPermission(context); diff --git a/Lib9c/Action/AdventureBoss/ClaimAdventureBossReward.cs b/Lib9c/Action/AdventureBoss/ClaimAdventureBossReward.cs index e4515e23a0..cfe7ea39ca 100644 --- a/Lib9c/Action/AdventureBoss/ClaimAdventureBossReward.cs +++ b/Lib9c/Action/AdventureBoss/ClaimAdventureBossReward.cs @@ -44,7 +44,7 @@ IImmutableDictionary plainValue public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var ncg = states.GetGoldCurrency(); diff --git a/Lib9c/Action/AdventureBoss/ExploreAdventureBoss.cs b/Lib9c/Action/AdventureBoss/ExploreAdventureBoss.cs index 8d3f455a35..de52497144 100644 --- a/Lib9c/Action/AdventureBoss/ExploreAdventureBoss.cs +++ b/Lib9c/Action/AdventureBoss/ExploreAdventureBoss.cs @@ -82,7 +82,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { var addressesHex = $"[{context.Signer.ToHex()}, {AvatarAddress.ToHex()}]"; - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; // Validation diff --git a/Lib9c/Action/AdventureBoss/SweepAdventureBoss.cs b/Lib9c/Action/AdventureBoss/SweepAdventureBoss.cs index d3e41ff041..987190d911 100644 --- a/Lib9c/Action/AdventureBoss/SweepAdventureBoss.cs +++ b/Lib9c/Action/AdventureBoss/SweepAdventureBoss.cs @@ -60,7 +60,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); var states = context.PreviousState; diff --git a/Lib9c/Action/AdventureBoss/UnlockFloor.cs b/Lib9c/Action/AdventureBoss/UnlockFloor.cs index e6562ecd9c..85bc7258c7 100644 --- a/Lib9c/Action/AdventureBoss/UnlockFloor.cs +++ b/Lib9c/Action/AdventureBoss/UnlockFloor.cs @@ -48,7 +48,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var currency = states.GetGoldCurrency(); var latestSeason = states.GetLatestAdventureBossSeason(); diff --git a/Lib9c/Action/AdventureBoss/Wanted.cs b/Lib9c/Action/AdventureBoss/Wanted.cs index d0158f29f6..3903375b1e 100644 --- a/Lib9c/Action/AdventureBoss/Wanted.cs +++ b/Lib9c/Action/AdventureBoss/Wanted.cs @@ -48,7 +48,7 @@ IImmutableDictionary plainValue public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var currency = states.GetGoldCurrency(); var gameConfig = states.GetGameConfigState(); diff --git a/Lib9c/Action/ApprovePledge.cs b/Lib9c/Action/ApprovePledge.cs index f5bad206c9..d25271b59e 100644 --- a/Lib9c/Action/ApprovePledge.cs +++ b/Lib9c/Action/ApprovePledge.cs @@ -4,6 +4,7 @@ using Libplanet.Crypto; using Nekoyume.Action.Guild; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; using Nekoyume.Model.State; using Nekoyume.Module; using Nekoyume.Module.Guild; @@ -30,7 +31,7 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); Address signer = context.Signer; var states = context.PreviousState; var contractAddress = signer.GetPledgeAddress(); @@ -49,9 +50,11 @@ public override IWorld Execute(IActionContext context) throw new AlreadyContractedException($"{signer} already contracted."); } - if (PatronAddress == MeadConfig.PatronAddress && states.GetJoinedGuild(GuildConfig.PlanetariumGuildOwner) is { } guildAddress) + var repository = new GuildRepository(states, context); + if (PatronAddress == MeadConfig.PatronAddress + && repository.GetJoinedGuild(GuildConfig.PlanetariumGuildOwner) is { } guildAddress) { - states = states.JoinGuild(guildAddress, context.GetAgentAddress()); + states = repository.JoinGuild(guildAddress, context.GetAgentAddress()).World; } return states.SetLegacyState( diff --git a/Lib9c/Action/AuraSummon.cs b/Lib9c/Action/AuraSummon.cs index f8830b558f..f340df00b0 100644 --- a/Lib9c/Action/AuraSummon.cs +++ b/Lib9c/Action/AuraSummon.cs @@ -9,6 +9,7 @@ using Libplanet.Action.State; using Libplanet.Crypto; using Nekoyume.Action.Exceptions; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Extensions; using Nekoyume.Model.Item; @@ -162,7 +163,7 @@ long blockIndex public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); @@ -224,15 +225,20 @@ public override IWorld Execute(IActionContext context) // Transfer Cost NCG first for fast-fail if (summonRow.CostNcg > 0L) { - var arenaSheet = states.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = - ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } states = states.TransferAsset( context, context.Signer, - feeStoreAddress, + feeAddress, states.GetGoldCurrency() * summonRow.CostNcg * SummonCount ); } diff --git a/Lib9c/Action/BattleArena.cs b/Lib9c/Action/BattleArena.cs index a802db8b61..4615c2e78b 100644 --- a/Lib9c/Action/BattleArena.cs +++ b/Lib9c/Action/BattleArena.cs @@ -7,6 +7,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Battle; using Nekoyume.Exceptions; @@ -95,7 +96,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); ValidateTicket(); var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex( @@ -318,8 +319,6 @@ public override IWorld Execute(IActionContext context) } else { - var arenaAddr = - ArenaHelper.DeriveArenaAddress(roundData.ChampionshipId, roundData.Round); var goldCurrency = states.GetGoldCurrency(); var ticketBalance = ArenaHelper.GetTicketPrice(roundData, myArenaInformation, goldCurrency); @@ -331,8 +330,17 @@ public override IWorld Execute(IActionContext context) } purchasedCountDuringInterval++; + + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + feeAddress = ArenaHelper.DeriveArenaAddress(roundData.ChampionshipId, roundData.Round); + } + states = states - .TransferAsset(context, context.Signer, arenaAddr, ticketBalance) + .TransferAsset(context, context.Signer, feeAddress, ticketBalance) .SetLegacyState(purchasedCountAddr, purchasedCountDuringInterval); } diff --git a/Lib9c/Action/BurnAsset.cs b/Lib9c/Action/BurnAsset.cs index be4cd4b858..9219ac91c4 100644 --- a/Lib9c/Action/BurnAsset.cs +++ b/Lib9c/Action/BurnAsset.cs @@ -31,7 +31,7 @@ public BurnAsset(Address owner, FungibleAssetValue amount, string memo) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IWorld state = context.PreviousState; diff --git a/Lib9c/Action/Buy.cs b/Lib9c/Action/Buy.cs index 9787daf3af..a202e16be0 100644 --- a/Lib9c/Action/Buy.cs +++ b/Lib9c/Action/Buy.cs @@ -10,6 +10,7 @@ using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Types.Assets; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Model.EnumType; using Nekoyume.Model.Mail; @@ -18,6 +19,7 @@ using Nekoyume.TableData; using Serilog; using static Lib9c.SerializeKeys; +using static Nekoyume.TableData.ArenaSheet; namespace Nekoyume.Action { @@ -68,7 +70,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); - states = states.TransferAsset( - context, - context.Signer, - feeStoreAddress, - tax); + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } + + states = states + .TransferAsset(context, context.Signer, feeAddress, tax); // Transfer seller. states = states.TransferAsset( diff --git a/Lib9c/Action/Buy7.cs b/Lib9c/Action/Buy7.cs index 7c22fba70b..5279fc2a32 100644 --- a/Lib9c/Action/Buy7.cs +++ b/Lib9c/Action/Buy7.cs @@ -197,7 +197,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); var tax = product.Price.DivRem(100, out _) * Action.Buy.TaxRate; var taxedPrice = product.Price - tax; // Receipt var receipt = new ProductReceipt(productId, sellerAvatarAddress, buyerAvatarState.address, product.Price, context.BlockIndex); + + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } + states = states .RemoveLegacyState(productAddress) .SetLegacyState(productsStateAddress, productsState.Serialize()) .SetAvatarState(sellerAvatarAddress, sellerAvatarState) .SetLegacyState(ProductReceipt.DeriveAddress(productId), receipt.Serialize()) - .TransferAsset(context, context.Signer, feeStoreAddress, tax) + .TransferAsset(context, context.Signer, feeAddress, tax) .TransferAsset(context, context.Signer, sellerAgentAddress, taxedPrice); return states; @@ -299,13 +308,20 @@ private static IWorld Buy_Order(PurchaseInfo purchaseInfo, IActionContext contex var taxedPrice = order.Price - tax; // Transfer tax. - var arenaSheet = states.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } + states = states.TransferAsset( context, context.Signer, - feeStoreAddress, + feeAddress, tax); // Transfer seller. diff --git a/Lib9c/Action/CancelProductRegistration.cs b/Lib9c/Action/CancelProductRegistration.cs index 0687a81256..9ab27899f8 100644 --- a/Lib9c/Action/CancelProductRegistration.cs +++ b/Lib9c/Action/CancelProductRegistration.cs @@ -29,7 +29,7 @@ public class CancelProductRegistration : GameAction public bool ChargeAp; public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IWorld states = context.PreviousState; if (!ProductInfos.Any()) diff --git a/Lib9c/Action/ChargeActionPoint.cs b/Lib9c/Action/ChargeActionPoint.cs index e38c667ce0..d473c46e57 100644 --- a/Lib9c/Action/ChargeActionPoint.cs +++ b/Lib9c/Action/ChargeActionPoint.cs @@ -33,7 +33,7 @@ public class ChargeActionPoint : GameAction, IChargeActionPointV1 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, avatarAddress); var started = DateTimeOffset.UtcNow; diff --git a/Lib9c/Action/ClaimItems.cs b/Lib9c/Action/ClaimItems.cs index 82825dbee4..4a9cf52196 100644 --- a/Lib9c/Action/ClaimItems.cs +++ b/Lib9c/Action/ClaimItems.cs @@ -83,7 +83,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); if (ClaimData.Count > MaxClaimDataCount) { diff --git a/Lib9c/Action/ClaimRaidReward.cs b/Lib9c/Action/ClaimRaidReward.cs index 9782ee95e6..a8df905997 100644 --- a/Lib9c/Action/ClaimRaidReward.cs +++ b/Lib9c/Action/ClaimRaidReward.cs @@ -35,7 +35,7 @@ public ClaimRaidReward(Address avatarAddress) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IWorld states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); diff --git a/Lib9c/Action/ClaimStakeReward.cs b/Lib9c/Action/ClaimStakeReward.cs index 909e1ac12a..98073aca4a 100644 --- a/Lib9c/Action/ClaimStakeReward.cs +++ b/Lib9c/Action/ClaimStakeReward.cs @@ -54,19 +54,35 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); - var stakeStateAddr = StakeState.DeriveAddress(context.Signer); - if (!states.TryGetStakeStateV2(context.Signer, out var stakeStateV2)) + var stakeStateAddr = LegacyStakeState.DeriveAddress(context.Signer); + if (!states.TryGetStakeState(context.Signer, out var stakeStateV2)) { throw new FailedLoadStateException( ActionTypeText, addressesHex, - typeof(StakeState), + typeof(LegacyStakeState), stakeStateAddr); } + if (stakeStateV2.StateVersion == 2) + { + if (!StakeStateUtils.TryMigrateV2ToV3( + context, + states, + StakeState.DeriveAddress(context.Signer), + stakeStateV2, out var result)) + { + throw new InvalidOperationException( + "Failed to migrate stake state. Unexpected situation."); + } + + states = result.Value.world; + stakeStateV2 = result.Value.newStakeState; + } + if (stakeStateV2.ClaimableBlockIndex > context.BlockIndex) { throw new RequiredBlockIndexException( @@ -148,7 +164,7 @@ public override IWorld Execute(IActionContext context) } // NOTE: update claimed block index. - stakeStateV2 = new StakeStateV2( + stakeStateV2 = new StakeState( stakeStateV2.Contract, stakeStateV2.StartedBlockIndex, context.BlockIndex); diff --git a/Lib9c/Action/ClaimWordBossKillReward.cs b/Lib9c/Action/ClaimWordBossKillReward.cs index 027c78ae39..133cc1c25d 100644 --- a/Lib9c/Action/ClaimWordBossKillReward.cs +++ b/Lib9c/Action/ClaimWordBossKillReward.cs @@ -25,7 +25,7 @@ public class ClaimWordBossKillReward : GameAction, IClaimWordBossKillRewardV1 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IWorld states = context.PreviousState; Dictionary sheets = states.GetSheets(sheetTypes: new [] { diff --git a/Lib9c/Action/CombinationConsumable.cs b/Lib9c/Action/CombinationConsumable.cs index 272ef752f9..3f0cfed609 100644 --- a/Lib9c/Action/CombinationConsumable.cs +++ b/Lib9c/Action/CombinationConsumable.cs @@ -55,7 +55,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary 0L) { - var arenaSheet = states.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + // Transfer tax. + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } states = states.TransferAsset( context, context.Signer, - feeStoreAddress, + feeAddress, states.GetGoldCurrency() * costNcg ); } diff --git a/Lib9c/Action/Coupons/IssueCoupons.cs b/Lib9c/Action/Coupons/IssueCoupons.cs index 06999289c4..6e23b33e27 100644 --- a/Lib9c/Action/Coupons/IssueCoupons.cs +++ b/Lib9c/Action/Coupons/IssueCoupons.cs @@ -30,7 +30,7 @@ public IssueCoupons(IImmutableDictionary rewards, Address recip public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; CheckPermission(context); diff --git a/Lib9c/Action/Coupons/RedeemCoupon.cs b/Lib9c/Action/Coupons/RedeemCoupon.cs index 53d5177663..86875022e7 100644 --- a/Lib9c/Action/Coupons/RedeemCoupon.cs +++ b/Lib9c/Action/Coupons/RedeemCoupon.cs @@ -31,7 +31,7 @@ public RedeemCoupon(Guid couponId, Address avatarAddress) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; if (!states.TryGetAvatarState( diff --git a/Lib9c/Action/Coupons/TransferCoupons.cs b/Lib9c/Action/Coupons/TransferCoupons.cs index f8a57fb872..5623c1f134 100644 --- a/Lib9c/Action/Coupons/TransferCoupons.cs +++ b/Lib9c/Action/Coupons/TransferCoupons.cs @@ -31,7 +31,7 @@ public IImmutableDictionary> CouponsPerRecipient public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var signerWallet = states.GetCouponWallet(context.Signer); var orderedRecipients = CouponsPerRecipient.OrderBy(pair => pair.Key); diff --git a/Lib9c/Action/CreateAvatar.cs b/Lib9c/Action/CreateAvatar.cs index c2a8e7339d..9a4a0fbcf0 100644 --- a/Lib9c/Action/CreateAvatar.cs +++ b/Lib9c/Action/CreateAvatar.cs @@ -76,7 +76,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary states) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); CheckObsolete(ActionObsoleteConfig.V200030ObsoleteIndex, context); CheckPermission(context); var state = context.PreviousState; diff --git a/Lib9c/Action/CreatePledge.cs b/Lib9c/Action/CreatePledge.cs index f4d9a0f80f..cbaa8bc089 100644 --- a/Lib9c/Action/CreatePledge.cs +++ b/Lib9c/Action/CreatePledge.cs @@ -48,7 +48,7 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); CheckPermission(context); var states = context.PreviousState; var mead = Mead * Currencies.Mead; diff --git a/Lib9c/Action/CustomEquipmentCraft/CustomEquipmentCraft.cs b/Lib9c/Action/CustomEquipmentCraft/CustomEquipmentCraft.cs index a52ea9d7e6..9ca9d6e823 100644 --- a/Lib9c/Action/CustomEquipmentCraft/CustomEquipmentCraft.cs +++ b/Lib9c/Action/CustomEquipmentCraft/CustomEquipmentCraft.cs @@ -70,7 +70,7 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; // Validate duplicated slot indices in action diff --git a/Lib9c/Action/DailyReward.cs b/Lib9c/Action/DailyReward.cs index 6fb6ff8b43..46c8d41056 100644 --- a/Lib9c/Action/DailyReward.cs +++ b/Lib9c/Action/DailyReward.cs @@ -32,7 +32,7 @@ public class DailyReward : GameAction, IDailyRewardV1 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, avatarAddress); var started = DateTimeOffset.UtcNow; diff --git a/Lib9c/Action/DailyReward2.cs b/Lib9c/Action/DailyReward2.cs index cc95089a30..ff3b509a36 100644 --- a/Lib9c/Action/DailyReward2.cs +++ b/Lib9c/Action/DailyReward2.cs @@ -29,7 +29,7 @@ public class DailyReward2 : GameAction, IDailyRewardV1 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IActionContext ctx = context; var states = ctx.PreviousState; diff --git a/Lib9c/Action/EndPledge.cs b/Lib9c/Action/EndPledge.cs index c0197214e9..980a1597d8 100644 --- a/Lib9c/Action/EndPledge.cs +++ b/Lib9c/Action/EndPledge.cs @@ -28,7 +28,7 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); Address signer = context.Signer; var states = context.PreviousState; var contractAddress = AgentAddress.GetPledgeAddress(); diff --git a/Lib9c/Action/EventConsumableItemCrafts.cs b/Lib9c/Action/EventConsumableItemCrafts.cs index 675cd168e7..e0961ac1ec 100644 --- a/Lib9c/Action/EventConsumableItemCrafts.cs +++ b/Lib9c/Action/EventConsumableItemCrafts.cs @@ -81,7 +81,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary 0) { + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + feeAddress = Addresses.EventDungeon; + } + states = states.TransferAsset( context, context.Signer, - Addresses.EventDungeon, - cost); + feeAddress, + cost + ); } // NOTE: The number of ticket purchases should be increased diff --git a/Lib9c/Action/EventMaterialItemCrafts.cs b/Lib9c/Action/EventMaterialItemCrafts.cs index 0688ab9361..d8b48ee680 100644 --- a/Lib9c/Action/EventMaterialItemCrafts.cs +++ b/Lib9c/Action/EventMaterialItemCrafts.cs @@ -89,7 +89,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary Dictionary.Empty - .Add("type_id", TypeIdentifier) - .Add("values", Dictionary.Empty - .Add(TargetKey, Target.Bencoded)); - - public override void LoadPlainValue(IValue plainValue) - { - var root = (Dictionary)plainValue; - if (plainValue is not Dictionary || - !root.TryGetValue((Text)"values", out var rawValues) || - rawValues is not Dictionary values || - !values.TryGetValue((Text)TargetKey, out var rawTargetAddress)) - { - throw new InvalidCastException(); - } - - Target = new AgentAddress(rawTargetAddress); - } - - public override IWorld Execute(IActionContext context) - { - context.UseGas(1); - - var world = context.PreviousState; - var signer = context.GetAgentAddress(); - - return world.AcceptGuildApplication(signer, Target); - } - } -} diff --git a/Lib9c/Action/Guild/BanGuildMember.cs b/Lib9c/Action/Guild/BanGuildMember.cs index a0be85aeb3..897a251109 100644 --- a/Lib9c/Action/Guild/BanGuildMember.cs +++ b/Lib9c/Action/Guild/BanGuildMember.cs @@ -3,6 +3,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; using Nekoyume.TypedAddress; @@ -45,19 +46,20 @@ rawValues is not Dictionary values || public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var world = context.PreviousState; + var repository = new GuildRepository(world, context); var signer = context.GetAgentAddress(); - if (world.GetJoinedGuild(signer) is not { } guildAddress) + if (repository.GetJoinedGuild(signer) is not { } guildAddress) { throw new InvalidOperationException("The signer does not have a guild."); } - world = world.Ban(guildAddress, signer, Target); + repository.Ban(guildAddress, signer, Target); - return world; + return repository.World; } } } diff --git a/Lib9c/Action/Guild/ClaimGuildReward.cs b/Lib9c/Action/Guild/ClaimGuildReward.cs new file mode 100644 index 0000000000..7a05ee74fc --- /dev/null +++ b/Lib9c/Action/Guild/ClaimGuildReward.cs @@ -0,0 +1,54 @@ +using System; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Nekoyume.Model.Guild; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Action.Guild +{ + [ActionType(TypeIdentifier)] + public sealed class ClaimGuildReward : ActionBase + { + public const string TypeIdentifier = "claim_guild_reward"; + + public ClaimGuildReward() { } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", Null.Value); + + public override void LoadPlainValue(IValue plainValue) + { + var root = (Dictionary)plainValue; + if (plainValue is not Dictionary || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not Null) + { + throw new InvalidCastException(); + }; + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + var guildRepository = new GuildRepository(world, context); + + var guildParticipant = guildRepository.GetGuildParticipant(context.Signer); + var guild = guildRepository.GetGuild(guildParticipant.GuildAddress); + if (context.Signer != guild.GuildMasterAddress) + { + throw new InvalidOperationException("Signer is not a guild master."); + } + + var repository = new ValidatorRepository(guildRepository); + var validatorDelegatee = repository.GetValidatorDelegatee(guild.ValidatorAddress); + var validatorDelegator = repository.GetValidatorDelegator(guild.Address); + validatorDelegator.ClaimReward(validatorDelegatee, context.BlockIndex); + + return repository.World; + } + } +} diff --git a/Lib9c/Action/Guild/ClaimReward.cs b/Lib9c/Action/Guild/ClaimReward.cs new file mode 100644 index 0000000000..51e45fa034 --- /dev/null +++ b/Lib9c/Action/Guild/ClaimReward.cs @@ -0,0 +1,47 @@ +using System; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Nekoyume.Model.Guild; + +namespace Nekoyume.Action.Guild +{ + [ActionType(TypeIdentifier)] + public sealed class ClaimReward : ActionBase + { + public const string TypeIdentifier = "claim_reward"; + + public ClaimReward() { } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", Null.Value); + + public override void LoadPlainValue(IValue plainValue) + { + var root = (Dictionary)plainValue; + if (plainValue is not Dictionary || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not Null) + { + throw new InvalidCastException(); + } + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + var repository = new GuildRepository(world, context); + + var guildParticipant = repository.GetGuildParticipant(context.Signer); + var guild = repository.GetGuild(guildParticipant.GuildAddress); + var guildDelegatee = repository.GetGuildDelegatee(guild.ValidatorAddress); + var guildDelegator = repository.GetGuildDelegator(context.Signer); + guildDelegator.ClaimReward(guildDelegatee, context.BlockIndex); + + return repository.World; + } + } +} diff --git a/Lib9c/Action/Guild/ApplyGuild.cs b/Lib9c/Action/Guild/JoinGuild.cs similarity index 73% rename from Lib9c/Action/Guild/ApplyGuild.cs rename to Lib9c/Action/Guild/JoinGuild.cs index a9f77212e4..38d4a256ac 100644 --- a/Lib9c/Action/Guild/ApplyGuild.cs +++ b/Lib9c/Action/Guild/JoinGuild.cs @@ -3,6 +3,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; using Nekoyume.TypedAddress; @@ -10,15 +11,15 @@ namespace Nekoyume.Action.Guild { // TODO(GUILD-FEATURE): Enable again when Guild features are enabled. // [ActionType(TypeIdentifier)] - public class ApplyGuild : ActionBase + public class JoinGuild : ActionBase { - public const string TypeIdentifier = "apply_guild"; + public const string TypeIdentifier = "join_guild"; private const string GuildAddressKey = "ga"; - public ApplyGuild() {} + public JoinGuild() {} - public ApplyGuild(GuildAddress guildAddress) + public JoinGuild(GuildAddress guildAddress) { GuildAddress = guildAddress; } @@ -46,14 +47,16 @@ rawValues is not Dictionary values || public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var world = context.PreviousState; - var signer = context.GetAgentAddress(); + var repository = new GuildRepository(world, context); + var target = context.GetAgentAddress(); + var guildAddress = GuildAddress; - // TODO: Do something related with ConsensusPower delegation. + repository.JoinGuild(guildAddress, target); - return world.ApplyGuild(signer, GuildAddress); + return repository.World; } } } diff --git a/Lib9c/Action/Guild/MakeGuild.cs b/Lib9c/Action/Guild/MakeGuild.cs index 90911a131c..a5fac12d97 100644 --- a/Lib9c/Action/Guild/MakeGuild.cs +++ b/Lib9c/Action/Guild/MakeGuild.cs @@ -4,6 +4,7 @@ using Libplanet.Action.State; using Libplanet.Crypto; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; using Nekoyume.TypedAddress; @@ -14,38 +15,62 @@ public class MakeGuild : ActionBase { public const string TypeIdentifier = "make_guild"; + private const string ValidatorAddressKey = "va"; + + public MakeGuild() { } + + public MakeGuild(Address validatorAddress) + { + ValidatorAddress = validatorAddress; + } + + public Address ValidatorAddress { get; private set; } + public override IValue PlainValue => Dictionary.Empty .Add("type_id", TypeIdentifier) - .Add("values", Null.Value); + .Add("values", Dictionary.Empty + .Add(ValidatorAddressKey, ValidatorAddress.Bencoded)); public override void LoadPlainValue(IValue plainValue) { if (plainValue is not Dictionary root || !root.TryGetValue((Text)"values", out var rawValues) || - rawValues is not Null) + rawValues is not Dictionary values || + !values.TryGetValue((Text)ValidatorAddressKey, out var rawValidatorAddress)) { throw new InvalidCastException(); } + + ValidatorAddress = new Address(rawValidatorAddress); } + // TODO: Replace this with ExecutePublic when to deliver features to users. public override IWorld Execute(IActionContext context) { - context.UseGas(1); + var world = ExecutePublic(context); - var world = context.PreviousState; - var random = context.GetRandom(); - - // TODO: Remove this check when to deliver features to users. if (context.Signer != GuildConfig.PlanetariumGuildOwner) { throw new InvalidOperationException( $"This action is not allowed for {context.Signer}."); } + return world; + } + + public IWorld ExecutePublic(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + var random = context.GetRandom(); + + var repository = new GuildRepository(world, context); var guildAddress = new GuildAddress(random.GenerateAddress()); - var signer = context.GetAgentAddress(); + var validatorAddress = ValidatorAddress; - return world.MakeGuild(guildAddress, signer); + repository.MakeGuild(guildAddress, validatorAddress); + return repository.World; } } } diff --git a/Lib9c/Action/Guild/Migration/Controls/GuildMigrationCtrl.cs b/Lib9c/Action/Guild/Migration/Controls/GuildMigrationCtrl.cs deleted file mode 100644 index 721c7635fb..0000000000 --- a/Lib9c/Action/Guild/Migration/Controls/GuildMigrationCtrl.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Bencodex.Types; -using Libplanet.Action.State; -using Nekoyume.Extensions; -using Nekoyume.Model.State; -using Nekoyume.Module; -using Nekoyume.Module.Guild; -using Nekoyume.PolicyAction.Tx.Begin; -using Nekoyume.TypedAddress; -using Serilog; - -namespace Nekoyume.Action.Guild.Migration.Controls -{ - public static class GuildMigrationCtrl - { - /// - /// Migrate the pledge to the guild if the has contracted pledge - /// with Planetarium (). - /// - /// - /// - /// - /// Migration to guild from pledge failed. - public static IWorld MigratePlanetariumPledgeToGuild(IWorld world, AgentAddress target) - { - if (world.GetJoinedGuild(GuildConfig.PlanetariumGuildOwner) is not - { } planetariumGuildAddress) - { - throw new GuildMigrationFailedException("Planetarium guild is not found."); - } - - if (!world.TryGetGuild(planetariumGuildAddress, out var planetariumGuild)) - { - throw new GuildMigrationFailedException("Planetarium guild is not found."); - } - - if (planetariumGuild.GuildMasterAddress != GuildConfig.PlanetariumGuildOwner) - { - throw new GuildMigrationFailedException("Unexpected guild master."); - } - - if (world.GetJoinedGuild(target) is not null) - { - throw new GuildMigrationFailedException("Already joined to other guild."); - } - - var pledgeAddress = target.GetPledgeAddress(); - - // Patron contract structure: - // [0] = PatronAddress - // [1] = IsApproved - // [2] = Mead amount to refill. - if (!world.TryGetLegacyState(pledgeAddress, out List list) || list.Count < 3 || - list[0] is not Binary || list[0].ToAddress() != MeadConfig.PatronAddress || - list[1] is not Boolean approved || !approved) - { - throw new GuildMigrationFailedException("Unexpected pledge structure."); - } - - return world.JoinGuild(planetariumGuildAddress, target); - } - } -} diff --git a/Lib9c/Action/Guild/Migration/LegacyModels/LegacyGuild.cs b/Lib9c/Action/Guild/Migration/LegacyModels/LegacyGuild.cs new file mode 100644 index 0000000000..b494b3d33e --- /dev/null +++ b/Lib9c/Action/Guild/Migration/LegacyModels/LegacyGuild.cs @@ -0,0 +1,61 @@ +using System; +using Bencodex; +using Bencodex.Types; +using Nekoyume.TypedAddress; + +namespace Nekoyume.Action.Guild.Migration.LegacyModels +{ + // TODO: [GuildMigration] Remove this class when the migration is done. + public class LegacyGuild : IEquatable, IBencodable + { + private const string StateTypeName = "guild"; + private const long StateVersion = 1; + + public readonly AgentAddress GuildMasterAddress; + + public LegacyGuild(AgentAddress guildMasterAddress) + { + GuildMasterAddress = guildMasterAddress; + } + + public LegacyGuild(List list) : this(new AgentAddress(list[2])) + { + if (list[0] is not Text text || text != StateTypeName || list[1] is not Integer integer) + { + throw new InvalidCastException(); + } + + if (integer > StateVersion) + { + throw new FailedLoadStateException("Un-deserializable state."); + } + } + + public List Bencoded => List.Empty + .Add(StateTypeName) + .Add(StateVersion) + .Add(GuildMasterAddress.Bencoded); + + IValue IBencodable.Bencoded => Bencoded; + + public bool Equals(LegacyGuild other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return GuildMasterAddress.Equals(other.GuildMasterAddress); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((LegacyGuild)obj); + } + + public override int GetHashCode() + { + return GuildMasterAddress.GetHashCode(); + } + } +} diff --git a/Lib9c/Action/Guild/Migration/LegacyModels/LegacyGuildParticipant.cs b/Lib9c/Action/Guild/Migration/LegacyModels/LegacyGuildParticipant.cs new file mode 100644 index 0000000000..e69b6297aa --- /dev/null +++ b/Lib9c/Action/Guild/Migration/LegacyModels/LegacyGuildParticipant.cs @@ -0,0 +1,61 @@ +using System; +using Bencodex; +using Bencodex.Types; +using Nekoyume.TypedAddress; + +namespace Nekoyume.Action.Guild.Migration.LegacyModels +{ + // TODO: [GuildMigration] Remove this class when the migration is done. + public class LegacyGuildParticipant : IBencodable, IEquatable + { + private const string StateTypeName = "guild_participant"; + private const long StateVersion = 1; + + public readonly GuildAddress GuildAddress; + + public LegacyGuildParticipant(GuildAddress guildAddress) + { + GuildAddress = guildAddress; + } + + public LegacyGuildParticipant(List list) : this(new GuildAddress(list[2])) + { + if (list[0] is not Text text || text != StateTypeName || list[1] is not Integer integer) + { + throw new InvalidCastException(); + } + + if (integer > StateVersion) + { + throw new FailedLoadStateException("Un-deserializable state."); + } + } + + public List Bencoded => List.Empty + .Add(StateTypeName) + .Add(StateVersion) + .Add(GuildAddress.Bencoded); + + IValue IBencodable.Bencoded => Bencoded; + + public bool Equals(LegacyGuildParticipant other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return GuildAddress.Equals(other.GuildAddress); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((LegacyGuildParticipant)obj); + } + + public override int GetHashCode() + { + return GuildAddress.GetHashCode(); + } + } +} diff --git a/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs b/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs new file mode 100644 index 0000000000..4d2724c6f5 --- /dev/null +++ b/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs @@ -0,0 +1,39 @@ +using System; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Extensions; + +namespace Nekoyume.Action.Guild.Migration.LegacyModels +{ + public static class MigrationModule + { + /// + /// An address for delegation height migration. + /// + public static readonly Address DelegationMigrationHeight + = new Address("0000000000000000000000000000000000000000"); + + public static long? GetDelegationMigrationHeight(this IWorldState worldState) + => worldState + .GetAccountState(Addresses.Migration) + .GetState(DelegationMigrationHeight) is Integer height + ? height + : null; + + public static IWorld SetDelegationMigrationHeight(this IWorld world, long height) + { + if (world.GetDelegationMigrationHeight() is long) + { + throw new InvalidOperationException("Cannot overwrite delegation migration index."); + } + + return world + .MutateAccount( + Addresses.Migration, + account => account.SetState( + DelegationMigrationHeight, + (Integer)height)); + } + } +} diff --git a/Lib9c/Action/Guild/Migration/MigrateDelegation.cs b/Lib9c/Action/Guild/Migration/MigrateDelegation.cs new file mode 100644 index 0000000000..65dbec9dde --- /dev/null +++ b/Lib9c/Action/Guild/Migration/MigrateDelegation.cs @@ -0,0 +1,142 @@ +using System; +using Bencodex.Types; +using Lib9c; +using Libplanet.Action.State; +using Libplanet.Action; +using Nekoyume.Model.Guild; +using Nekoyume.TypedAddress; +using Nekoyume.Action.Guild.Migration.LegacyModels; +using Nekoyume.Module.Guild; +using Nekoyume.Model.Stake; +using Nekoyume.Model.State; +using Nekoyume.Module; +using Libplanet.Crypto; + +namespace Nekoyume.Action.Guild.Migration +{ + // TODO: [GuildMigration] Remove this class when the migration is done. + /// + /// An action to migrate guild delegation. + /// + [ActionType(TypeIdentifier)] + public class MigrateDelegation : ActionBase + { + public const string TypeIdentifier = "migrate_delegation"; + + private const string TargetKey = "t"; + + public AgentAddress Target { get; private set; } + + [Obsolete("Don't call in code.", error: false)] + public MigrateDelegation() + { + } + + public MigrateDelegation(AgentAddress target) + { + Target = target; + } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", Dictionary.Empty + .Add(TargetKey, Target.Bencoded)); + + public override void LoadPlainValue(IValue plainValue) + { + if (plainValue is not Dictionary root || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not Dictionary values || + !values.TryGetValue((Text)TargetKey, out var rawTarget) || + rawTarget is not Binary target) + { + throw new InvalidCastException(); + } + + Target = new AgentAddress(target); + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + + // Migrate stake state from v2 to v3 (Mint guild gold for staking) + var stakeStateAddr = LegacyStakeState.DeriveAddress(Target); + if (world.TryGetStakeState(Target, out var stakeState) + && stakeState.StateVersion == 2) + { + if (!StakeStateUtils.TryMigrateV2ToV3( + context, + world, + StakeState.DeriveAddress(Target), + stakeState, out var result)) + { + throw new InvalidOperationException( + "Failed to migrate stake state. Unexpected situation."); + } + + world = result.Value.world; + } + + + // Migrate guild participant state from legacy to new + var value = world.GetAccountState(Addresses.GuildParticipant).GetState(Target) as List; + + var repository = new GuildRepository(world, context); + if (repository.GetJoinedGuild(GuildConfig.PlanetariumGuildOwner) is not { } planetariumGuildAddress) + { + throw new NullReferenceException("Planetarium guild is not found."); + } + + if (!repository.TryGetGuild(planetariumGuildAddress, out var planetariumGuild)) + { + throw new GuildMigrationFailedException("Planetarium guild is not found."); + } + + if (planetariumGuild.GuildMasterAddress != GuildConfig.PlanetariumGuildOwner) + { + throw new GuildMigrationFailedException("Unexpected guild master."); + } + + try + { + var legacyGuildParticipant = new LegacyGuildParticipant(value); + var guildParticipant = new GuildParticipant( + Target, + legacyGuildParticipant.GuildAddress, + repository); + repository.SetGuildParticipant(guildParticipant); + + // Migrate delegation + var guild = repository.GetGuild(guildParticipant.GuildAddress); + var guildGold = repository.GetBalance(guildParticipant.DelegationPoolAddress, Currencies.GuildGold); + if (guildGold.RawValue > 0) + { + repository.Delegate(guildParticipant.Address, guildGold); + } + + return repository.World; + } + catch (FailedLoadStateException) + { + var pledgeAddress = ((Address)Target).GetPledgeAddress(); + + // Patron contract structure: + // [0] = PatronAddress + // [1] = IsApproved + // [2] = Mead amount to refill. + if (!world.TryGetLegacyState(pledgeAddress, out List list) || list.Count < 3 || + list[0] is not Binary || list[0].ToAddress() != MeadConfig.PatronAddress || + list[1] is not Bencodex.Types.Boolean approved || !approved) + { + throw new GuildMigrationFailedException("Unexpected pledge structure."); + } + + repository.JoinGuild(planetariumGuildAddress, Target); + return repository.World; + } + } + } +} diff --git a/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs b/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs new file mode 100644 index 0000000000..48d1ad2b9d --- /dev/null +++ b/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs @@ -0,0 +1,59 @@ +using System; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Nekoyume.Action.Guild.Migration.LegacyModels; + +namespace Nekoyume.Action.Guild.Migration +{ + // TODO: [GuildMigration] Remove this class when the migration is done. + /// + /// An action to migrate the delegation height. + /// + [ActionType(TypeIdentifier)] + public class MigrateDelegationHeight : ActionBase + { + public const string TypeIdentifier = "migrate_delegation_height"; + + private const string HeightKey = "h"; + + public long Height { get; private set; } + + public MigrateDelegationHeight() + { + } + + public MigrateDelegationHeight(long height) + { + Height = height; + } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", Dictionary.Empty + .Add(HeightKey, Height)); + + public override void LoadPlainValue(IValue plainValue) + { + if (plainValue is not Dictionary root || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not Dictionary values || + !values.TryGetValue((Text)HeightKey, out var rawHeight) || + rawHeight is not Integer height) + { + throw new InvalidCastException(); + } + + Height = height; + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + + return world.SetDelegationMigrationHeight(Height); + } + } +} diff --git a/Lib9c/Action/Guild/Migration/MigratePlanetariumGuild.cs b/Lib9c/Action/Guild/Migration/MigratePlanetariumGuild.cs new file mode 100644 index 0000000000..cd7dbc37fa --- /dev/null +++ b/Lib9c/Action/Guild/Migration/MigratePlanetariumGuild.cs @@ -0,0 +1,83 @@ +using System; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Nekoyume.Action.ValidatorDelegation; +using Nekoyume.Model.Guild; +using Nekoyume.Action.Guild.Migration.LegacyModels; +using Lib9c; +using Nekoyume.Module.Guild; + +namespace Nekoyume.Action.Guild.Migration +{ + // TODO: [GuildMigration] Remove this class when the migration is done. + /// + /// An action to migrate the planetarium guild. + /// + [ActionType(TypeIdentifier)] + public class MigratePlanetariumGuild : ActionBase + { + public const string TypeIdentifier = "migrate_planetarium_guild"; + + public MigratePlanetariumGuild() + { + } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", Null.Value); + + public override void LoadPlainValue(IValue plainValue) + { + if (plainValue is not Dictionary root || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not Null) + { + throw new InvalidCastException(); + } + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + var repository = new GuildRepository(world, context); + + // Get Guild address + var guildMasterValue = world + .GetAccountState(Addresses.GuildParticipant) + .GetState(GuildConfig.PlanetariumGuildOwner) as List; + var legacyGuildMaster = new LegacyGuildParticipant(guildMasterValue); + var guildAddress = legacyGuildMaster.GuildAddress; + + // MigratePlanetariumGuild + var guildValue = world + .GetAccountState(Addresses.Guild) + .GetState(guildAddress) as List; + var legacyGuild = new LegacyGuild(guildValue); + var guild = new Model.Guild.Guild( + guildAddress, + legacyGuild.GuildMasterAddress, + ValidatorConfig.PlanetariumValidatorAddress, + repository); + repository.SetGuild(guild); + + // MigratePlanetariumGuildMaster + var guildParticipant = new GuildParticipant( + GuildConfig.PlanetariumGuildOwner, + guildAddress, + repository); + repository.SetGuildParticipant(guildParticipant); + + // Migrate delegation + var guildGold = repository.GetBalance(guildParticipant.DelegationPoolAddress, Currencies.GuildGold); + if (guildGold.RawValue > 0) + { + repository.Delegate(guildParticipant.Address, guildGold); + } + + return repository.World; + } + } +} diff --git a/Lib9c/Action/Guild/Migration/MigratePledgeToGuild.cs b/Lib9c/Action/Guild/Migration/MigratePledgeToGuild.cs deleted file mode 100644 index 4b958b798d..0000000000 --- a/Lib9c/Action/Guild/Migration/MigratePledgeToGuild.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Bencodex.Types; -using Libplanet.Action; -using Libplanet.Action.State; -using Nekoyume.Action.Guild.Migration.Controls; -using Nekoyume.TypedAddress; - -namespace Nekoyume.Action.Guild.Migration -{ - /// - /// An action to migrate the pledge to the guild. - /// But it is only for accounts contracted pledge with Planetarium (). - /// - [ActionType(TypeIdentifier)] - public class MigratePledgeToGuild : ActionBase - { - public const string TypeIdentifier = "migrate_pledge_to_guild"; - - private const string TargetKey = "t"; - - public AgentAddress Target { get; private set; } - - [Obsolete("Don't call in code.", error: true)] - public MigratePledgeToGuild() - { - } - - public MigratePledgeToGuild(AgentAddress target) - { - Target = target; - } - - public override IValue PlainValue => Dictionary.Empty - .Add("type_id", TypeIdentifier) - .Add("values", Dictionary.Empty - .Add(TargetKey, Target.Bencoded)); - - public override void LoadPlainValue(IValue plainValue) - { - if (plainValue is not Dictionary root || - !root.TryGetValue((Text)"values", out var rawValues) || - rawValues is not Dictionary values || - !values.TryGetValue((Text)TargetKey, out var rawTarget) || - rawTarget is not Binary target) - { - throw new InvalidCastException(); - } - - Target = new AgentAddress(target); - } - - public override IWorld Execute(IActionContext context) - { - context.UseGas(1); - - var world = context.PreviousState; - - return GuildMigrationCtrl.MigratePlanetariumPledgeToGuild(world, Target); - } - } -} diff --git a/Lib9c/Action/Guild/RejectGuildApplication.cs b/Lib9c/Action/Guild/MoveGuild.cs similarity index 54% rename from Lib9c/Action/Guild/RejectGuildApplication.cs rename to Lib9c/Action/Guild/MoveGuild.cs index 4af7bc70e2..84ede7baae 100644 --- a/Lib9c/Action/Guild/RejectGuildApplication.cs +++ b/Lib9c/Action/Guild/MoveGuild.cs @@ -3,6 +3,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; using Nekoyume.TypedAddress; @@ -10,25 +11,25 @@ namespace Nekoyume.Action.Guild { // TODO(GUILD-FEATURE): Enable again when Guild features are enabled. // [ActionType(TypeIdentifier)] - public class RejectGuildApplication : ActionBase + public class MoveGuild : ActionBase { - public const string TypeIdentifier = "reject_guild_application"; + public const string TypeIdentifier = "move_guild"; - private const string TargetKey = "t"; + private const string GuildAddressKey = "ga"; - public RejectGuildApplication() {} + public MoveGuild() {} - public RejectGuildApplication(AgentAddress target) + public MoveGuild(GuildAddress guildAddress) { - Target = target; + GuildAddress = guildAddress; } - public AgentAddress Target { get; private set; } + public GuildAddress GuildAddress { get; private set; } public override IValue PlainValue => Dictionary.Empty .Add("type_id", TypeIdentifier) .Add("values", Dictionary.Empty - .Add(TargetKey, Target.Bencoded)); + .Add(GuildAddressKey, GuildAddress.Bencoded)); public override void LoadPlainValue(IValue plainValue) { @@ -36,22 +37,26 @@ public override void LoadPlainValue(IValue plainValue) if (plainValue is not Dictionary || !root.TryGetValue((Text)"values", out var rawValues) || rawValues is not Dictionary values || - !values.TryGetValue((Text)TargetKey, out var rawTargetAddress)) + !values.TryGetValue((Text)GuildAddressKey, out var rawGuildAddress)) { throw new InvalidCastException(); } - Target = new AgentAddress(rawTargetAddress); + GuildAddress = new GuildAddress(rawGuildAddress); } public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var world = context.PreviousState; - var signer = context.GetAgentAddress(); + var repository = new GuildRepository(world, context); + var target = context.GetAgentAddress(); + var guildAddress = GuildAddress; - return world.RejectGuildApplication(signer, Target); + repository.MoveGuild(target, guildAddress); + + return repository.World; } } } diff --git a/Lib9c/Action/Guild/QuitGuild.cs b/Lib9c/Action/Guild/QuitGuild.cs index 9b468ad2e1..d330ed86f8 100644 --- a/Lib9c/Action/Guild/QuitGuild.cs +++ b/Lib9c/Action/Guild/QuitGuild.cs @@ -3,6 +3,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; namespace Nekoyume.Action.Guild @@ -29,14 +30,16 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var world = context.PreviousState; + var repository = new GuildRepository(world, context); var signer = context.GetAgentAddress(); // TODO: Do something to return 'Power' token; + repository.LeaveGuild(signer); - return world.LeaveGuild(signer); + return repository.World; } } } diff --git a/Lib9c/Action/Guild/RemoveGuild.cs b/Lib9c/Action/Guild/RemoveGuild.cs index 3ae72fc336..39bd0652e7 100644 --- a/Lib9c/Action/Guild/RemoveGuild.cs +++ b/Lib9c/Action/Guild/RemoveGuild.cs @@ -3,6 +3,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; namespace Nekoyume.Action.Guild @@ -32,14 +33,14 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var world = context.PreviousState; - var signer = context.GetAgentAddress(); + var repository = new GuildRepository(world, context); // TODO: Do something to return 'Power' token; - - return world.RemoveGuild(signer); + repository.RemoveGuild(); + return repository.World; } } } diff --git a/Lib9c/Action/Guild/UnbanGuildMember.cs b/Lib9c/Action/Guild/UnbanGuildMember.cs index f18fc478e1..b4641099ff 100644 --- a/Lib9c/Action/Guild/UnbanGuildMember.cs +++ b/Lib9c/Action/Guild/UnbanGuildMember.cs @@ -5,6 +5,7 @@ using Libplanet.Action.State; using Libplanet.Crypto; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; using Nekoyume.Module.Guild; namespace Nekoyume.Action.Guild @@ -46,17 +47,19 @@ rawValues is not Dictionary values || public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var world = context.PreviousState; + var repository = new GuildRepository(world, context); var signer = context.GetAgentAddress(); - if (world.GetJoinedGuild(signer) is not { } guildAddress) + if (repository.GetJoinedGuild(signer) is not { } guildAddress) { throw new InvalidOperationException("The signer does not join any guild."); } - return world.Unban(guildAddress, signer, Target); + repository.Unban(guildAddress, signer, Target); + return repository.World; } } } diff --git a/Lib9c/Action/HackAndSlash.cs b/Lib9c/Action/HackAndSlash.cs index 8fab7c36f3..9620a13041 100644 --- a/Lib9c/Action/HackAndSlash.cs +++ b/Lib9c/Action/HackAndSlash.cs @@ -100,7 +100,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var random = context.GetRandom(); return Execute( context.PreviousState, diff --git a/Lib9c/Action/HackAndSlashRandomBuff.cs b/Lib9c/Action/HackAndSlashRandomBuff.cs index 0473849f7a..311cdf648c 100644 --- a/Lib9c/Action/HackAndSlashRandomBuff.cs +++ b/Lib9c/Action/HackAndSlashRandomBuff.cs @@ -46,7 +46,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary TableSheets { get; set; } @@ -75,6 +84,7 @@ public InitializeStates() } public InitializeStates( + ValidatorSet validatorSet, RankingState0 rankingState, ShopState shopState, Dictionary tableSheets, @@ -89,6 +99,7 @@ public InitializeStates( CreditsState creditsState = null, ISet
assetMinters = null) { + ValidatorSet = validatorSet.Bencoded; Ranking = (Dictionary)rankingState.Serialize(); Shop = (Dictionary)shopState.Serialize(); TableSheets = tableSheets; @@ -118,7 +129,7 @@ public InitializeStates( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IActionContext ctx = context; var states = ctx.PreviousState; var weeklyArenaState = new WeeklyArenaState(0); @@ -146,7 +157,8 @@ public override IWorld Execute(IActionContext context) .SetLegacyState(RedeemCodeState.Address, RedeemCode) .SetLegacyState(ActivatedAccountsState.Address, ActivatedAccounts) .SetLegacyState(GoldCurrencyState.Address, GoldCurrency) - .SetLegacyState(Addresses.GoldDistribution, GoldDistributions); + .SetLegacyState(Addresses.GoldDistribution, GoldDistributions) + .SetDelegationMigrationHeight(0); if (!(AdminAddressState is null)) { @@ -192,6 +204,26 @@ public override IWorld Execute(IActionContext context) ); } + var validatorSet = new ValidatorSet(ValidatorSet); + foreach (var validator in validatorSet.Validators) + { + var delegationFAV = FungibleAssetValue.FromRawValue( + ValidatorDelegatee.ValidatorDelegationCurrency, validator.Power); + states = states.MintAsset(ctx, StakeState.DeriveAddress(validator.OperatorAddress), delegationFAV); + + var validatorRepository = new ValidatorRepository(states, ctx); + var validatorDelegatee = validatorRepository.CreateValidatorDelegatee( + validator.PublicKey, ValidatorDelegatee.DefaultCommissionPercentage); + var validatorDelegator = validatorRepository.GetValidatorDelegator(validator.OperatorAddress); + validatorDelegatee.Bond(validatorDelegator, delegationFAV, context.BlockIndex); + + var guildRepository = new GuildRepository(validatorRepository); + var guildDelegatee = guildRepository.CreateGuildDelegatee(validator.OperatorAddress); + var guildDelegator = guildRepository.GetGuildDelegator(validator.OperatorAddress); + guildDelegator.Delegate(guildDelegatee, delegationFAV, context.BlockIndex); + states = guildRepository.World; + } + return states; } @@ -200,6 +232,7 @@ protected override IImmutableDictionary PlainValueInternal get { var rv = ImmutableDictionary.Empty + .Add("validator_set", ValidatorSet) .Add("ranking_state", Ranking) .Add("shop_state", Shop) .Add("table_sheets", @@ -241,6 +274,7 @@ protected override IImmutableDictionary PlainValueInternal protected override void LoadPlainValueInternal(IImmutableDictionary plainValue) { + ValidatorSet = plainValue["validator_set"]; Ranking = (Dictionary) plainValue["ranking_state"]; Shop = (Dictionary) plainValue["shop_state"]; TableSheets = ((Dictionary) plainValue["table_sheets"]) diff --git a/Lib9c/Action/IssueToken.cs b/Lib9c/Action/IssueToken.cs index 8f29824eba..a55c75e523 100644 --- a/Lib9c/Action/IssueToken.cs +++ b/Lib9c/Action/IssueToken.cs @@ -50,7 +50,7 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); Addresses.CheckAvatarAddrIsContainedInAgent(context.Signer, AvatarAddress); if (!FungibleAssetValues.Any() && !Items.Any()) { diff --git a/Lib9c/Action/IssueTokensFromGarage.cs b/Lib9c/Action/IssueTokensFromGarage.cs index b754094cdc..083c063c54 100644 --- a/Lib9c/Action/IssueTokensFromGarage.cs +++ b/Lib9c/Action/IssueTokensFromGarage.cs @@ -47,7 +47,7 @@ Specs is { } public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); if (Specs is null) { diff --git a/Lib9c/Action/ItemEnhancement.cs b/Lib9c/Action/ItemEnhancement.cs index 14b0923dac..a757d58560 100644 --- a/Lib9c/Action/ItemEnhancement.cs +++ b/Lib9c/Action/ItemEnhancement.cs @@ -9,6 +9,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Extensions; using Nekoyume.Helper; @@ -111,7 +112,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary 0) { - var arenaSheet = states.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = - ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); - states = states.TransferAsset(ctx, ctx.Signer, feeStoreAddress, + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } + + states = states.TransferAsset(ctx, ctx.Signer, feeAddress, states.GetGoldCurrency() * requiredNcg); } diff --git a/Lib9c/Action/ItemEnhancement10.cs b/Lib9c/Action/ItemEnhancement10.cs index ad75723ddc..551d524fef 100644 --- a/Lib9c/Action/ItemEnhancement10.cs +++ b/Lib9c/Action/ItemEnhancement10.cs @@ -121,7 +121,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary 0) { - var arenaSheet = states.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); - states = states.TransferAsset(ctx, ctx.Signer, feeStoreAddress, states.GetGoldCurrency() * requiredNcg); + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } + + states = states.TransferAsset(ctx, ctx.Signer, feeAddress, states.GetGoldCurrency() * requiredNcg); } // Unequip items diff --git a/Lib9c/Action/ItemEnhancement12.cs b/Lib9c/Action/ItemEnhancement12.cs index 12cb72afbb..10fc68c4e2 100644 --- a/Lib9c/Action/ItemEnhancement12.cs +++ b/Lib9c/Action/ItemEnhancement12.cs @@ -11,6 +11,7 @@ using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Types.Assets; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Extensions; using Nekoyume.Helper; @@ -131,7 +132,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary 0) { - var arenaSheet = states.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = - ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); - states = states.TransferAsset(ctx, ctx.Signer, feeStoreAddress, + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } + + states = states.TransferAsset(ctx, ctx.Signer, feeAddress, states.GetGoldCurrency() * requiredNcg); } diff --git a/Lib9c/Action/ItemEnhancement13.cs b/Lib9c/Action/ItemEnhancement13.cs index bf5891981e..2f866d1578 100644 --- a/Lib9c/Action/ItemEnhancement13.cs +++ b/Lib9c/Action/ItemEnhancement13.cs @@ -11,6 +11,7 @@ using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Types.Assets; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Extensions; using Nekoyume.Helper; @@ -134,7 +135,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary 0) { - var arenaSheet = states.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = - ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); - states = states.TransferAsset(ctx, ctx.Signer, feeStoreAddress, + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } + + states = states.TransferAsset(ctx, ctx.Signer, feeAddress, states.GetGoldCurrency() * requiredNcg); } diff --git a/Lib9c/Action/ItemEnhancement7.cs b/Lib9c/Action/ItemEnhancement7.cs index 510cc5d4c7..c01a9e1ad3 100644 --- a/Lib9c/Action/ItemEnhancement7.cs +++ b/Lib9c/Action/ItemEnhancement7.cs @@ -76,7 +76,7 @@ public override IValue Serialize() => public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IActionContext ctx = context; var states = ctx.PreviousState; var slotAddress = avatarAddress.Derive( diff --git a/Lib9c/Action/ItemEnhancement9.cs b/Lib9c/Action/ItemEnhancement9.cs index e3a6c29d91..2686b7020b 100644 --- a/Lib9c/Action/ItemEnhancement9.cs +++ b/Lib9c/Action/ItemEnhancement9.cs @@ -117,7 +117,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary 2) @@ -145,8 +146,15 @@ public override IWorld Execute(IActionContext context) $"required {fee}, but balance is {crystalBalance}"); } - var arenaAdr = ArenaHelper.DeriveArenaAddress(roundData.ChampionshipId, roundData.Round); - states = states.TransferAsset(context, context.Signer, arenaAdr, fee); + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + feeAddress = ArenaHelper.DeriveArenaAddress(roundData.ChampionshipId, roundData.Round); + } + + states = states.TransferAsset(context, context.Signer, feeAddress, fee); } // check medal diff --git a/Lib9c/Action/JoinArena3.cs b/Lib9c/Action/JoinArena3.cs index c6641ca923..7573de49e7 100644 --- a/Lib9c/Action/JoinArena3.cs +++ b/Lib9c/Action/JoinArena3.cs @@ -7,6 +7,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Extensions; using Nekoyume.Helper; @@ -69,7 +70,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, avatarAddress); var started = DateTimeOffset.UtcNow; @@ -85,7 +86,7 @@ public override IWorld Execute(IActionContext context) { throw new FailedLoadStateException($"[{nameof(JoinArena)}] Aborted as the avatar state of the signer was failed to load."); } - + if (!avatarState.worldInformation.TryGetUnlockedWorldByStageClearedBlockIndex( out var world)) { @@ -162,8 +163,15 @@ public override IWorld Execute(IActionContext context) $"required {fee}, but balance is {crystalBalance}"); } - var arenaAdr = ArenaHelper.DeriveArenaAddress(roundData.ChampionshipId, roundData.Round); - states = states.TransferAsset(context, context.Signer, arenaAdr, fee); + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + feeAddress = ArenaHelper.DeriveArenaAddress(roundData.ChampionshipId, roundData.Round); + } + + states = states.TransferAsset(context, context.Signer, feeAddress, fee); } // check medal diff --git a/Lib9c/Action/MigrateAgentAvatar.cs b/Lib9c/Action/MigrateAgentAvatar.cs index e4dff011c9..028ab162a1 100644 --- a/Lib9c/Action/MigrateAgentAvatar.cs +++ b/Lib9c/Action/MigrateAgentAvatar.cs @@ -42,7 +42,7 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); #if !LIB9C_DEV_EXTENSIONS && !UNITY_EDITOR if (context.Signer != Operator) diff --git a/Lib9c/Action/MigrateFee.cs b/Lib9c/Action/MigrateFee.cs index ac4bbb666b..5edc8f9456 100644 --- a/Lib9c/Action/MigrateFee.cs +++ b/Lib9c/Action/MigrateFee.cs @@ -67,7 +67,7 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); CheckPermission(context); var states = context.PreviousState; diff --git a/Lib9c/Action/MigrateMonsterCollection.cs b/Lib9c/Action/MigrateMonsterCollection.cs index a79c0906b7..1e4a9f0d53 100644 --- a/Lib9c/Action/MigrateMonsterCollection.cs +++ b/Lib9c/Action/MigrateMonsterCollection.cs @@ -17,7 +17,7 @@ namespace Nekoyume.Action { /// /// An action to claim remained monster collection rewards and to migrate - /// into without cancellation, to + /// into without cancellation, to /// keep its staked period. /// [ActionType("migrate_monster_collection")] @@ -48,12 +48,12 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); var started = DateTimeOffset.UtcNow; Log.Debug("{AddressesHex}MigrateMonsterCollection exec started", addressesHex); - if (states.TryGetStakeState(context.Signer, out StakeState _)) + if (states.TryGetLegacyStakeState(context.Signer, out LegacyStakeState _)) { throw new InvalidOperationException("The user has already staked."); } @@ -80,13 +80,13 @@ public override IWorld Execute(IActionContext context) } var monsterCollectionState = new MonsterCollectionState(stateDict); - var migratedStakeStateAddress = StakeState.DeriveAddress(context.Signer); - var migratedStakeState = new StakeState( + var migratedStakeStateAddress = LegacyStakeState.DeriveAddress(context.Signer); + var migratedStakeState = new LegacyStakeState( migratedStakeStateAddress, monsterCollectionState.StartedBlockIndex, monsterCollectionState.ReceivedBlockIndex, monsterCollectionState.ExpiredBlockIndex, - new StakeState.StakeAchievements()); + new LegacyStakeState.StakeAchievements()); var ended = DateTimeOffset.UtcNow; Log.Debug("{AddressesHex}MigrateMonsterCollection Total Executed Time: {Elapsed}", addressesHex, ended - started); diff --git a/Lib9c/Action/MigrationActivatedAccountsState.cs b/Lib9c/Action/MigrationActivatedAccountsState.cs index 4c5842daa0..169814aaa1 100644 --- a/Lib9c/Action/MigrationActivatedAccountsState.cs +++ b/Lib9c/Action/MigrationActivatedAccountsState.cs @@ -18,7 +18,7 @@ public class MigrationActivatedAccountsState : GameAction, IMigrationActivatedAc { public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; CheckPermission(context); diff --git a/Lib9c/Action/MintAssets.cs b/Lib9c/Action/MintAssets.cs index 2e1d30f863..f0c097c0b1 100644 --- a/Lib9c/Action/MintAssets.cs +++ b/Lib9c/Action/MintAssets.cs @@ -36,7 +36,7 @@ public MintAssets(IEnumerable specs, string? memo) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); if (MintSpecs is null) { diff --git a/Lib9c/Action/PatchTableSheet.cs b/Lib9c/Action/PatchTableSheet.cs index fd70e54b74..fef0c3f233 100644 --- a/Lib9c/Action/PatchTableSheet.cs +++ b/Lib9c/Action/PatchTableSheet.cs @@ -38,7 +38,7 @@ public class PatchTableSheet : GameAction, IPatchTableSheetV1 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IActionContext ctx = context; var states = ctx.PreviousState; var sheetAddress = Addresses.TableSheet.Derive(TableName); diff --git a/Lib9c/Action/PetEnhancement.cs b/Lib9c/Action/PetEnhancement.cs index 16a2e7095a..31194dec54 100644 --- a/Lib9c/Action/PetEnhancement.cs +++ b/Lib9c/Action/PetEnhancement.cs @@ -5,6 +5,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Exceptions; using Nekoyume.Extensions; @@ -43,7 +44,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var addresses = GetSignerAndOtherAddressesHex(context, AvatarAddress); // NOTE: The `AvatarAddress` must contained in `Signer`'s `AgentState.avatarAddresses`. @@ -125,11 +126,6 @@ public override IWorld Execute(IActionContext context) petState.Level, TargetLevel); - var arenaSheet = sheets.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress( - arenaData.ChampionshipId, - arenaData.Round); if (ncgQuantity > 0) { var ncgCost = ncgQuantity * ncgCurrency; @@ -143,7 +139,17 @@ public override IWorld Execute(IActionContext context) currentNcg); } - states = states.TransferAsset(context, context.Signer, feeStoreAddress, ncgCost); + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } + + states = states.TransferAsset(context, context.Signer, feeAddress, ncgCost); } if (soulStoneQuantity > 0) @@ -164,7 +170,7 @@ public override IWorld Execute(IActionContext context) states = states.TransferAsset( context, AvatarAddress, - feeStoreAddress, + Addresses.RewardPool, soulStoneCost); } diff --git a/Lib9c/Action/PrepareRewardAssets.cs b/Lib9c/Action/PrepareRewardAssets.cs index e5e1441b3d..12a53743f1 100644 --- a/Lib9c/Action/PrepareRewardAssets.cs +++ b/Lib9c/Action/PrepareRewardAssets.cs @@ -45,7 +45,7 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IWorld states = context.PreviousState; CheckPermission(context); diff --git a/Lib9c/Action/Raid.cs b/Lib9c/Action/Raid.cs index 4775f90581..f331b75801 100644 --- a/Lib9c/Action/Raid.cs +++ b/Lib9c/Action/Raid.cs @@ -8,6 +8,7 @@ using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Types.Assets; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Battle; using Nekoyume.Extensions; @@ -48,7 +49,7 @@ public class Raid : GameAction, IRaidV2 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IWorld states = context.PreviousState; var addressHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); var started = DateTimeOffset.UtcNow; @@ -149,10 +150,17 @@ public override IWorld Execute(IActionContext context) throw new ExceedTicketPurchaseLimitException(""); } var goldCurrency = states.GetGoldCurrency(); - var arenaSheet = states.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeAddress = - ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } + states = states.TransferAsset(context, context.Signer, feeAddress, WorldBossHelper.CalculateTicketPrice(row, raiderState, goldCurrency)); raiderState.PurchaseCount++; diff --git a/Lib9c/Action/RankingBattle.cs b/Lib9c/Action/RankingBattle.cs index 56272890ba..706d577607 100644 --- a/Lib9c/Action/RankingBattle.cs +++ b/Lib9c/Action/RankingBattle.cs @@ -47,7 +47,7 @@ public class RankingBattle : GameAction, IRankingBattleV2 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var ctx = context; var states = ctx.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, avatarAddress, enemyAddress); diff --git a/Lib9c/Action/RankingBattle11.cs b/Lib9c/Action/RankingBattle11.cs index 3fb8a58151..e2c2716906 100644 --- a/Lib9c/Action/RankingBattle11.cs +++ b/Lib9c/Action/RankingBattle11.cs @@ -53,7 +53,7 @@ public class RankingBattle11 : GameAction, IRankingBattleV2 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IActionContext ctx = context; var states = ctx.PreviousState; diff --git a/Lib9c/Action/RapidCombination.cs b/Lib9c/Action/RapidCombination.cs index b8c001e817..877efd843f 100644 --- a/Lib9c/Action/RapidCombination.cs +++ b/Lib9c/Action/RapidCombination.cs @@ -33,7 +33,7 @@ public class RapidCombination : GameAction, IRapidCombinationV2 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, avatarAddress); var started = DateTimeOffset.UtcNow; diff --git a/Lib9c/Action/RapidCombination0.cs b/Lib9c/Action/RapidCombination0.cs index 6cba135383..cd439a5bda 100644 --- a/Lib9c/Action/RapidCombination0.cs +++ b/Lib9c/Action/RapidCombination0.cs @@ -53,7 +53,7 @@ public override IValue Serialize() => public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var slotAddress = avatarAddress.Derive( string.Format( diff --git a/Lib9c/Action/RapidCombination5.cs b/Lib9c/Action/RapidCombination5.cs index c0b9f1c231..b7bbdce416 100644 --- a/Lib9c/Action/RapidCombination5.cs +++ b/Lib9c/Action/RapidCombination5.cs @@ -56,7 +56,7 @@ public override IValue Serialize() => public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var slotAddress = avatarAddress.Derive( string.Format( diff --git a/Lib9c/Action/ReRegisterProduct.cs b/Lib9c/Action/ReRegisterProduct.cs index a6dad6fb4e..592e57cec3 100644 --- a/Lib9c/Action/ReRegisterProduct.cs +++ b/Lib9c/Action/ReRegisterProduct.cs @@ -28,7 +28,7 @@ public class ReRegisterProduct : GameAction public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IWorld states = context.PreviousState; if (!ReRegisterInfos.Any()) diff --git a/Lib9c/Action/RedeemCode.cs b/Lib9c/Action/RedeemCode.cs index 1e89ca519e..31fb1e20ef 100644 --- a/Lib9c/Action/RedeemCode.cs +++ b/Lib9c/Action/RedeemCode.cs @@ -44,7 +44,7 @@ public RedeemCode(string code, Address avatarAddress) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); var started = DateTimeOffset.UtcNow; diff --git a/Lib9c/Action/RedeemCode2.cs b/Lib9c/Action/RedeemCode2.cs index 2ffe4bb36a..c0c93c3c71 100644 --- a/Lib9c/Action/RedeemCode2.cs +++ b/Lib9c/Action/RedeemCode2.cs @@ -40,7 +40,7 @@ public RedeemCode2(string code, Address avatarAddress) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; CheckObsolete(ActionObsoleteConfig.V100080ObsoleteIndex, context); diff --git a/Lib9c/Action/RegisterProduct.cs b/Lib9c/Action/RegisterProduct.cs index 2b0578aa41..dfc1b463fa 100644 --- a/Lib9c/Action/RegisterProduct.cs +++ b/Lib9c/Action/RegisterProduct.cs @@ -43,7 +43,7 @@ public override IWorld Execute(IActionContext context) { var sw = new Stopwatch(); - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; sw.Start(); diff --git a/Lib9c/Action/RegisterProduct0.cs b/Lib9c/Action/RegisterProduct0.cs index 3d0fdb30b6..02c6221494 100644 --- a/Lib9c/Action/RegisterProduct0.cs +++ b/Lib9c/Action/RegisterProduct0.cs @@ -30,7 +30,7 @@ public class RegisterProduct0 : GameAction public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; if (!RegisterInfos.Any()) diff --git a/Lib9c/Action/RenewAdminState.cs b/Lib9c/Action/RenewAdminState.cs index d1f19dc15c..05766f5622 100644 --- a/Lib9c/Action/RenewAdminState.cs +++ b/Lib9c/Action/RenewAdminState.cs @@ -35,7 +35,7 @@ public RenewAdminState(long newValidUntil) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; if (TryGetAdminState(context, out AdminState adminState)) diff --git a/Lib9c/Action/RequestPledge.cs b/Lib9c/Action/RequestPledge.cs index 5e74498027..de18748018 100644 --- a/Lib9c/Action/RequestPledge.cs +++ b/Lib9c/Action/RequestPledge.cs @@ -37,7 +37,7 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var contractAddress = AgentAddress.GetPledgeAddress(); if (states.TryGetLegacyState(contractAddress, out List _)) diff --git a/Lib9c/Action/RetrieveAvatarAssets.cs b/Lib9c/Action/RetrieveAvatarAssets.cs index 719a3068da..721c832418 100644 --- a/Lib9c/Action/RetrieveAvatarAssets.cs +++ b/Lib9c/Action/RetrieveAvatarAssets.cs @@ -36,7 +36,7 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); Address signer = context.Signer; var state = context.PreviousState; var agentState = state.GetAgentState(signer); diff --git a/Lib9c/Action/RewardGold.cs b/Lib9c/Action/RewardGold.cs index a4a60fcafb..ad86f8d9b3 100644 --- a/Lib9c/Action/RewardGold.cs +++ b/Lib9c/Action/RewardGold.cs @@ -37,7 +37,6 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); var states = context.PreviousState; states = TransferMead(context, states); states = GenesisGoldDistribution(context, states); @@ -283,24 +282,19 @@ public IWorld ResetChallengeCount(IActionContext ctx, IWorld states) public IWorld MinerReward(IActionContext ctx, IWorld states) { - // 마이닝 보상 - // https://www.notion.so/planetarium/Mining-Reward-b7024ef463c24ebca40a2623027d497d - Currency currency = states.GetGoldCurrency(); - FungibleAssetValue defaultMiningReward = currency * 10; - var countOfHalfLife = (int)Math.Pow(2, Convert.ToInt64((ctx.BlockIndex - 1) / 12614400)); - FungibleAssetValue miningReward = - defaultMiningReward.DivRem(countOfHalfLife, out FungibleAssetValue _); - - var balance = states.GetBalance(GoldCurrencyState.Address, currency); - if (miningReward >= FungibleAssetValue.Parse(currency, "1.25") && balance >= miningReward) + Currency currency = Currencies.Mead; + var usedGas = states.GetBalance(Addresses.GasPool, currency); + var defaultReward = currency * 5; + var halfOfUsedGas = usedGas.DivRem(2).Quotient; + var gasToBurn = usedGas - halfOfUsedGas; + var miningReward = halfOfUsedGas + defaultReward; + states = states.MintAsset(ctx, Addresses.GasPool, defaultReward); + if (gasToBurn.Sign > 0) { - states = states.TransferAsset( - ctx, - GoldCurrencyState.Address, - ctx.Miner, - miningReward - ); + states = states.BurnAsset(ctx, Addresses.GasPool, gasToBurn); } + states = states.TransferAsset( + ctx, Addresses.GasPool, Addresses.RewardPool, miningReward); return states; } diff --git a/Lib9c/Action/RuneEnhancement.cs b/Lib9c/Action/RuneEnhancement.cs index af6bf6718f..d01babfd8d 100644 --- a/Lib9c/Action/RuneEnhancement.cs +++ b/Lib9c/Action/RuneEnhancement.cs @@ -7,6 +7,7 @@ using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Types.Assets; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Extensions; using Nekoyume.Helper; @@ -59,7 +60,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; if (!states.TryGetAvatarState(context.Signer, AvatarAddress, out _)) @@ -152,27 +153,32 @@ public override IWorld Execute(IActionContext context) runeState.LevelUp(levelUpResult.LevelUpCount); states = states.SetRuneState(AvatarAddress, allRuneState); - var arenaSheet = sheets.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = - ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } // Burn costs if (levelUpResult.NcgCost > 0) { - states = states.TransferAsset(context, context.Signer, feeStoreAddress, + states = states.TransferAsset(context, context.Signer, feeAddress, levelUpResult.NcgCost * ncgCurrency); } if (levelUpResult.CrystalCost > 0) { - states = states.TransferAsset(context, context.Signer, feeStoreAddress, + states = states.TransferAsset(context, context.Signer, feeAddress, levelUpResult.CrystalCost * crystalCurrency); } if (levelUpResult.RuneCost > 0) { - states = states.TransferAsset(context, AvatarAddress, feeStoreAddress, + states = states.TransferAsset(context, AvatarAddress, feeAddress, levelUpResult.RuneCost * runeCurrency); } diff --git a/Lib9c/Action/RuneSummon.cs b/Lib9c/Action/RuneSummon.cs index bc903b826b..c9b7b9980b 100644 --- a/Lib9c/Action/RuneSummon.cs +++ b/Lib9c/Action/RuneSummon.cs @@ -11,6 +11,7 @@ using Libplanet.Crypto; using Libplanet.Types.Assets; using Nekoyume.Action.Exceptions; +using Nekoyume.Action.Guild.Migration.LegacyModels; using Nekoyume.Arena; using Nekoyume.Extensions; using Nekoyume.Model.State; @@ -38,7 +39,7 @@ public class RuneSummon : GameAction, IRuneSummonV1 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); @@ -90,15 +91,20 @@ public override IWorld Execute(IActionContext context) // Transfer Cost NCG first for fast-fail if (summonRow.CostNcg > 0L) { - var arenaSheet = states.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = - ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } states = states.TransferAsset( context, context.Signer, - feeStoreAddress, + feeAddress, states.GetGoldCurrency() * summonRow.CostNcg * SummonCount ); } diff --git a/Lib9c/Action/SecureMiningReward.cs b/Lib9c/Action/SecureMiningReward.cs index 8e9593d76f..672cc02f40 100644 --- a/Lib9c/Action/SecureMiningReward.cs +++ b/Lib9c/Action/SecureMiningReward.cs @@ -57,7 +57,7 @@ public SecureMiningReward(Address recipient) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); IWorld state = context.PreviousState; CheckPermission(context); diff --git a/Lib9c/Action/Sell.cs b/Lib9c/Action/Sell.cs index 8f885e2770..f45dc78f20 100644 --- a/Lib9c/Action/Sell.cs +++ b/Lib9c/Action/Sell.cs @@ -64,7 +64,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; Address shopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, orderId); Address itemAddress = Addresses.GetItemAddress(tradableId); diff --git a/Lib9c/Action/Sell6.cs b/Lib9c/Action/Sell6.cs index e07fde2116..08b82743b6 100644 --- a/Lib9c/Action/Sell6.cs +++ b/Lib9c/Action/Sell6.cs @@ -61,7 +61,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; CheckObsolete(ActionObsoleteConfig.V100080ObsoleteIndex, context); diff --git a/Lib9c/Action/SellCancellation.cs b/Lib9c/Action/SellCancellation.cs index 350f10269e..f0923a0a23 100644 --- a/Lib9c/Action/SellCancellation.cs +++ b/Lib9c/Action/SellCancellation.cs @@ -90,7 +90,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary context.BlockIndex) { // NOTE: Cannot re-contract with less balance. @@ -149,12 +158,14 @@ public override IWorld Execute(IActionContext context) } } - // NOTE: Withdraw staking. - if (Amount == 0) + if (stakeStateV2.StateVersion == 2) { - return states - .RemoveLegacyState(stakeStateAddress) - .TransferAsset(context, stakeStateAddress, context.Signer, stakedBalance); + if (!StakeStateUtils.TryMigrateV2ToV3(context, states, stakeStateAddress, stakeStateV2, out var result)) + { + throw new InvalidOperationException("Failed to migration. Unexpected situation."); + } + + states = result.Value.world; } // NOTE: Contract a new staking. @@ -176,23 +187,108 @@ private static IWorld ContractNewStake( IActionContext context, IWorld state, Address stakeStateAddr, - FungibleAssetValue? stakedBalance, + FungibleAssetValue stakedBalance, FungibleAssetValue targetStakeBalance, Contract latestStakeContract) { - var newStakeState = new StakeStateV2(latestStakeContract, context.BlockIndex); - if (stakedBalance.HasValue) + var stakeStateValue = new StakeState(latestStakeContract, context.BlockIndex).Serialize(); + var additionalBalance = targetStakeBalance - stakedBalance; + var height = context.BlockIndex; + var agentAddress = new AgentAddress(context.Signer); + + if (additionalBalance.Sign > 0) { - state = state.TransferAsset( - context, - stakeStateAddr, - context.Signer, - stakedBalance.Value); + var gg = GetGuildCoinFromNCG(additionalBalance); + state = state + .TransferAsset(context, context.Signer, stakeStateAddr, additionalBalance) + .MintAsset(context, stakeStateAddr, gg); + + var guildRepository = new GuildRepository(state, context); + if (guildRepository.TryGetGuildParticipant(agentAddress, out var guildParticipant)) + { + var guild = guildRepository.GetGuild(guildParticipant.GuildAddress); + guildParticipant.Delegate(guild, gg, height); + state = guildRepository.World; + } + else + { + state = state + .TransferAsset(context, stakeStateAddr, Addresses.NonValidatorDelegatee, gg); + } + + } + else if (additionalBalance.Sign < 0) + { + var gg = GetGuildCoinFromNCG(-additionalBalance); + + var guildRepository = new GuildRepository(state, context); + + // TODO : [GuildMigration] Remove below code when the migration is done. + if (guildRepository.TryGetGuildParticipant(agentAddress, out var guildParticipant)) + { + var guild = guildRepository.GetGuild(guildParticipant.GuildAddress); + var guildDelegatee = guildRepository.GetGuildDelegatee(guild.ValidatorAddress); + var share = guildDelegatee.ShareFromFAV(gg); + + var guildDelegator = guildRepository.GetGuildDelegator(agentAddress); + guildDelegatee.Unbond(guildDelegator, share, height); + + var validatorRepository = new ValidatorRepository(guildRepository); + var validatorDelegatee = validatorRepository.GetValidatorDelegatee(guild.ValidatorAddress); + var validatorDelegator = validatorRepository.GetValidatorDelegator(guild.Address); + validatorDelegatee.Unbond(validatorDelegator, share, height); + + state = validatorRepository.World; + + state = state.BurnAsset(context, guildDelegatee.DelegationPoolAddress, gg); + } + else + { + state = state.BurnAsset(context, Addresses.NonValidatorDelegatee, gg); + } + + state = state.TransferAsset(context, stakeStateAddr, context.Signer, -additionalBalance); + + // TODO : [GuildMigration] Revive below code when the migration is done. + // if (guildRepository.TryGetGuildParticipant(agentAddress, out var guildParticipant)) + // { + // var guild = guildRepository.GetGuild(guildParticipant.GuildAddress); + // var guildDelegatee = guildRepository.GetGuildDelegatee(guild.ValidatorAddress); + // var share = guildDelegatee.ShareFromFAV(gg); + // guildParticipant.Undelegate(guild, share, height); + // state = guildRepository.World; + // } + // else + // { + // var delegateeAddress = Addresses.NonValidatorDelegatee; + // var delegatorAddress = context.Signer; + // var repository = new GuildRepository(state, context); + // var unbondLockInAddress = DelegationAddress.UnbondLockInAddress(delegateeAddress, repository.DelegateeAccountAddress, delegatorAddress); + // var unbondLockIn = new UnbondLockIn( + // unbondLockInAddress, ValidatorDelegatee.ValidatorMaxUnbondLockInEntries, delegateeAddress, delegatorAddress, null); + // unbondLockIn = unbondLockIn.LockIn( + // gg, height, height + ValidatorDelegatee.ValidatorUnbondingPeriod); + // repository.SetUnbondLockIn(unbondLockIn); + // repository.SetUnbondingSet( + // repository.GetUnbondingSet().SetUnbonding(unbondLockIn)); + // state = repository.World; + // } + + if ((stakedBalance + additionalBalance).Sign == 0) + { + return state.MutateAccount( + ReservedAddresses.LegacyAccount, + state => state.RemoveState(stakeStateAddr)); + } } - return state - .TransferAsset(context, context.Signer, stakeStateAddr, targetStakeBalance) - .SetLegacyState(stakeStateAddr, newStakeState.Serialize()); + return state.SetLegacyState(stakeStateAddr, stakeStateValue); + } + + private static FungibleAssetValue GetGuildCoinFromNCG(FungibleAssetValue balance) + { + return FungibleAssetValue.Parse(Currencies.GuildGold, + balance.GetQuantityString(true)); } } } diff --git a/Lib9c/Action/Stake2.cs b/Lib9c/Action/Stake2.cs index 586a40d7db..e330b0e575 100644 --- a/Lib9c/Action/Stake2.cs +++ b/Lib9c/Action/Stake2.cs @@ -46,7 +46,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary RecipientsCapacity) diff --git a/Lib9c/Action/UnlockCombinationSlot.cs b/Lib9c/Action/UnlockCombinationSlot.cs index 030c70a470..aa54d055f9 100644 --- a/Lib9c/Action/UnlockCombinationSlot.cs +++ b/Lib9c/Action/UnlockCombinationSlot.cs @@ -45,7 +45,7 @@ protected override void LoadPlainValueInternal(IImmutableDictionary(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); int cost; Currency currency; switch (slot.RuneSlotType) @@ -106,8 +104,18 @@ public override IWorld Execute(IActionContext context) arenaSlotState.Unlock(SlotIndex); raidSlotState.Unlock(SlotIndex); + var feeAddress = Addresses.RewardPool; + // TODO: [GuildMigration] Remove this after migration + if (states.GetDelegationMigrationHeight() is long migrationHeight + && context.BlockIndex < migrationHeight) + { + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + feeAddress = ArenaHelper.DeriveArenaAddress(arenaData.ChampionshipId, arenaData.Round); + } + return states - .TransferAsset(context, context.Signer, feeStoreAddress, cost * currency) + .TransferAsset(context, context.Signer, feeAddress, cost * currency) .SetLegacyState(adventureSlotStateAddress, adventureSlotState.Serialize()) .SetLegacyState(arenaSlotStateAddress, arenaSlotState.Serialize()) .SetLegacyState(raidSlotStateAddress, raidSlotState.Serialize()); diff --git a/Lib9c/Action/UnlockWorld.cs b/Lib9c/Action/UnlockWorld.cs index aa0cdcac8b..eaa2ca7300 100644 --- a/Lib9c/Action/UnlockWorld.cs +++ b/Lib9c/Action/UnlockWorld.cs @@ -32,7 +32,7 @@ public class UnlockWorld: GameAction, IUnlockWorldV1 public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var unlockedWorldIdsAddress = AvatarAddress.Derive("world_ids"); var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); diff --git a/Lib9c/Action/UpdateSell.cs b/Lib9c/Action/UpdateSell.cs index 3c5354999c..2eda09e1ea 100644 --- a/Lib9c/Action/UpdateSell.cs +++ b/Lib9c/Action/UpdateSell.cs @@ -53,7 +53,7 @@ protected override void LoadPlainValueInternal( public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var states = context.PreviousState; var digestListAddress = OrderDigestListState.DeriveAddress(sellerAvatarAddress); diff --git a/Lib9c/Action/ValidatorDelegation/AllocateGuildReward.cs b/Lib9c/Action/ValidatorDelegation/AllocateGuildReward.cs new file mode 100644 index 0000000000..64d557e150 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/AllocateGuildReward.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Libplanet.Types.Assets; +using Libplanet.Types.Consensus; +using Nekoyume.ValidatorDelegation; +using Nekoyume.Module.ValidatorDelegation; +using Libplanet.Types.Blocks; +using Lib9c; +using Nekoyume.Action.Guild.Migration.LegacyModels; + +namespace Nekoyume.Action.ValidatorDelegation +{ + public sealed class AllocateGuildReward : ActionBase + { + public AllocateGuildReward() + { + } + + public override IValue PlainValue => Null.Value; + + public override void LoadPlainValue(IValue plainValue) + { + } + + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + + if (world.GetDelegationMigrationHeight() is not { } migrationHeight + || context.BlockIndex < migrationHeight) + { + return world; + } + + var repository = new ValidatorRepository(world, context); + var rewardCurrency = Currencies.Mead; + var proposerInfo = repository.GetProposerInfo(); + + if (context.LastCommit is BlockCommit lastCommit) + { + var validatorSetPower = lastCommit.Votes.Aggregate( + BigInteger.Zero, + (total, next) + => total + (next.ValidatorPower ?? BigInteger.Zero)); + + DistributeProposerReward( + repository, + rewardCurrency, + proposerInfo, + validatorSetPower, + lastCommit.Votes); + + DistributeValidatorReward( + repository, + rewardCurrency, + validatorSetPower, + lastCommit.Votes); + } + + var communityFund = repository.GetBalance(Addresses.RewardPool, rewardCurrency); + + if (communityFund.Sign > 0) + { + repository.TransferAsset( + Addresses.RewardPool, + Addresses.CommunityPool, + communityFund); + } + + return repository.World; + } + + private static void DistributeProposerReward( + ValidatorRepository repository, + Currency rewardCurrency, + ProposerInfo proposerInfo, + BigInteger validatorSetPower, + IEnumerable lastVotes) + { + FungibleAssetValue blockReward = repository.GetBalance( + Addresses.RewardPool, rewardCurrency); + + if (proposerInfo.BlockIndex != repository.ActionContext.BlockIndex - 1) + { + return; + } + + if (blockReward.Sign <= 0) + { + return; + } + + BigInteger votePowerNumerator + = lastVotes.Aggregate( + BigInteger.Zero, + (total, next) + => total + + (next.Flag == VoteFlag.PreCommit + ? next.ValidatorPower ?? BigInteger.Zero + : BigInteger.Zero)); + + BigInteger votePowerDenominator = validatorSetPower; + + if (votePowerDenominator == BigInteger.Zero) + { + return; + } + + var baseProposerReward + = (blockReward * ValidatorDelegatee.BaseProposerRewardPercentage) + .DivRem(100).Quotient; + var bonusProposerReward + = (blockReward * votePowerNumerator * ValidatorDelegatee.BonusProposerRewardPercentage) + .DivRem(votePowerDenominator * 100).Quotient; + FungibleAssetValue proposerReward = baseProposerReward + bonusProposerReward; + + if (proposerReward.Sign > 0) + { + repository.TransferAsset( + Addresses.RewardPool, + proposerInfo.Proposer, + proposerReward); + } + } + + private static void DistributeValidatorReward( + ValidatorRepository repository, + Currency rewardCurrency, + BigInteger validatorSetPower, + IEnumerable lastVotes) + { + long blockHeight = repository.ActionContext.BlockIndex; + + FungibleAssetValue rewardToAllocate + = repository.GetBalance(Addresses.RewardPool, rewardCurrency); + + if (rewardToAllocate.Sign <= 0) + { + return; + } + + if (validatorSetPower == BigInteger.Zero) + { + return; + } + + foreach (Vote vote in lastVotes) + { + if (vote.Flag == VoteFlag.Null || vote.Flag == VoteFlag.Unknown) + { + continue; + } + + if (!repository.TryGetValidatorDelegatee( + vote.ValidatorPublicKey.Address, out var validatorDelegatee)) + { + continue; + } + + BigInteger validatorPower = vote.ValidatorPower ?? BigInteger.Zero; + if (validatorPower == BigInteger.Zero) + { + continue; + } + + validatorDelegatee.AllocateReward( + rewardToAllocate, + validatorPower, + validatorSetPower, + Addresses.RewardPool, + blockHeight); + } + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/AllocateReward.cs b/Lib9c/Action/ValidatorDelegation/AllocateReward.cs new file mode 100644 index 0000000000..20e6feecd4 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/AllocateReward.cs @@ -0,0 +1,246 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Libplanet.Types.Assets; +using Libplanet.Types.Consensus; +using Libplanet.Types.Blocks; +using Nekoyume.Module; +using Nekoyume.Model.Guild; +using Nekoyume.Module.Guild; +using Nekoyume.ValidatorDelegation; +using Nekoyume.Module.ValidatorDelegation; +using Nekoyume.Action.Guild.Migration.LegacyModels; + +namespace Nekoyume.Action.ValidatorDelegation +{ + public sealed class AllocateReward : ActionBase + { + public AllocateReward() + { + } + + public override IValue PlainValue => Null.Value; + + public override void LoadPlainValue(IValue plainValue) + { + } + + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + + if (world.GetDelegationMigrationHeight() is not { } migrationHeight + || context.BlockIndex < migrationHeight) + { + return world; + } + + var rewardCurrency = world.GetGoldCurrency(); + var repository = new GuildRepository(world, context); + + if (context.LastCommit is BlockCommit lastCommit) + { + var validatorSetPower = lastCommit.Votes.Aggregate( + BigInteger.Zero, + (total, next) + => total + (next.ValidatorPower ?? BigInteger.Zero)); + + DistributeValidator(repository, rewardCurrency, validatorSetPower, lastCommit.Votes); + var validatorRepository = new ValidatorRepository(repository.World, context); + DistributeGuild(validatorRepository, rewardCurrency, validatorSetPower, lastCommit.Votes); + repository.UpdateWorld(validatorRepository.World); + DistributeGuildParticipant(repository, rewardCurrency, validatorSetPower, lastCommit.Votes); + } + + var communityFund = repository.GetBalance(Addresses.RewardPool, rewardCurrency); + + if (communityFund.Sign > 0) + { + repository.TransferAsset( + Addresses.RewardPool, + Addresses.CommunityPool, + communityFund); + } + + return repository.World; + } + + private static void DistributeValidator( + GuildRepository repository, + Currency rewardCurrency, + BigInteger validatorSetPower, + IEnumerable lastVotes) + { + FungibleAssetValue reward + = repository.GetBalance(Addresses.RewardPool, rewardCurrency); + + if (reward.Sign <= 0) + { + return; + } + + if (validatorSetPower == BigInteger.Zero) + { + return; + } + + var validatorReward = reward.DivRem(10).Quotient; + var distributed = rewardCurrency * 0; + foreach (Vote vote in lastVotes) + { + if (vote.Flag == VoteFlag.Null || vote.Flag == VoteFlag.Unknown) + { + continue; + } + + BigInteger validatorPower = vote.ValidatorPower ?? BigInteger.Zero; + if (validatorPower == BigInteger.Zero) + { + continue; + } + + var validatorAddress = vote.ValidatorPublicKey.Address; + if (!repository.TryGetGuildDelegatee( + validatorAddress, out var validatorDelegatee)) + { + continue; + } + + + FungibleAssetValue rewardEach + = (validatorReward * validatorPower).DivRem(validatorSetPower).Quotient; + + if (rewardEach.Sign > 0) + { + repository.TransferAsset(Addresses.RewardPool, validatorAddress, rewardEach); + distributed += rewardEach; + } + } + + var remainder = validatorReward - distributed; + if (remainder.Sign > 0) + { + repository.TransferAsset(Addresses.RewardPool, Addresses.CommunityPool, remainder); + } + } + + private static void DistributeGuild( + ValidatorRepository repository, + Currency rewardCurrency, + BigInteger validatorSetPower, + IEnumerable lastVotes) + { + long blockHeight = repository.ActionContext.BlockIndex; + + FungibleAssetValue reward + = repository.GetBalance(Addresses.RewardPool, rewardCurrency); + + if (reward.Sign <= 0) + { + return; + } + + if (validatorSetPower == BigInteger.Zero) + { + return; + } + + var guildReward = reward.DivRem(10).Quotient; + var distributed = rewardCurrency * 0; + + foreach (Vote vote in lastVotes) + { + if (vote.Flag == VoteFlag.Null || vote.Flag == VoteFlag.Unknown) + { + continue; + } + + BigInteger validatorPower = vote.ValidatorPower ?? BigInteger.Zero; + if (validatorPower == BigInteger.Zero) + { + continue; + } + + var validatorAddress = vote.ValidatorPublicKey.Address; + if (!repository.TryGetValidatorDelegatee( + validatorAddress, out var validatorDelegatee)) + { + continue; + } + + + FungibleAssetValue rewardEach + = (guildReward * validatorPower).DivRem(validatorSetPower).Quotient; + + if (rewardEach.Sign > 0) + { + repository.TransferAsset( + Addresses.RewardPool, validatorDelegatee.RewardPoolAddress, rewardEach); + validatorDelegatee.CollectRewards(blockHeight); + distributed += rewardEach; + } + } + + var remainder = guildReward - distributed; + if (remainder.Sign > 0) + { + repository.TransferAsset(Addresses.RewardPool, Addresses.CommunityPool, remainder); + } + } + + private static void DistributeGuildParticipant( + GuildRepository repository, + Currency rewardCurrency, + BigInteger validatorSetPower, + IEnumerable lastVotes) + { + long blockHeight = repository.ActionContext.BlockIndex; + + FungibleAssetValue reward + = repository.GetBalance(Addresses.RewardPool, rewardCurrency); + + if (reward.Sign <= 0) + { + return; + } + + if (validatorSetPower == BigInteger.Zero) + { + return; + } + + foreach (Vote vote in lastVotes) + { + if (vote.Flag == VoteFlag.Null || vote.Flag == VoteFlag.Unknown) + { + continue; + } + + BigInteger validatorPower = vote.ValidatorPower ?? BigInteger.Zero; + if (validatorPower == BigInteger.Zero) + { + continue; + } + + var validatorAddress = vote.ValidatorPublicKey.Address; + if (!repository.TryGetGuildDelegatee( + validatorAddress, out var validatorDelegatee)) + { + continue; + } + + FungibleAssetValue rewardEach + = (reward * validatorPower).DivRem(validatorSetPower).Quotient; + + if (rewardEach.Sign > 0) + { + repository.TransferAsset(Addresses.RewardPool, validatorDelegatee.RewardPoolAddress, rewardEach); + validatorDelegatee.CollectRewards(blockHeight); + } + } + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/ClaimValidatorRewardSelf.cs b/Lib9c/Action/ValidatorDelegation/ClaimValidatorRewardSelf.cs new file mode 100644 index 0000000000..412e86f6c5 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/ClaimValidatorRewardSelf.cs @@ -0,0 +1,53 @@ +using System; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Libplanet.Crypto; +using Nekoyume.ValidatorDelegation; +using Nekoyume.Model.Guild; + +namespace Nekoyume.Action.ValidatorDelegation +{ + [ActionType(TypeIdentifier)] + public sealed class ClaimValidatorRewardSelf : ActionBase + { + public const string TypeIdentifier = "claim_validator_reward_self"; + + public ClaimValidatorRewardSelf() { } + + public Address ValidatorDelegatee { get; private set; } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", Null.Value); + + public override void LoadPlainValue(IValue plainValue) + { + var root = (Dictionary)plainValue; + if (plainValue is not Dictionary || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not Null) + { + throw new InvalidCastException(); + } + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + var repository = new ValidatorRepository(world, context); + var validatorDelegatee = repository.GetValidatorDelegatee(context.Signer); + var validatorDelegator = repository.GetValidatorDelegator(context.Signer); + validatorDelegator.ClaimReward(validatorDelegatee, context.BlockIndex); + + var guildRepository = new GuildRepository(repository); + var guildDelegatee = guildRepository.GetGuildDelegatee(context.Signer); + var guildDelegator = guildRepository.GetGuildDelegator(context.Signer); + guildDelegator.ClaimReward(guildDelegatee, context.BlockIndex); + + return guildRepository.World; + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/DelegateValidator.cs b/Lib9c/Action/ValidatorDelegation/DelegateValidator.cs new file mode 100644 index 0000000000..fbfcad76ed --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/DelegateValidator.cs @@ -0,0 +1,66 @@ +using System; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Libplanet.Types.Assets; +using Nekoyume.ValidatorDelegation; +using Nekoyume.Model.Guild; + +namespace Nekoyume.Action.ValidatorDelegation +{ + [ActionType(TypeIdentifier)] + public sealed class DelegateValidator : ActionBase + { + public const string TypeIdentifier = "delegate_validator"; + + public DelegateValidator() { } + + public DelegateValidator(FungibleAssetValue fav) + { + FAV = fav; + } + + public FungibleAssetValue FAV { get; private set; } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", List.Empty + .Add(FAV.Serialize())); + + public override void LoadPlainValue(IValue plainValue) + { + var root = (Dictionary)plainValue; + if (plainValue is not Dictionary || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not List values) + { + throw new InvalidCastException(); + } + + FAV = new FungibleAssetValue(values[0]); + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + if (FAV.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(FAV), FAV, "Fungible asset value must be positive."); + } + + var guildRepository = new GuildRepository(context.PreviousState, context); + var guildDelegatee = guildRepository.GetGuildDelegatee(context.Signer); + var guildDelegator = guildRepository.GetGuildDelegator(context.Signer); + guildDelegator.Delegate(guildDelegatee, FAV, context.BlockIndex); + + var validatorRepository = new ValidatorRepository(guildRepository); + var validatorDelegatee = validatorRepository.GetValidatorDelegatee(context.Signer); + var validatorDelegator = validatorRepository.GetValidatorDelegator(context.Signer); + validatorDelegatee.Bond(validatorDelegator, FAV, context.BlockIndex); + + return validatorRepository.World; + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/Mortgage.cs b/Lib9c/Action/ValidatorDelegation/Mortgage.cs new file mode 100644 index 0000000000..bd65d6e7d7 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/Mortgage.cs @@ -0,0 +1,62 @@ +using Bencodex.Types; +using Lib9c; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Types.Assets; + +namespace Nekoyume.Action.ValidatorDelegation +{ + /// + /// An action for mortgage gas fee for a transaction. + /// Should be executed at the beginning of the tx. + /// + public sealed class Mortgage : ActionBase + { + /// + /// Creates a new instance of . + /// + public Mortgage() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var state = context.PreviousState; + if (context.MaxGasPrice is not { Sign: > 0 } realGasPrice) + { + return state; + } + + var gasOwned = state.GetBalance(context.Signer, realGasPrice.Currency); + var gasRequired = realGasPrice * GasTracer.GasAvailable; + var gasToMortgage = gasOwned < gasRequired ? gasOwned : gasRequired; + if (gasOwned < gasRequired) + { + // var msg = + // $"The account {context.Signer}'s balance of {realGasPrice.Currency} is " + + // "insufficient to pay gas fee: " + + // $"{gasOwned} < {realGasPrice * gasLimit}."; + GasTracer.CancelTrace(); + // throw new InsufficientBalanceException(msg, context.Signer, gasOwned); + } + + if (gasToMortgage.Sign > 0) + { + return state.TransferAsset( + context, context.Signer, Addresses.MortgagePool, gasToMortgage); + } + + return state; + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/PromoteValidator.cs b/Lib9c/Action/ValidatorDelegation/PromoteValidator.cs new file mode 100644 index 0000000000..63f892c011 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/PromoteValidator.cs @@ -0,0 +1,99 @@ +using System; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module.ValidatorDelegation; +using Nekoyume.ValidatorDelegation; +using Nekoyume.Model.Guild; +using Nekoyume.Module.Guild; + +namespace Nekoyume.Action.ValidatorDelegation +{ + [ActionType(TypeIdentifier)] + public sealed class PromoteValidator : ActionBase + { + public const string TypeIdentifier = "promote_validator"; + + public PromoteValidator() { } + + public PromoteValidator(PublicKey publicKey, FungibleAssetValue fav) + : this(publicKey, fav, ValidatorDelegatee.DefaultCommissionPercentage) + { + } + + public PromoteValidator(PublicKey publicKey, FungibleAssetValue fav, BigInteger commissionPercentage) + { + PublicKey = publicKey; + FAV = fav; + CommissionPercentage = commissionPercentage; + } + + public PublicKey PublicKey { get; private set; } + + public FungibleAssetValue FAV { get; private set; } + + public BigInteger CommissionPercentage { get; private set; } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", List.Empty + .Add(PublicKey.Format(true)) + .Add(FAV.Serialize()) + .Add(CommissionPercentage)); + + public override void LoadPlainValue(IValue plainValue) + { + var root = (Dictionary)plainValue; + if (plainValue is not Dictionary || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not List values) + { + throw new InvalidCastException(); + } + + PublicKey = new PublicKey(((Binary)values[0]).ByteArray); + FAV = new FungibleAssetValue(values[1]); + CommissionPercentage = (Integer)values[2]; + } + + // TODO: Remove this with ExecutePublic when to deliver features to users. + public override IWorld Execute(IActionContext context) + { + var world = ExecutePublic(context); + // if (context.Signer != ValidatorConfig.PlanetariumValidatorAddress) + // { + // throw new InvalidOperationException( + // $"This action is not allowed for {context.Signer}."); + // } + + return world; + } + + public IWorld ExecutePublic(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + + if (!PublicKey.Address.Equals(context.Signer)) + { + throw new ArgumentException("The public key does not match the signer."); + } + + var repository = new ValidatorRepository(world, context); + var validatorDelegatee = repository.CreateValidatorDelegatee(PublicKey, CommissionPercentage); + var validatorDelegator = repository.GetValidatorDelegator(context.Signer); + validatorDelegatee.Bond(validatorDelegator, FAV, context.BlockIndex); + + var guildRepository = new GuildRepository(repository); + var guildDelegatee = guildRepository.CreateGuildDelegatee(context.Signer); + var guildDelegator = guildRepository.GetGuildDelegator(context.Signer); + guildDelegator.Delegate(guildDelegatee, FAV, context.BlockIndex); + + return guildRepository.World; + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/RecordProposer.cs b/Lib9c/Action/ValidatorDelegation/RecordProposer.cs new file mode 100644 index 0000000000..dc1fc59089 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/RecordProposer.cs @@ -0,0 +1,49 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Nekoyume.Action.Guild.Migration.LegacyModels; +using Nekoyume.Extensions; +using Nekoyume.ValidatorDelegation; +using static Nekoyume.Model.WorldInformation; + +namespace Nekoyume.Action.ValidatorDelegation +{ + /// + /// An action for recording proposer of the block to use in next block's reward distribution. + /// + public sealed class RecordProposer : ActionBase + { + /// + /// Creates a new instance of . + /// + public RecordProposer() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + + if (world.GetDelegationMigrationHeight() is null) + { + return world; + } + + return world.MutateAccount( + Addresses.ValidatorList, + account => account.SetState( + ProposerInfo.Address, + new ProposerInfo(context.BlockIndex, context.Miner).Bencoded)); + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/Refund.cs b/Lib9c/Action/ValidatorDelegation/Refund.cs new file mode 100644 index 0000000000..6ee1b8731a --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/Refund.cs @@ -0,0 +1,50 @@ +using Bencodex.Types; +using Lib9c; +using Libplanet.Action; +using Libplanet.Action.State; + +namespace Nekoyume.Action.ValidatorDelegation +{ + /// + /// An action for refund gas fee for a transaction. + /// Should be executed at the beginning of the tx. + /// + public sealed class Refund : ActionBase + { + /// + /// Creates a new instance of . + /// + public Refund() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + if (context.MaxGasPrice is not { Sign: > 0 } realGasPrice) + { + return world; + } + + // Need to check if this matches GasTracer.GasAvailable? + var remaining = world.GetBalance(Addresses.MortgagePool, realGasPrice.Currency); + if (remaining.Sign <= 0) + { + return world; + } + + return world.TransferAsset( + context, Addresses.MortgagePool, context.Signer, remaining); + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs b/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs new file mode 100644 index 0000000000..280f7f7a8e --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs @@ -0,0 +1,142 @@ +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Libplanet.Crypto; +using Nekoyume.ValidatorDelegation; +using Nekoyume.Delegation; +using System; +using System.Linq; +using System.Collections.Immutable; +using Libplanet.Types.Assets; +using System.Numerics; +using Nekoyume.Model.Guild; +using Nekoyume.Module.Guild; +using Nekoyume.TypedAddress; +using Nekoyume.Module; +using Nekoyume.Model.Stake; +using Lib9c; +using Nekoyume.Action.Guild.Migration.LegacyModels; + +namespace Nekoyume.Action.ValidatorDelegation +{ + public sealed class ReleaseValidatorUnbondings : ActionBase + { + public ReleaseValidatorUnbondings() { } + + public ReleaseValidatorUnbondings(Address validatorDelegatee) + { + } + + public override IValue PlainValue => Null.Value; + + public override void LoadPlainValue(IValue plainValue) + { + } + + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + + if (world.GetDelegationMigrationHeight() is null) + { + return world; + } + + var repository = new GuildRepository(world, context); + var unbondingSet = repository.GetUnbondingSet(); + var unbondings = unbondingSet.UnbondingsToRelease(context.BlockIndex); + + unbondings = unbondings.Select(unbonding => unbonding.Release(context.BlockIndex)).ToImmutableArray(); + + foreach (var unbonding in unbondings) + { + switch (unbonding) + { + case UnbondLockIn unbondLockIn: + { + repository.SetUnbondLockIn(unbondLockIn); + repository.UpdateWorld( + Unstake(repository.World, context, unbondLockIn.DelegatorAddress)); + } + break; + case RebondGrace rebondGrace: + repository.SetRebondGrace(rebondGrace); + break; + default: + throw new InvalidOperationException("Invalid unbonding type."); + } + } + + repository.SetUnbondingSet(unbondingSet.SetUnbondings(unbondings)); + + return repository.World; + } + + private IWorld Unstake(IWorld world, IActionContext context, Address address) + { + var agentAddress = new AgentAddress(address); + var guildRepository = new GuildRepository(world, context); + var goldCurrency = world.GetGoldCurrency(); + if (guildRepository.TryGetGuildParticipant(agentAddress, out var guildParticipant)) + { + var guild = guildRepository.GetGuild(guildParticipant.GuildAddress); + var stakeStateAddress = guildParticipant.DelegationPoolAddress; + var gg = world.GetBalance(stakeStateAddress, ValidatorDelegatee.ValidatorDelegationCurrency); + if (gg.Sign > 0) + { + var (ncg, _) = ConvertToGoldCurrency(gg, goldCurrency); + world = world.BurnAsset(context, stakeStateAddress, gg); + world = world.TransferAsset( + context, stakeStateAddress, address, ncg); + } + } + else if (!IsValidator(world, context, address)) + { + var stakeStateAddress = StakeState.DeriveAddress(address); + var gg = world.GetBalance(stakeStateAddress, Currencies.GuildGold); + if (gg.Sign > 0) + { + var (ncg, _) = ConvertToGoldCurrency(gg, goldCurrency); + world = world.BurnAsset(context, stakeStateAddress, gg); + world = world.TransferAsset( + context, stakeStateAddress, address, ncg); + } + } + + return world; + } + + private static bool IsValidator(IWorld world, IActionContext context, Address address) + { + var repository = new ValidatorRepository(world, context); + try + { + repository.GetValidatorDelegatee(address); + return true; + } + catch (FailedLoadStateException) + { + return false; + } + } + + private static (FungibleAssetValue Gold, FungibleAssetValue Remainder) + ConvertToGoldCurrency(FungibleAssetValue fav, Currency targetCurrency) + { + var sourceCurrency = fav.Currency; + if (targetCurrency.DecimalPlaces < sourceCurrency.DecimalPlaces) + { + var d = BigInteger.Pow(10, sourceCurrency.DecimalPlaces - targetCurrency.DecimalPlaces); + var value = FungibleAssetValue.FromRawValue(targetCurrency, fav.RawValue / d); + var fav2 = FungibleAssetValue.FromRawValue(sourceCurrency, value.RawValue * d); + return (value, fav - fav2); + } + else + { + var d = BigInteger.Pow(10, targetCurrency.DecimalPlaces - sourceCurrency.DecimalPlaces); + var value = FungibleAssetValue.FromRawValue(targetCurrency, fav.RawValue * d); + return (value, targetCurrency * 0); + } + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/Reward.cs b/Lib9c/Action/ValidatorDelegation/Reward.cs new file mode 100644 index 0000000000..4d45e425a6 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/Reward.cs @@ -0,0 +1,56 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; + +namespace Nekoyume.Action.ValidatorDelegation +{ + /// + /// An action for reward for a transaction. + /// Should be executed at the beginning of the tx. + /// + public sealed class Reward : ActionBase + { + /// + /// Creates a new instance of . + /// + public Reward() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + if (context.MaxGasPrice is not { Sign: > 0 } realGasPrice) + { + return world; + } + + if (GasTracer.GasUsed <= 0) + { + return world; + } + + var gasMortgaged = world.GetBalance(Addresses.MortgagePool, realGasPrice.Currency); + var gasUsedPrice = realGasPrice * GasTracer.GasUsed; + var gasToTransfer = gasMortgaged < gasUsedPrice ? gasMortgaged : gasUsedPrice; + + if (gasToTransfer.Sign <= 0) + { + return world; + } + + return world.TransferAsset( + context, Addresses.MortgagePool, Addresses.GasPool, gasToTransfer); + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/SetValidatorCommission.cs b/Lib9c/Action/ValidatorDelegation/SetValidatorCommission.cs new file mode 100644 index 0000000000..ebf99fbb4b --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/SetValidatorCommission.cs @@ -0,0 +1,59 @@ +using System; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Libplanet.Crypto; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Action.ValidatorDelegation +{ + [ActionType(TypeIdentifier)] + public sealed class SetValidatorCommission : ActionBase + { + public const string TypeIdentifier = "set_validator_commission"; + + public SetValidatorCommission() { } + + public SetValidatorCommission(Address validatorDelegatee, BigInteger commissionPercentage) + { + ValidatorDelegatee = validatorDelegatee; + CommissionPercentage = commissionPercentage; + } + + public Address ValidatorDelegatee { get; private set; } + + public BigInteger CommissionPercentage { get; private set; } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", List.Empty + .Add(ValidatorDelegatee.Bencoded) + .Add(CommissionPercentage)); + + public override void LoadPlainValue(IValue plainValue) + { + var root = (Dictionary)plainValue; + if (plainValue is not Dictionary || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not List values) + { + throw new InvalidCastException(); + } + + ValidatorDelegatee = new Address(values[0]); + CommissionPercentage = (Integer)values[1]; + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + var repository = new ValidatorRepository(world, context); + repository.SetCommissionPercentage(ValidatorDelegatee, CommissionPercentage, context.BlockIndex); + + return repository.World; + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/SlashValidator.cs b/Lib9c/Action/ValidatorDelegation/SlashValidator.cs new file mode 100644 index 0000000000..59ca7beba6 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/SlashValidator.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Types.Consensus; +using Libplanet.Types.Evidence; +using Nekoyume.Action.Guild.Migration.LegacyModels; +using Nekoyume.Model.Guild; +using Nekoyume.Module.ValidatorDelegation; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Action.ValidatorDelegation +{ + public sealed class SlashValidator : ActionBase + { + public SlashValidator() + { + } + + public static BigInteger DuplicateVoteSlashFactor => 10; + + public static BigInteger LivenessSlashFactor => 10; + + public static long AbstainJailTime => 10L; + + public override IValue PlainValue => Null.Value; + + public override void LoadPlainValue(IValue plainValue) + { + } + + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + + if (world.GetDelegationMigrationHeight() is null) + { + return world; + } + + var repository = new ValidatorRepository(world, context); + + var abstainHistory = repository.GetAbstainHistory(); + var abstainsToSlash = abstainHistory.FindToSlashAndAdd( + context.LastCommit.Votes.Where(vote => vote.Flag == VoteFlag.Null) + .Select(vote => vote.ValidatorPublicKey), + context.BlockIndex); + repository.SetAbstainHistory(abstainHistory); + + foreach (var abstain in abstainsToSlash) + { + var validatorDelegatee = repository.GetValidatorDelegatee(abstain.Address); + validatorDelegatee.Slash(LivenessSlashFactor, context.BlockIndex, context.BlockIndex); + validatorDelegatee.Jail(context.BlockIndex + AbstainJailTime); + + var guildRepository = new GuildRepository(repository.World, repository.ActionContext); + var validatorDelegateeForGuildParticipant = guildRepository.GetGuildDelegatee(abstain.Address); + validatorDelegateeForGuildParticipant.Slash(LivenessSlashFactor, context.BlockIndex, context.BlockIndex); + repository.UpdateWorld(guildRepository.World); + } + + foreach (var evidence in context.Evidence) + { + switch (evidence) + { + case DuplicateVoteEvidence e: + if (e.Height > context.BlockIndex) + { + throw new Exception("Evidence height is greater than block index."); + } + + var validatorDelegatee = repository.GetValidatorDelegatee(e.TargetAddress); + validatorDelegatee.Slash(DuplicateVoteSlashFactor, e.Height, context.BlockIndex); + validatorDelegatee.Tombstone(); + + var guildRepository = new GuildRepository(repository.World, repository.ActionContext); + var validatorDelegateeForGuildParticipant = guildRepository.GetGuildDelegatee(e.TargetAddress); + validatorDelegateeForGuildParticipant.Slash(DuplicateVoteSlashFactor, e.Height, context.BlockIndex); + repository.UpdateWorld(guildRepository.World); + break; + default: + break; + } + } + + return repository.World; + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/UndelegateValidator.cs b/Lib9c/Action/ValidatorDelegation/UndelegateValidator.cs new file mode 100644 index 0000000000..06a9898954 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/UndelegateValidator.cs @@ -0,0 +1,66 @@ +using System; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Nekoyume.ValidatorDelegation; +using Nekoyume.Model.Guild; + +namespace Nekoyume.Action.ValidatorDelegation +{ + [ActionType(TypeIdentifier)] + public sealed class UndelegateValidator : ActionBase + { + public const string TypeIdentifier = "undelegate_validator"; + + public UndelegateValidator() { } + + public UndelegateValidator(BigInteger share) + { + Share = share; + } + + public BigInteger Share { get; private set; } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", List.Empty + .Add(Share)); + + public override void LoadPlainValue(IValue plainValue) + { + var root = (Dictionary)plainValue; + if (plainValue is not Dictionary || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not List values) + { + throw new InvalidCastException(); + } + + Share = (Integer)values[0]; + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + if (Share.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(Share), Share, "Share must be positive."); + } + + var guildRepository = new GuildRepository(context.PreviousState, context); + var guildDelegatee = guildRepository.GetGuildDelegatee(context.Signer); + var guildDelegator = guildRepository.GetGuildDelegator(context.Signer); + guildDelegator.Undelegate(guildDelegatee, Share, context.BlockIndex); + + var validatorRepository = new ValidatorRepository(guildRepository); + var validatorDelegatee = validatorRepository.GetValidatorDelegatee(context.Signer); + var validatorDelegator = validatorRepository.GetValidatorDelegator(context.Signer); + validatorDelegatee.Unbond(validatorDelegator, Share, context.BlockIndex); + + return validatorRepository.World; + } + } +} diff --git a/Lib9c/Action/Guild/CancelGuildApplication.cs b/Lib9c/Action/ValidatorDelegation/UnjailValidator.cs similarity index 52% rename from Lib9c/Action/Guild/CancelGuildApplication.cs rename to Lib9c/Action/ValidatorDelegation/UnjailValidator.cs index d973da8f70..a232dcc717 100644 --- a/Lib9c/Action/Guild/CancelGuildApplication.cs +++ b/Lib9c/Action/ValidatorDelegation/UnjailValidator.cs @@ -1,17 +1,17 @@ using System; using Bencodex.Types; -using Libplanet.Action; using Libplanet.Action.State; -using Nekoyume.Extensions; -using Nekoyume.Module.Guild; +using Libplanet.Action; +using Nekoyume.ValidatorDelegation; -namespace Nekoyume.Action.Guild +namespace Nekoyume.Action.ValidatorDelegation { - // TODO(GUILD-FEATURE): Enable again when Guild features are enabled. - // [ActionType(TypeIdentifier)] - public class CancelGuildApplication : ActionBase + [ActionType(TypeIdentifier)] + public sealed class UnjailValidator : ActionBase { - public const string TypeIdentifier = "cancel_guild_application"; + public const string TypeIdentifier = "unjail_validator"; + + public UnjailValidator() { } public override IValue PlainValue => Dictionary.Empty .Add("type_id", TypeIdentifier) @@ -19,8 +19,7 @@ public class CancelGuildApplication : ActionBase public override void LoadPlainValue(IValue plainValue) { - var root = (Dictionary)plainValue; - if (plainValue is not Dictionary || + if (plainValue is not Dictionary root || !root.TryGetValue((Text)"values", out var rawValues) || rawValues is not Null) { @@ -30,12 +29,14 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); var world = context.PreviousState; - var signer = context.GetAgentAddress(); + var repository = new ValidatorRepository(world, context); + var delegatee = repository.GetValidatorDelegatee(context.Signer); + delegatee.Unjail(context.BlockIndex); - return world.CancelGuildApplication(signer); + return repository.World; } } } diff --git a/Lib9c/Action/ValidatorDelegation/UpdateValidators.cs b/Lib9c/Action/ValidatorDelegation/UpdateValidators.cs new file mode 100644 index 0000000000..fb1c87075b --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/UpdateValidators.cs @@ -0,0 +1,64 @@ +using System.Linq; +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Libplanet.Types.Consensus; +using Nekoyume.ValidatorDelegation; +using Nekoyume.Model.Guild; +using Nekoyume.Action.Guild.Migration.LegacyModels; + +namespace Nekoyume.Action.ValidatorDelegation +{ + public sealed class UpdateValidators : ActionBase + { + public UpdateValidators() { } + + public override IValue PlainValue => Null.Value; + + public override void LoadPlainValue(IValue plainValue) + { + } + + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + + if (world.GetDelegationMigrationHeight() is null) + { + return world; + } + + var prevValidators = world.GetValidatorSet().Validators; + var repository = new ValidatorRepository(world, context); + var validators = repository.GetValidatorList().ActiveSet(); + + foreach (var deactivated in prevValidators.Select(v => v.OperatorAddress) + .Except(validators.Select(v => v.OperatorAddress))) + { + var validatorDelegatee = repository.GetValidatorDelegatee(deactivated); + validatorDelegatee.Deactivate(); + repository.SetValidatorDelegatee(validatorDelegatee); + var guildRepository = new GuildRepository(repository.World, repository.ActionContext); + var validatorDelegateeForGuildParticipant = guildRepository.GetGuildDelegatee(deactivated); + validatorDelegateeForGuildParticipant.Deactivate(); + guildRepository.SetGuildDelgatee(validatorDelegateeForGuildParticipant); + repository.UpdateWorld(guildRepository.World); + } + + foreach (var activated in validators.Select(v => v.OperatorAddress) + .Except(prevValidators.Select(v => v.OperatorAddress))) + { + var validatorDelegatee = repository.GetValidatorDelegatee(activated); + validatorDelegatee.Activate(); + repository.SetValidatorDelegatee(validatorDelegatee); + var guildRepository = new GuildRepository(repository.World, repository.ActionContext); + var validatorDelegateeForGuildParticipant = guildRepository.GetGuildDelegatee(activated); + validatorDelegateeForGuildParticipant.Activate(); + guildRepository.SetGuildDelgatee(validatorDelegateeForGuildParticipant); + repository.UpdateWorld(guildRepository.World); + } + + return repository.World.SetValidatorSet(new ValidatorSet(validators)); + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/ValidatorConfig.cs b/Lib9c/Action/ValidatorDelegation/ValidatorConfig.cs new file mode 100644 index 0000000000..985f631c29 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/ValidatorConfig.cs @@ -0,0 +1,13 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.ValidatorDelegation +{ + public static class ValidatorConfig + { + public static readonly Address PlanetariumValidatorAddress = + new Address("0x8E1b572db70aB80bb02783A0D2c594A0edE6db28"); + + public static readonly PublicKey PlanetariumValidatorPublicKey = + PublicKey.FromHex("03a0f95711564d10c60ba1889d068c26cb8e5fcd5211d5aeb8810e133d629aa306"); + } +} diff --git a/Lib9c/Action/ValidatorSetOperate.cs b/Lib9c/Action/ValidatorSetOperate.cs index 0057258ef7..a2ee327bd9 100644 --- a/Lib9c/Action/ValidatorSetOperate.cs +++ b/Lib9c/Action/ValidatorSetOperate.cs @@ -71,7 +71,7 @@ public override int GetHashCode() => public override IWorld Execute(IActionContext context) { - context.UseGas(1); + GasTracer.UseGas(1); if (Error != null) { throw new InvalidOperationException(Error); diff --git a/Lib9c/Addresses.cs b/Lib9c/Addresses.cs index f5881717af..630c84f818 100644 --- a/Lib9c/Addresses.cs +++ b/Lib9c/Addresses.cs @@ -60,6 +60,9 @@ public static class Addresses public static readonly Address ExploreBoard = new("0000000000000000000000000000000000000102"); public static readonly Address ExplorerList = new("0000000000000000000000000000000000000103"); + public static readonly Address MortgagePool = new Address("0000000000000000000000000000000000100000"); + public static readonly Address GasPool = new Address("0000000000000000000000000000000000100001"); + #region Guild /// @@ -82,6 +85,12 @@ public static class Addresses /// public static readonly Address GuildParticipant = new("0000000000000000000000000000000000000203"); + /// + /// An address of an account having + /// + public static readonly Address GuildRejoinCooldown = + new Address("0000000000000000000000000000000000000204"); + /// /// Build an of an , /// represented as `agentAddress` ↔ , indicates whether @@ -94,6 +103,145 @@ public static Address GetGuildBanAccountAddress(Address guildAddress) => public static readonly Address EmptyAccountAddress = new("ffffffffffffffffffffffffffffffffffffffff"); + /// + /// An address of an account having . + /// + public static readonly Address GuildDelegateeMetadata + = new Address("0000000000000000000000000000000000000210"); + + /// + /// An address of an account having . + /// + public static readonly Address GuildDelegatorMetadata + = new Address("0000000000000000000000000000000000000211"); + + /// + /// An address of an account having . + /// + public static readonly Address GuildBond + = new Address("0000000000000000000000000000000000000212"); + + /// + /// An address of an account having . + /// + public static readonly Address GuildUnbondLockIn + = new Address("0000000000000000000000000000000000000213"); + + /// + /// An address of an account having . + /// + public static readonly Address GuildRebondGrace + = new Address("0000000000000000000000000000000000000214"); + + /// + /// An address of an account having . + /// + public static readonly Address GuildLumpSumRewardsRecord + = new Address("0000000000000000000000000000000000000215"); + + /// + /// An address of an account having . + /// + public static readonly Address GuildUnbondingSet + = new Address("0000000000000000000000000000000000000216"); + + #endregion + + #region Validator + /// + /// An address of an account having . + /// + public static readonly Address ValidatorDelegatee + = new Address("0000000000000000000000000000000000000300"); + + /// + /// An address of an account having . + /// + public static readonly Address ValidatorDelegator + = new Address("0000000000000000000000000000000000000301"); + + /// + /// An address of an account having . + /// + public static readonly Address ValidatorDelegateeMetadata + = new Address("0000000000000000000000000000000000000302"); + + /// + /// An address of an account having . + /// + public static readonly Address ValidatorDelegatorMetadata + = new Address("0000000000000000000000000000000000000303"); + + /// + /// An address of an account having . + /// + public static readonly Address ValidatorBond + = new Address("0000000000000000000000000000000000000304"); + + /// + /// An address of an account having . + /// + public static readonly Address ValidatorUnbondLockIn + = new Address("0000000000000000000000000000000000000305"); + + /// + /// An address of an account having . + /// + public static readonly Address ValidatorRebondGrace + = new Address("0000000000000000000000000000000000000306"); + + /// + /// An address of an account having . + /// + public static readonly Address ValidatorLumpSumRewardsRecord + = new Address("0000000000000000000000000000000000000307"); + + /// + /// An address of an account having . + /// + public static readonly Address ValidatorUnbondingSet + = new Address("0000000000000000000000000000000000000308"); + + /// + /// An address of an account having . + /// + public static readonly Address AbstainHistory + = new Address("0000000000000000000000000000000000000309"); + + /// + /// An address of an account having . + /// + public static readonly Address ValidatorList + = new Address("0000000000000000000000000000000000000310"); + + /// + /// An address for reward pool of validators. + /// + public static readonly Address RewardPool + = new Address("0000000000000000000000000000000000000311"); + + /// + /// An address for community fund. + /// + public static readonly Address CommunityPool + = new Address("0000000000000000000000000000000000000312"); + + /// + /// An address for non validator. + /// + public static readonly Address NonValidatorDelegatee + = new Address("0000000000000000000000000000000000000313"); + + #endregion + + #region Migration + + /// + /// An account address for migration. + /// + public static readonly Address Migration + = new Address("0000000000000000000000000000000000000400"); + #endregion public static Address GetSheetAddress() where T : ISheet => GetSheetAddress(typeof(T).Name); diff --git a/Lib9c/Currencies.cs b/Lib9c/Currencies.cs index c13a0620b2..4f702b8548 100644 --- a/Lib9c/Currencies.cs +++ b/Lib9c/Currencies.cs @@ -65,6 +65,9 @@ public static class Currencies public static readonly Currency Mead = Currency.Legacy("Mead", 18, null); + public static readonly Currency GuildGold = Currency.Uncapped( + "GUILD_GOLD", 18, null); + /// /// Covers the reward.CurrencyTicker is following cases: /// - Currencies.Crystal.Ticker diff --git a/Lib9c/Delegation/Bond.cs b/Lib9c/Delegation/Bond.cs new file mode 100644 index 0000000000..e0d057d4a2 --- /dev/null +++ b/Lib9c/Delegation/Bond.cs @@ -0,0 +1,127 @@ +#nullable enable +using System; +using System.Numerics; +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; + +namespace Nekoyume.Delegation +{ + public sealed class Bond : IBencodable, IEquatable + { + public Bond(Address address) + : this(address, BigInteger.Zero, null) + { + } + + public Bond(Address address, IValue bencoded) + : this(address, (List)bencoded) + { + } + + public Bond(Address address, List bencoded) + : this( + address, + (Integer)bencoded[0], + bencoded[1] is Integer share ? share : null) + { + } + + private Bond(Address address, BigInteger share, long? lastDistributeHeight) + { + if (share.Sign < 0) + { + throw new ArgumentOutOfRangeException( + nameof(share), + share, + "Share must be non-negative."); + } + + if (lastDistributeHeight < 0) + { + throw new ArgumentOutOfRangeException( + nameof(lastDistributeHeight), + lastDistributeHeight, + "Last distribute height must be non-negative."); + } + + Address = address; + Share = share; + LastDistributeHeight = lastDistributeHeight; + } + + public Address Address { get; } + + public BigInteger Share { get; } + + public long? LastDistributeHeight { get; } + + public bool IsEmpty => Share.IsZero && LastDistributeHeight is null; + + public List Bencoded => List.Empty + .Add(Share) + .Add(LastDistributeHeight.HasValue + ? new Integer(LastDistributeHeight.Value) + : Null.Value); + + IValue IBencodable.Bencoded => Bencoded; + + public override bool Equals(object? obj) + => obj is Bond other && Equals(other); + + public bool Equals(Bond? other) + => ReferenceEquals(this, other) + || (other is Bond bond + && Address.Equals(bond.Address) + && Share.Equals(bond.Share) + && LastDistributeHeight.Equals(bond.LastDistributeHeight)); + + public override int GetHashCode() + => Address.GetHashCode(); + + internal Bond AddShare(BigInteger share) + { + if (share.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(share), + share, + "share must be positive."); + } + + return new Bond(Address, Share + share, LastDistributeHeight); + } + + internal Bond SubtractShare(BigInteger share) + { + if (share > Share) + { + throw new ArgumentOutOfRangeException( + nameof(share), + share, + "share must be less than or equal to the current share."); + } + + return new Bond(Address, Share - share, LastDistributeHeight); + } + + internal Bond UpdateLastDistributeHeight(long height) + { + // TODO: [GuildMigration] Revive below check after migration + // if (LastDistributeHeight.HasValue && LastDistributeHeight >= height) + // { + // throw new ArgumentOutOfRangeException( + // nameof(height), + // height, + // "height must be greater than the last distribute height."); + // } + + return new Bond(Address, Share, height); + } + + internal Bond ClearLastDistributeHeight() + { + return new Bond(Address, Share, null); + } + } +} diff --git a/Lib9c/Delegation/CurrencyComparer.cs b/Lib9c/Delegation/CurrencyComparer.cs new file mode 100644 index 0000000000..8f560b75d4 --- /dev/null +++ b/Lib9c/Delegation/CurrencyComparer.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Collections.Generic; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + internal class CurrencyComparer : IComparer + { + public int Compare(Currency x, Currency y) + => ByteArrayCompare(x.Hash.ToByteArray(), y.Hash.ToByteArray()); + + private static int ByteArrayCompare(byte[] x, byte[] y) + { + for (int i = 0; i < x.Length; i++) + { + if (x[i] < y[i]) + { + return -1; + } + else if (x[i] > y[i]) + { + return 1; + } + } + + return 0; + } + } +} diff --git a/Lib9c/Delegation/Delegatee.cs b/Lib9c/Delegation/Delegatee.cs new file mode 100644 index 0000000000..e18ae6d3e8 --- /dev/null +++ b/Lib9c/Delegation/Delegatee.cs @@ -0,0 +1,486 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + public abstract class Delegatee : IDelegatee + where T : Delegator + where TSelf : Delegatee + { + public Delegatee( + Address address, + Address accountAddress, + Currency delegationCurrency, + IEnumerable rewardCurrencies, + Address delegationPoolAddress, + Address rewardPoolAddress, + Address rewardRemainderPoolAddress, + Address slashedPoolAddress, + long unbondingPeriod, + int maxUnbondLockInEntries, + int maxRebondGraceEntries, + IDelegationRepository repository) + : this( + new DelegateeMetadata( + address, + accountAddress, + delegationCurrency, + rewardCurrencies, + delegationPoolAddress, + rewardPoolAddress, + rewardRemainderPoolAddress, + slashedPoolAddress, + unbondingPeriod, + maxUnbondLockInEntries, + maxRebondGraceEntries), + repository) + { + } + + public Delegatee( + Address address, + IDelegationRepository repository) + : this(repository.GetDelegateeMetadata(address), repository) + { + } + + private Delegatee(DelegateeMetadata metadata, IDelegationRepository repository) + { + Metadata = metadata; + Repository = repository; + } + + public event EventHandler? DelegationChanged; + + public event EventHandler? Enjailed; + + public event EventHandler? Unjailed; + + public DelegateeMetadata Metadata { get; } + + public IDelegationRepository Repository { get; } + + public Address Address => Metadata.DelegateeAddress; + + public Address AccountAddress => Metadata.DelegateeAccountAddress; + + public Address MetadataAddress => Metadata.Address; + + public Currency DelegationCurrency => Metadata.DelegationCurrency; + + public ImmutableSortedSet RewardCurrencies => Metadata.RewardCurrencies; + + public Address DelegationPoolAddress => Metadata.DelegationPoolAddress; + + public Address RewardPoolAddress => Metadata.RewardPoolAddress; + + public Address RewardRemainderPoolAddress => Metadata.RewardRemainderPoolAddress; + + public Address SlashedPoolAddress => Metadata.SlashedPoolAddress; + + public long UnbondingPeriod => Metadata.UnbondingPeriod; + + public int MaxUnbondLockInEntries => Metadata.MaxUnbondLockInEntries; + + public int MaxRebondGraceEntries => Metadata.MaxRebondGraceEntries; + + public ImmutableSortedSet
Delegators => Metadata.Delegators; + + public FungibleAssetValue TotalDelegated => Metadata.TotalDelegatedFAV; + + public BigInteger TotalShares => Metadata.TotalShares; + + public bool Jailed => Metadata.Jailed; + + public long JailedUntil => Metadata.JailedUntil; + + public bool Tombstoned => Metadata.Tombstoned; + + public List MetadataBencoded => Metadata.Bencoded; + + public BigInteger ShareFromFAV(FungibleAssetValue fav) + => Metadata.ShareFromFAV(fav); + + public FungibleAssetValue FAVFromShare(BigInteger share) + => Metadata.FAVFromShare(share); + + public BigInteger Bond(IDelegator delegator, FungibleAssetValue fav, long height) + => Bond((T)delegator, fav, height); + + public FungibleAssetValue Unbond(IDelegator delegator, BigInteger share, long height) + => Unbond((T)delegator, share, height); + + public void DistributeReward(IDelegator delegator, long height) + => DistributeReward((T)delegator, height); + + public void Jail(long releaseHeight) + { + Metadata.JailedUntil = releaseHeight; + Metadata.Jailed = true; + Repository.SetDelegateeMetadata(Metadata); + Enjailed?.Invoke(this, EventArgs.Empty); + } + + public void Unjail(long height) + { + if (!Jailed) + { + throw new InvalidOperationException("Cannot unjail non-jailed delegatee."); + } + + if (Tombstoned) + { + throw new InvalidOperationException("Cannot unjail tombstoned delegatee."); + } + + if (JailedUntil >= height) + { + throw new InvalidOperationException("Cannot unjail before jailed until."); + } + + Metadata.JailedUntil = -1L; + Metadata.Jailed = false; + Repository.SetDelegateeMetadata(Metadata); + Unjailed?.Invoke(this, EventArgs.Empty); + } + + public void Tombstone() + { + Jail(long.MaxValue); + Metadata.Tombstoned = true; + Repository.SetDelegateeMetadata(Metadata); + } + + public Address BondAddress(Address delegatorAddress) + => Metadata.BondAddress(delegatorAddress); + + public Address UnbondLockInAddress(Address delegatorAddress) + => Metadata.UnbondLockInAddress(delegatorAddress); + + public Address RebondGraceAddress(Address delegatorAddress) + => Metadata.RebondGraceAddress(delegatorAddress); + + public Address CurrentLumpSumRewardsRecordAddress() + => Metadata.CurrentLumpSumRewardsRecordAddress(); + + public Address LumpSumRewardsRecordAddress(long height) + => Metadata.LumpSumRewardsRecordAddress(height); + + public virtual BigInteger Bond(T delegator, FungibleAssetValue fav, long height) + { + DistributeReward(delegator, height); + + if (!fav.Currency.Equals(DelegationCurrency)) + { + throw new InvalidOperationException( + "Cannot bond with invalid currency."); + } + + if (Tombstoned) + { + throw new InvalidOperationException( + "Cannot bond to tombstoned delegatee."); + } + + Bond bond = Repository.GetBond(this, delegator.Address); + BigInteger share = ShareFromFAV(fav); + bond = bond.AddShare(share); + Metadata.AddDelegator(delegator.Address); + Metadata.AddShare(share); + Metadata.AddDelegatedFAV(fav); + Repository.SetBond(bond); + StartNewRewardPeriod(height); + Repository.SetDelegateeMetadata(Metadata); + DelegationChanged?.Invoke(this, height); + + return share; + } + + BigInteger IDelegatee.Bond(IDelegator delegator, FungibleAssetValue fav, long height) + => Bond((T)delegator, fav, height); + + public FungibleAssetValue Unbond(T delegator, BigInteger share, long height) + { + DistributeReward(delegator, height); + if (TotalShares.IsZero || TotalDelegated.RawValue.IsZero) + { + throw new InvalidOperationException( + "Cannot unbond without bonding."); + } + + Bond bond = Repository!.GetBond(this, delegator.Address); + FungibleAssetValue fav = FAVFromShare(share); + bond = bond.SubtractShare(share); + if (bond.Share.IsZero) + { + bond = bond.ClearLastDistributeHeight(); + Metadata.RemoveDelegator(delegator.Address); + } + + Metadata.RemoveShare(share); + Metadata.RemoveDelegatedFAV(fav); + Repository.SetBond(bond); + StartNewRewardPeriod(height); + Repository.SetDelegateeMetadata(Metadata); + DelegationChanged?.Invoke(this, height); + + return fav; + } + + FungibleAssetValue IDelegatee.Unbond(IDelegator delegator, BigInteger share, long height) + => Unbond((T)delegator, share, height); + + public void DistributeReward(T delegator, long height) + { + Bond bond = Repository.GetBond(this, delegator.Address); + BigInteger share = bond.Share; + + if (!share.IsZero && bond.LastDistributeHeight.HasValue) + { + IEnumerable lumpSumRewardsRecords + = GetLumpSumRewardsRecords(bond.LastDistributeHeight); + + foreach (LumpSumRewardsRecord record in lumpSumRewardsRecords) + { + if (!record.Delegators.Contains(delegator.Address)) + { + continue; + } + + TransferReward(delegator, share, record); + LumpSumRewardsRecord newRecord = record.RemoveDelegator(delegator.Address); + TransferRemainders(newRecord); + Repository.SetLumpSumRewardsRecord(newRecord); + } + } + + if (bond.LastDistributeHeight != height) + { + bond = bond.UpdateLastDistributeHeight(height); + } + + Repository.SetBond(bond); + } + + void IDelegatee.DistributeReward(IDelegator delegator, long height) + => DistributeReward((T)delegator, height); + + public void CollectRewards(long height) + { + var rewards = RewardCurrencies.Select(c => Repository.GetBalance(RewardPoolAddress, c)); + LumpSumRewardsRecord record = Repository.GetCurrentLumpSumRewardsRecord(this) + ?? new LumpSumRewardsRecord( + CurrentLumpSumRewardsRecordAddress(), + height, + TotalShares, + Delegators, + RewardCurrencies); + record = record.AddLumpSumRewards(rewards); + + foreach (var rewardsEach in rewards) + { + if (rewardsEach.Sign > 0) + { + Repository.TransferAsset(RewardPoolAddress, record.Address, rewardsEach); + } + } + + Repository.SetLumpSumRewardsRecord(record); + } + + public void Slash(BigInteger slashFactor, long infractionHeight, long height) + { + FungibleAssetValue slashed = TotalDelegated.DivRem(slashFactor, out var rem); + if (rem.Sign > 0) + { + slashed += FungibleAssetValue.FromRawValue(rem.Currency, 1); + } + + if (slashed > Metadata.TotalDelegatedFAV) + { + slashed = Metadata.TotalDelegatedFAV; + } + + Metadata.RemoveDelegatedFAV(slashed); + + foreach (var item in Metadata.UnbondingRefs) + { + var unbonding = UnbondingFactory.GetUnbondingFromRef(item, Repository); + + unbonding = unbonding.Slash(slashFactor, infractionHeight, height, out var slashedFAV); + + if (slashedFAV.HasValue) + { + slashed += slashedFAV.Value; + } + + if (unbonding.IsEmpty) + { + Metadata.RemoveUnbondingRef(item); + } + + switch (unbonding) + { + case UnbondLockIn unbondLockIn: + Repository.SetUnbondLockIn(unbondLockIn); + break; + case RebondGrace rebondGrace: + Repository.SetRebondGrace(rebondGrace); + break; + default: + throw new InvalidOperationException("Invalid unbonding type."); + } + } + + var delegationBalance = Repository.GetBalance(DelegationPoolAddress, DelegationCurrency); + if (delegationBalance < slashed) + { + slashed = delegationBalance; + } + + if (slashed > DelegationCurrency * 0) + { + Repository.TransferAsset(DelegationPoolAddress, SlashedPoolAddress, slashed); + } + + Repository.SetDelegateeMetadata(Metadata); + DelegationChanged?.Invoke(this, height); + } + + void IDelegatee.Slash(BigInteger slashFactor, long infractionHeight, long height) + => Slash(slashFactor, infractionHeight, height); + + public void AddUnbondingRef(UnbondingRef reference) + => Metadata.AddUnbondingRef(reference); + + public void RemoveUnbondingRef(UnbondingRef reference) + => Metadata.RemoveUnbondingRef(reference); + + public ImmutableDictionary CalculateReward( + BigInteger share, + IEnumerable lumpSumRewardsRecords) + { + ImmutableDictionary reward + = RewardCurrencies.ToImmutableDictionary(c => c, c => c * 0); + + foreach (LumpSumRewardsRecord record in lumpSumRewardsRecords) + { + var rewardDuringPeriod = record.RewardsDuringPeriod(share); + reward = rewardDuringPeriod.Aggregate(reward, (acc, pair) + => acc.SetItem(pair.Key, acc[pair.Key] + pair.Value)); + } + + return reward; + } + + private void StartNewRewardPeriod(long height) + { + LumpSumRewardsRecord? currentRecord = Repository.GetCurrentLumpSumRewardsRecord(this); + long? lastStartHeight = null; + if (currentRecord is LumpSumRewardsRecord lastRecord) + { + lastStartHeight = lastRecord.StartHeight; + if (lastStartHeight == height) + { + currentRecord = new( + currentRecord.Address, + currentRecord.StartHeight, + TotalShares, + Delegators, + RewardCurrencies, + currentRecord.LastStartHeight); + + Repository.SetLumpSumRewardsRecord(currentRecord); + return; + } + + Address archiveAddress = LumpSumRewardsRecordAddress(lastRecord.StartHeight); + + foreach (var rewardCurrency in RewardCurrencies) + { + FungibleAssetValue reward = Repository.GetBalance(lastRecord.Address, rewardCurrency); + if (reward.Sign > 0) + { + Repository.TransferAsset(lastRecord.Address, archiveAddress, reward); + } + } + + lastRecord = lastRecord.MoveAddress(archiveAddress); + Repository.SetLumpSumRewardsRecord(lastRecord); + } + + LumpSumRewardsRecord newRecord = new( + CurrentLumpSumRewardsRecordAddress(), + height, + TotalShares, + Delegators, + RewardCurrencies, + lastStartHeight); + + Repository.SetLumpSumRewardsRecord(newRecord); + } + + private List GetLumpSumRewardsRecords(long? lastRewardHeight) + { + List records = new(); + if (lastRewardHeight is null + || !(Repository.GetCurrentLumpSumRewardsRecord(this) is LumpSumRewardsRecord record)) + { + return records; + } + + while (record.StartHeight >= lastRewardHeight) + { + records.Add(record); + + if (!(record.LastStartHeight is long lastStartHeight)) + { + break; + } + + record = Repository.GetLumpSumRewardsRecord(this, lastStartHeight) + ?? throw new InvalidOperationException( + $"Lump sum rewards record for #{lastStartHeight} is missing"); + } + + return records; + } + + private void TransferReward(T delegator, BigInteger share, LumpSumRewardsRecord record) + { + ImmutableSortedDictionary reward = record.RewardsDuringPeriod(share); + foreach (var rewardEach in reward) + { + if (rewardEach.Value.Sign > 0) + { + Repository.TransferAsset(record.Address, delegator.RewardAddress, rewardEach.Value); + } + } + } + + private void TransferRemainders(LumpSumRewardsRecord record) + { + if (!record.Delegators.IsEmpty) + { + return; + } + + foreach (var rewardCurrency in RewardCurrencies) + { + FungibleAssetValue remainder = Repository.GetBalance(record.Address, rewardCurrency); + + if (remainder.Sign > 0) + { + Repository.TransferAsset(record.Address, RewardRemainderPoolAddress, remainder); + } + } + } + } +} diff --git a/Lib9c/Delegation/DelegateeMetadata.cs b/Lib9c/Delegation/DelegateeMetadata.cs new file mode 100644 index 0000000000..d61a9f0354 --- /dev/null +++ b/Lib9c/Delegation/DelegateeMetadata.cs @@ -0,0 +1,309 @@ +#nullable enable +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; + +namespace Nekoyume.Delegation +{ + public class DelegateeMetadata : IDelegateeMetadata + { + private Address? _address; + private readonly IComparer _currencyComparer = new CurrencyComparer(); + + public DelegateeMetadata( + Address delegateeAddress, + Address delegateeAccountAddress, + Currency delegationCurrency, + IEnumerable rewardCurrencies, + Address delegationPoolAddress, + Address rewardPoolAddress, + Address rewardRemainderPoolAddress, + Address slashedPoolAddress, + long unbondingPeriod, + int maxUnbondLockInEntries, + int maxRebondGraceEntries) + : this( + delegateeAddress, + delegateeAccountAddress, + delegationCurrency, + rewardCurrencies, + delegationPoolAddress, + rewardPoolAddress, + rewardRemainderPoolAddress, + slashedPoolAddress, + unbondingPeriod, + maxUnbondLockInEntries, + maxRebondGraceEntries, + ImmutableSortedSet
.Empty, + delegationCurrency * 0, + BigInteger.Zero, + false, + -1L, + false, + ImmutableSortedSet.Empty) + { + } + + public DelegateeMetadata( + Address delegateeAddress, + Address delegateeAccountAddress, + IValue bencoded) + : this(delegateeAddress, delegateeAccountAddress, (List)bencoded) + { + } + + public DelegateeMetadata( + Address address, + Address accountAddress, + List bencoded) + : this( + address, + accountAddress, + new Currency(bencoded[0]), + ((List)bencoded[1]).Select(v => new Currency(v)), + new Address(bencoded[2]), + new Address(bencoded[3]), + new Address(bencoded[4]), + new Address(bencoded[5]), + (Integer)bencoded[6], + (Integer)bencoded[7], + (Integer)bencoded[8], + ((List)bencoded[9]).Select(item => new Address(item)), + new FungibleAssetValue(bencoded[10]), + (Integer)bencoded[11], + (Bencodex.Types.Boolean)bencoded[12], + (Integer)bencoded[13], + (Bencodex.Types.Boolean)bencoded[14], + ((List)bencoded[15]).Select(item => new UnbondingRef(item))) + { + } + + private DelegateeMetadata( + Address delegateeAddress, + Address delegateeAccountAddress, + Currency delegationCurrency, + IEnumerable rewardCurrencies, + Address delegationPoolAddress, + Address rewardPoolAddress, + Address rewardRemainderPoolAddress, + Address slashedPoolAddress, + long unbondingPeriod, + int maxUnbondLockInEntries, + int maxRebondGraceEntries, + IEnumerable
delegators, + FungibleAssetValue totalDelegated, + BigInteger totalShares, + bool jailed, + long jailedUntil, + bool tombstoned, + IEnumerable unbondingRefs) + { + if (!totalDelegated.Currency.Equals(delegationCurrency)) + { + throw new InvalidOperationException("Invalid currency."); + } + + if (totalDelegated.Sign < 0) + { + throw new ArgumentOutOfRangeException( + nameof(totalDelegated), + totalDelegated, + "Total delegated must be non-negative."); + } + + if (totalShares.Sign < 0) + { + throw new ArgumentOutOfRangeException( + nameof(totalShares), + totalShares, + "Total shares must be non-negative."); + } + + DelegateeAddress = delegateeAddress; + DelegateeAccountAddress = delegateeAccountAddress; + DelegationCurrency = delegationCurrency; + RewardCurrencies = rewardCurrencies.ToImmutableSortedSet(_currencyComparer); + DelegationPoolAddress = delegationPoolAddress; + RewardPoolAddress = rewardPoolAddress; + RewardRemainderPoolAddress = rewardRemainderPoolAddress; + SlashedPoolAddress = slashedPoolAddress; + UnbondingPeriod = unbondingPeriod; + MaxUnbondLockInEntries = maxUnbondLockInEntries; + MaxRebondGraceEntries = maxRebondGraceEntries; + Delegators = delegators.ToImmutableSortedSet(); + TotalDelegatedFAV = totalDelegated; + TotalShares = totalShares; + Jailed = jailed; + JailedUntil = jailedUntil; + Tombstoned = tombstoned; + UnbondingRefs = unbondingRefs.ToImmutableSortedSet(); + } + + public Address DelegateeAddress { get; } + + public Address DelegateeAccountAddress { get; } + + public Address Address + => _address ??= DelegationAddress.DelegateeMetadataAddress( + DelegateeAddress, + DelegateeAccountAddress); + + public Currency DelegationCurrency { get; } + + public ImmutableSortedSet RewardCurrencies { get; } + + public Address DelegationPoolAddress { get; internal set; } + + public Address RewardPoolAddress { get; } + + public Address RewardRemainderPoolAddress { get; } + + public Address SlashedPoolAddress { get; } + + public long UnbondingPeriod { get; private set; } + + public int MaxUnbondLockInEntries { get; } + + public int MaxRebondGraceEntries { get; } + + public ImmutableSortedSet
Delegators { get; private set; } + + public FungibleAssetValue TotalDelegatedFAV { get; private set; } + + public BigInteger TotalShares { get; private set; } + + public bool Jailed { get; internal set; } + + public long JailedUntil { get; internal set; } + + public bool Tombstoned { get; internal set; } + + public ImmutableSortedSet UnbondingRefs { get; private set; } + + // TODO : Better serialization + public List Bencoded => List.Empty + .Add(DelegationCurrency.Serialize()) + .Add(new List(RewardCurrencies.Select(c => c.Serialize()))) + .Add(DelegationPoolAddress.Bencoded) + .Add(RewardPoolAddress.Bencoded) + .Add(RewardRemainderPoolAddress.Bencoded) + .Add(SlashedPoolAddress.Bencoded) + .Add(UnbondingPeriod) + .Add(MaxUnbondLockInEntries) + .Add(MaxRebondGraceEntries) + .Add(new List(Delegators.Select(delegator => delegator.Bencoded))) + .Add(TotalDelegatedFAV.Serialize()) + .Add(TotalShares) + .Add(Jailed) + .Add(JailedUntil) + .Add(Tombstoned) + .Add(new List(UnbondingRefs.Select(unbondingRef => unbondingRef.Bencoded))); + + IValue IBencodable.Bencoded => Bencoded; + + public BigInteger ShareFromFAV(FungibleAssetValue fav) + => TotalShares.IsZero + ? fav.RawValue + : TotalShares * fav.RawValue / TotalDelegatedFAV.RawValue; + + public FungibleAssetValue FAVFromShare(BigInteger share) + => TotalShares == share + ? TotalDelegatedFAV + : (TotalDelegatedFAV * share).DivRem(TotalShares).Quotient; + + public void AddDelegator(Address delegatorAddress) + { + Delegators = Delegators.Add(delegatorAddress); + } + + public void RemoveDelegator(Address delegatorAddress) + { + Delegators = Delegators.Remove(delegatorAddress); + } + + public void AddDelegatedFAV(FungibleAssetValue fav) + { + TotalDelegatedFAV += fav; + } + + public void RemoveDelegatedFAV(FungibleAssetValue fav) + { + TotalDelegatedFAV -= fav; + } + + public void AddShare(BigInteger share) + { + TotalShares += share; + } + + public void RemoveShare(BigInteger share) + { + TotalShares -= share; + } + + public void AddUnbondingRef(UnbondingRef unbondingRef) + { + UnbondingRefs = UnbondingRefs.Add(unbondingRef); + } + + public void RemoveUnbondingRef(UnbondingRef unbondingRef) + { + UnbondingRefs = UnbondingRefs.Remove(unbondingRef); + } + + public Address BondAddress(Address delegatorAddress) + => DelegationAddress.BondAddress(Address, delegatorAddress); + + public Address UnbondLockInAddress(Address delegatorAddress) + => DelegationAddress.UnbondLockInAddress(Address, delegatorAddress); + + public virtual Address RebondGraceAddress(Address delegatorAddress) + => DelegationAddress.RebondGraceAddress(Address, delegatorAddress); + + public virtual Address CurrentLumpSumRewardsRecordAddress() + => DelegationAddress.CurrentLumpSumRewardsRecordAddress(Address); + + public virtual Address LumpSumRewardsRecordAddress(long height) + => DelegationAddress.LumpSumRewardsRecordAddress(Address, height); + + public override bool Equals(object? obj) + => obj is IDelegateeMetadata other && Equals(other); + + public virtual bool Equals(IDelegateeMetadata? other) + => ReferenceEquals(this, other) + || (other is DelegateeMetadata delegatee + && (GetType() != delegatee.GetType()) + && DelegateeAddress.Equals(delegatee.DelegateeAddress) + && DelegateeAccountAddress.Equals(delegatee.DelegateeAccountAddress) + && DelegationCurrency.Equals(delegatee.DelegationCurrency) + && RewardCurrencies.SequenceEqual(delegatee.RewardCurrencies) + && DelegationPoolAddress.Equals(delegatee.DelegationPoolAddress) + && RewardPoolAddress.Equals(delegatee.RewardPoolAddress) + && RewardRemainderPoolAddress.Equals(delegatee.RewardRemainderPoolAddress) + && SlashedPoolAddress.Equals(delegatee.SlashedPoolAddress) + && UnbondingPeriod == delegatee.UnbondingPeriod + && RewardPoolAddress.Equals(delegatee.RewardPoolAddress) + && Delegators.SequenceEqual(delegatee.Delegators) + && TotalDelegatedFAV.Equals(delegatee.TotalDelegatedFAV) + && TotalShares.Equals(delegatee.TotalShares) + && Jailed == delegatee.Jailed + && UnbondingRefs.SequenceEqual(delegatee.UnbondingRefs)); + + public override int GetHashCode() + => DelegateeAddress.GetHashCode(); + + // TODO: [GuildMigration] Remove this method when the migration is done. + // Remove private setter for UnbondingPeriod. + public void UpdateUnbondingPeriod(long unbondingPeriod) + { + UnbondingPeriod = unbondingPeriod; + } + + } +} diff --git a/Lib9c/Delegation/DelegationAddress.cs b/Lib9c/Delegation/DelegationAddress.cs new file mode 100644 index 0000000000..243b21e382 --- /dev/null +++ b/Lib9c/Delegation/DelegationAddress.cs @@ -0,0 +1,146 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using Libplanet.Crypto; + +namespace Nekoyume.Delegation +{ + public static class DelegationAddress + { + public static Address DelegateeMetadataAddress( + Address delegateeAddress, Address delegateeAccountAddress) + => DeriveAddress( + DelegationElementType.DelegateeMetadata, + delegateeAddress, + delegateeAccountAddress.ByteArray); + + public static Address DelegatorMetadataAddress( + Address delegatorAddress, Address delegatorAccountAddress) + => DeriveAddress( + DelegationElementType.DelegatorMetadata, + delegatorAddress, + delegatorAccountAddress.ByteArray); + + public static Address BondAddress( + Address delegateeAddress, Address delegateeAccountAddress, Address delegatorAddress) + => DeriveAddress( + DelegationElementType.Bond, + DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress), + delegatorAddress.ByteArray); + + public static Address BondAddress( + Address delegateeMetadataAddress, Address delegatorAddress) + => DeriveAddress( + DelegationElementType.Bond, + delegateeMetadataAddress, + delegatorAddress.ByteArray); + + public static Address UnbondLockInAddress( + Address delegateeAddress, Address delegateeAccountAddress, Address delegatorAddress) + => DeriveAddress( + DelegationElementType.UnbondLockIn, + DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress), + delegatorAddress.ByteArray); + + public static Address UnbondLockInAddress( + Address delegateeMetadataAddress, Address delegatorAddress) + => DeriveAddress( + DelegationElementType.UnbondLockIn, + delegateeMetadataAddress, + delegatorAddress.ByteArray); + + public static Address RebondGraceAddress( + Address delegateeAddress, Address delegateeAccountAddress, Address delegatorAddress) + => DeriveAddress( + DelegationElementType.RebondGrace, + DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress), + delegatorAddress.ByteArray); + + public static Address RebondGraceAddress( + Address delegateeMetadataAddress, Address delegatorAddress) + => DeriveAddress( + DelegationElementType.RebondGrace, + delegateeMetadataAddress, + delegatorAddress.ByteArray); + + public static Address CurrentLumpSumRewardsRecordAddress( + Address delegateeAddress, Address delegateeAccountAddress) + => DeriveAddress( + DelegationElementType.LumpSumRewardsRecord, + DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress)); + + public static Address CurrentLumpSumRewardsRecordAddress( + Address delegateeMetadataAddress) + => DeriveAddress( + DelegationElementType.LumpSumRewardsRecord, + delegateeMetadataAddress); + + public static Address LumpSumRewardsRecordAddress( + Address delegateeAddress, Address delegateeAccountAddress, long height) + => DeriveAddress( + DelegationElementType.LumpSumRewardsRecord, + DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress), + BitConverter.GetBytes(height)); + + public static Address LumpSumRewardsRecordAddress( + Address delegateeMetadataAddress, long height) + => DeriveAddress( + DelegationElementType.LumpSumRewardsRecord, + delegateeMetadataAddress, + BitConverter.GetBytes(height)); + + public static Address RewardPoolAddress( + Address delegateeAddress, Address delegateeAccountAddress) + => DeriveAddress( + DelegationElementType.RewardPool, + DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress)); + + public static Address RewardPoolAddress( + Address delegateeMetadataAddress) + => DeriveAddress( + DelegationElementType.RewardPool, + delegateeMetadataAddress); + + public static Address DelegationPoolAddress( + Address delegateeAddress, Address delegateeAccountAddress) + => DeriveAddress( + DelegationElementType.DelegationPool, + DelegateeMetadataAddress(delegateeAddress, delegateeAccountAddress)); + + public static Address DelegationPoolAddress( + Address delegateeMetadataAddress) + => DeriveAddress( + DelegationElementType.DelegationPool, + delegateeMetadataAddress); + + private static Address DeriveAddress( + DelegationElementType identifier, + Address address, + IEnumerable? bytes = null) + { + byte[] hashed; + using (HMACSHA1 hmac = new( + BitConverter.GetBytes((int)identifier).ToArray())) + { + hashed = hmac.ComputeHash( + address.ByteArray.Concat(bytes ?? Array.Empty()).ToArray()); + } + + return new Address(hashed); + } + + private enum DelegationElementType + { + DelegateeMetadata, + DelegatorMetadata, + Bond, + UnbondLockIn, + RebondGrace, + LumpSumRewardsRecord, + RewardPool, + DelegationPool, + } + } +} diff --git a/Lib9c/Delegation/DelegationRepository.cs b/Lib9c/Delegation/DelegationRepository.cs new file mode 100644 index 0000000000..c4002c2372 --- /dev/null +++ b/Lib9c/Delegation/DelegationRepository.cs @@ -0,0 +1,251 @@ +#nullable enable +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action; + +namespace Nekoyume.Delegation +{ + public abstract class DelegationRepository : IDelegationRepository + { + protected IWorld previousWorld; + protected IAccount delegateeAccount; + protected IAccount delegatorAccount; + protected IAccount delegateeMetadataAccount; + protected IAccount delegatorMetadataAccount; + protected IAccount bondAccount; + protected IAccount unbondLockInAccount; + protected IAccount rebondGraceAccount; + protected IAccount unbondingSetAccount; + protected IAccount lumpSumRewardsRecordAccount; + + public DelegationRepository( + IWorld world, + IActionContext actionContext, + Address delegateeAccountAddress, + Address delegatorAccountAddress, + Address delegateeMetadataAccountAddress, + Address delegatorMetadataAccountAddress, + Address bondAccountAddress, + Address unbondLockInAccountAddress, + Address rebondGraceAccountAddress, + Address unbondingSetAccountAddress, + Address lumpSumRewardRecordAccountAddress) + { + previousWorld = world; + ActionContext = actionContext; + DelegateeAccountAddress = delegateeAccountAddress; + DelegatorAccountAddress = delegatorAccountAddress; + DelegateeMetadataAccountAddress = delegateeMetadataAccountAddress; + DelegatorMetadataAccountAddress = delegatorMetadataAccountAddress; + BondAccountAddress = bondAccountAddress; + UnbondLockInAccountAddress = unbondLockInAccountAddress; + RebondGraceAccountAddress = rebondGraceAccountAddress; + UnbondingSetAccountAddress = unbondingSetAccountAddress; + LumpSumRewardsRecordAccountAddress = lumpSumRewardRecordAccountAddress; + + delegateeAccount = world.GetAccount(DelegateeAccountAddress); + delegatorAccount = world.GetAccount(DelegatorAccountAddress); + delegateeMetadataAccount = world.GetAccount(DelegateeMetadataAccountAddress); + delegatorMetadataAccount = world.GetAccount(DelegatorMetadataAccountAddress); + bondAccount = world.GetAccount(BondAccountAddress); + unbondLockInAccount = world.GetAccount(UnbondLockInAccountAddress); + rebondGraceAccount = world.GetAccount(RebondGraceAccountAddress); + unbondingSetAccount = world.GetAccount(UnbondingSetAccountAddress); + lumpSumRewardsRecordAccount = world.GetAccount(LumpSumRewardsRecordAccountAddress); + } + + public virtual IWorld World => previousWorld + .SetAccount(DelegateeAccountAddress, delegateeAccount) + .SetAccount(DelegatorAccountAddress, delegatorAccount) + .SetAccount(DelegateeMetadataAccountAddress, delegateeMetadataAccount) + .SetAccount(DelegatorMetadataAccountAddress, delegatorMetadataAccount) + .SetAccount(BondAccountAddress, bondAccount) + .SetAccount(UnbondLockInAccountAddress, unbondLockInAccount) + .SetAccount(RebondGraceAccountAddress, rebondGraceAccount) + .SetAccount(UnbondingSetAccountAddress, unbondingSetAccount) + .SetAccount(LumpSumRewardsRecordAccountAddress, lumpSumRewardsRecordAccount); + + public IActionContext ActionContext { get; } + + public Address DelegateeAccountAddress { get; } + + public Address DelegatorAccountAddress { get; } + + private Address DelegateeMetadataAccountAddress { get; } + + private Address DelegatorMetadataAccountAddress { get; } + + private Address BondAccountAddress { get; } + + private Address UnbondLockInAccountAddress { get; } + + private Address RebondGraceAccountAddress { get; } + + private Address UnbondingSetAccountAddress { get; } + + private Address LumpSumRewardsRecordAccountAddress { get; } + + public abstract IDelegatee GetDelegatee(Address address); + + public abstract IDelegator GetDelegator(Address address); + + public abstract void SetDelegatee(IDelegatee delegatee); + + public abstract void SetDelegator(IDelegator delegator); + + public DelegateeMetadata GetDelegateeMetadata(Address delegateeAddress) + { + IValue? value = delegateeMetadataAccount.GetState( + DelegationAddress.DelegateeMetadataAddress(delegateeAddress, DelegateeAccountAddress)); + return value is IValue bencoded + ? new DelegateeMetadata(delegateeAddress, DelegateeAccountAddress, bencoded) + : throw new FailedLoadStateException("DelegateeMetadata not found."); + } + + public DelegatorMetadata GetDelegatorMetadata(Address delegatorAddress) + { + IValue? value = delegatorMetadataAccount.GetState( + DelegationAddress.DelegatorMetadataAddress(delegatorAddress, DelegatorAccountAddress)); + return value is IValue bencoded + ? new DelegatorMetadata(delegatorAddress, DelegatorAccountAddress, bencoded) + : throw new FailedLoadStateException("DelegatorMetadata not found."); + } + + public Bond GetBond(IDelegatee delegatee, Address delegatorAddress) + { + Address address = delegatee.BondAddress(delegatorAddress); + IValue? value = bondAccount.GetState(address); + return value is IValue bencoded + ? new Bond(address, bencoded) + : new Bond(address); + } + + public UnbondLockIn GetUnbondLockIn(IDelegatee delegatee, Address delegatorAddress) + { + Address address = delegatee.UnbondLockInAddress(delegatorAddress); + IValue? value = unbondLockInAccount.GetState(address); + return value is IValue bencoded + ? new UnbondLockIn(address, delegatee.MaxUnbondLockInEntries, bencoded, this) + : new UnbondLockIn(address, delegatee.MaxUnbondLockInEntries, delegatee.Address, delegatorAddress, this); + } + + public UnbondLockIn GetUnlimitedUnbondLockIn(Address address) + { + IValue? value = unbondLockInAccount.GetState(address); + return value is IValue bencoded + ? new UnbondLockIn(address, int.MaxValue, bencoded, this) + : throw new FailedLoadStateException("UnbondLockIn not found."); + } + + public RebondGrace GetRebondGrace(IDelegatee delegatee, Address delegatorAddress) + { + Address address = delegatee.RebondGraceAddress(delegatorAddress); + IValue? value = rebondGraceAccount.GetState(address); + return value is IValue bencoded + ? new RebondGrace(address, delegatee.MaxRebondGraceEntries, bencoded, this) + : new RebondGrace(address, delegatee.MaxRebondGraceEntries, this); + } + + public RebondGrace GetUnlimitedRebondGrace(Address address) + { + IValue? value = rebondGraceAccount.GetState(address); + return value is IValue bencoded + ? new RebondGrace(address, int.MaxValue, bencoded, this) + : throw new FailedLoadStateException("RebondGrace not found."); + } + + public UnbondingSet GetUnbondingSet() + => unbondingSetAccount.GetState(UnbondingSet.Address) is IValue bencoded + ? new UnbondingSet(bencoded, this) + : new UnbondingSet(this); + + public LumpSumRewardsRecord? GetLumpSumRewardsRecord(IDelegatee delegatee, long height) + { + Address address = delegatee.LumpSumRewardsRecordAddress(height); + IValue? value = lumpSumRewardsRecordAccount.GetState(address); + return value is IValue bencoded + ? new LumpSumRewardsRecord(address, bencoded) + : null; + } + + public LumpSumRewardsRecord? GetCurrentLumpSumRewardsRecord(IDelegatee delegatee) + { + Address address = delegatee.CurrentLumpSumRewardsRecordAddress(); + IValue? value = lumpSumRewardsRecordAccount.GetState(address); + return value is IValue bencoded + ? new LumpSumRewardsRecord(address, bencoded) + : null; + } + + public FungibleAssetValue GetBalance(Address address, Currency currency) + => previousWorld.GetBalance(address, currency); + + public void SetDelegateeMetadata(DelegateeMetadata delegateeMetadata) + { + delegateeMetadataAccount + = delegateeMetadataAccount.SetState( + delegateeMetadata.Address, delegateeMetadata.Bencoded); + } + + public void SetDelegatorMetadata(DelegatorMetadata delegatorMetadata) + { + delegatorMetadataAccount + = delegatorMetadataAccount.SetState( + delegatorMetadata.Address, delegatorMetadata.Bencoded); + } + + public void SetBond(Bond bond) + { + bondAccount = bond.IsEmpty + ? bondAccount.RemoveState(bond.Address) + : bondAccount.SetState(bond.Address, bond.Bencoded); + } + + public void SetUnbondLockIn(UnbondLockIn unbondLockIn) + { + unbondLockInAccount = unbondLockIn.IsEmpty + ? unbondLockInAccount.RemoveState(unbondLockIn.Address) + : unbondLockInAccount.SetState(unbondLockIn.Address, unbondLockIn.Bencoded); + } + + public void SetRebondGrace(RebondGrace rebondGrace) + { + rebondGraceAccount = rebondGrace.IsEmpty + ? rebondGraceAccount.RemoveState(rebondGrace.Address) + : rebondGraceAccount.SetState(rebondGrace.Address, rebondGrace.Bencoded); + } + + public void SetUnbondingSet(UnbondingSet unbondingSet) + { + unbondingSetAccount = unbondingSet.IsEmpty + ? unbondingSetAccount.RemoveState(UnbondingSet.Address) + : unbondingSetAccount.SetState(UnbondingSet.Address, unbondingSet.Bencoded); + } + + public void SetLumpSumRewardsRecord(LumpSumRewardsRecord lumpSumRewardsRecord) + { + lumpSumRewardsRecordAccount = lumpSumRewardsRecordAccount.SetState( + lumpSumRewardsRecord.Address, lumpSumRewardsRecord.Bencoded); + } + + public void TransferAsset(Address sender, Address recipient, FungibleAssetValue value) + => previousWorld = previousWorld.TransferAsset(ActionContext, sender, recipient, value); + + public virtual void UpdateWorld(IWorld world) + { + previousWorld = world; + delegateeAccount = world.GetAccount(DelegateeAccountAddress); + delegatorAccount = world.GetAccount(DelegatorAccountAddress); + delegateeMetadataAccount = world.GetAccount(DelegateeMetadataAccountAddress); + delegatorMetadataAccount = world.GetAccount(DelegatorMetadataAccountAddress); + bondAccount = world.GetAccount(BondAccountAddress); + unbondLockInAccount = world.GetAccount(UnbondLockInAccountAddress); + rebondGraceAccount = world.GetAccount(RebondGraceAccountAddress); + unbondingSetAccount = world.GetAccount(UnbondingSetAccountAddress); + lumpSumRewardsRecordAccount = world.GetAccount(LumpSumRewardsRecordAccountAddress); + } + } +} diff --git a/Lib9c/Delegation/Delegator.cs b/Lib9c/Delegation/Delegator.cs new file mode 100644 index 0000000000..92c036f850 --- /dev/null +++ b/Lib9c/Delegation/Delegator.cs @@ -0,0 +1,230 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + public abstract class Delegator : IDelegator + where T : Delegatee + where TSelf : Delegator + { + public Delegator( + Address address, + Address accountAddress, + Address delegationPoolAddress, + Address rewardAddress, + IDelegationRepository repository) + : this( + new DelegatorMetadata( + address, + accountAddress, + delegationPoolAddress, + rewardAddress), + repository) + { + } + + public Delegator( + Address address, + IDelegationRepository repository) + : this(repository.GetDelegatorMetadata(address), repository) + { + } + + private Delegator(DelegatorMetadata metadata, IDelegationRepository repository) + { + Metadata = metadata; + Repository = repository; + } + + public DelegatorMetadata Metadata { get; } + + public IDelegationRepository Repository { get; } + + public Address Address => Metadata.DelegatorAddress; + + public Address AccountAddress => Metadata.DelegatorAccountAddress; + + public Address MetadataAddress => Metadata.Address; + + public Address DelegationPoolAddress => Metadata.DelegationPoolAddress; + + public Address RewardAddress => Metadata.RewardAddress; + + public ImmutableSortedSet
Delegatees => Metadata.Delegatees; + + public List MetadataBencoded => Metadata.Bencoded; + + public virtual void Delegate( + T delegatee, FungibleAssetValue fav, long height) + { + if (fav.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(fav), fav, "Fungible asset value must be positive."); + } + + if (delegatee.Tombstoned) + { + throw new InvalidOperationException("Delegatee is tombstoned."); + } + + delegatee.Bond(this, fav, height); + Metadata.AddDelegatee(delegatee.Address); + Repository.TransferAsset(DelegationPoolAddress, delegatee.DelegationPoolAddress, fav); + Repository.SetDelegator(this); + } + + void IDelegator.Delegate( + IDelegatee delegatee, FungibleAssetValue fav, long height) + => Delegate((T)delegatee, fav, height); + + public virtual void Undelegate( + T delegatee, BigInteger share, long height) + { + if (share.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(share), share, "Share must be positive."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), height, "Height must be positive."); + } + + UnbondLockIn unbondLockIn = Repository.GetUnbondLockIn(delegatee, Address); + + if (unbondLockIn.IsFull) + { + throw new InvalidOperationException("Undelegation is full."); + } + + FungibleAssetValue fav = delegatee.Unbond(this, share, height); + unbondLockIn = unbondLockIn.LockIn( + fav, height, height + delegatee.UnbondingPeriod); + + if (!delegatee.Delegators.Contains(Address)) + { + Metadata.RemoveDelegatee(delegatee.Address); + } + + delegatee.AddUnbondingRef(UnbondingFactory.ToReference(unbondLockIn)); + + Repository.SetUnbondLockIn(unbondLockIn); + Repository.SetUnbondingSet( + Repository.GetUnbondingSet().SetUnbonding(unbondLockIn)); + Repository.SetDelegator(this); + } + + void IDelegator.Undelegate( + IDelegatee delegatee, BigInteger share, long height) + => Undelegate((T)delegatee, share, height); + + + public virtual void Redelegate( + T srcDelegatee, T dstDelegatee, BigInteger share, long height) + { + if (share.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(share), share, "Share must be positive."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), height, "Height must be positive."); + } + + if (dstDelegatee.Tombstoned) + { + throw new InvalidOperationException("Destination delegatee is tombstoned."); + } + + FungibleAssetValue fav = srcDelegatee.Unbond( + this, share, height); + dstDelegatee.Bond( + this, fav, height); + RebondGrace srcRebondGrace = Repository.GetRebondGrace(srcDelegatee, Address).Grace( + dstDelegatee.Address, + fav, + height, + height + srcDelegatee.UnbondingPeriod); + + if (!srcDelegatee.Delegators.Contains(Address)) + { + Metadata.RemoveDelegatee(srcDelegatee.Address); + } + + Metadata.AddDelegatee(dstDelegatee.Address); + + srcDelegatee.AddUnbondingRef(UnbondingFactory.ToReference(srcRebondGrace)); + + Repository.SetRebondGrace(srcRebondGrace); + Repository.SetUnbondingSet( + Repository.GetUnbondingSet().SetUnbonding(srcRebondGrace)); + Repository.SetDelegator(this); + } + + void IDelegator.Redelegate( + IDelegatee srcDelegatee, IDelegatee dstDelegatee, BigInteger share, long height) + => Redelegate((T)srcDelegatee, (T)dstDelegatee, share, height); + + public void CancelUndelegate( + T delegatee, FungibleAssetValue fav, long height) + { + if (fav.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(fav), fav, "Fungible asset value must be positive."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), height, "Height must be positive."); + } + + UnbondLockIn unbondLockIn = Repository.GetUnbondLockIn(delegatee, Address); + + if (unbondLockIn.IsFull) + { + throw new InvalidOperationException("Undelegation is full."); + } + + delegatee.Bond(this, fav, height); + unbondLockIn = unbondLockIn.Cancel(fav, height); + Metadata.AddDelegatee(delegatee.Address); + + if (unbondLockIn.IsEmpty) + { + delegatee.RemoveUnbondingRef(UnbondingFactory.ToReference(unbondLockIn)); + } + + Repository.SetUnbondLockIn(unbondLockIn); + Repository.SetUnbondingSet( + Repository.GetUnbondingSet().SetUnbonding(unbondLockIn)); + Repository.SetDelegator(this); + } + + void IDelegator.CancelUndelegate( + IDelegatee delegatee, FungibleAssetValue fav, long height) + => CancelUndelegate((T)delegatee, fav, height); + + public void ClaimReward( + T delegatee, long height) + { + delegatee.DistributeReward(this, height); + Repository.SetDelegator(this); + } + + void IDelegator.ClaimReward(IDelegatee delegatee, long height) + => ClaimReward((T)delegatee, height); + } +} diff --git a/Lib9c/Delegation/DelegatorMetadata.cs b/Lib9c/Delegation/DelegatorMetadata.cs new file mode 100644 index 0000000000..9fe2be2088 --- /dev/null +++ b/Lib9c/Delegation/DelegatorMetadata.cs @@ -0,0 +1,113 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Linq; +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; + +namespace Nekoyume.Delegation +{ + public class DelegatorMetadata : IDelegatorMetadata + { + private Address? _address; + + public DelegatorMetadata( + Address address, + Address accountAddress, + Address delegationPoolAddress, + Address rewardAddress) + : this( + address, + accountAddress, + delegationPoolAddress, + rewardAddress, + ImmutableSortedSet
.Empty) + { + } + + public DelegatorMetadata( + Address address, + Address accountAddress, + IValue bencoded) + : this(address, accountAddress, (List)bencoded) + { + } + + public DelegatorMetadata( + Address address, + Address accountAddress, + List bencoded) + : this( + address, + accountAddress, + new Address(bencoded[0]), + new Address(bencoded[1]), + ((List)bencoded[2]).Select(item => new Address(item)).ToImmutableSortedSet()) + { + } + + private DelegatorMetadata( + Address address, + Address accountAddress, + Address delegationPoolAddress, + Address rewardAddress, + ImmutableSortedSet
delegatees) + { + DelegatorAddress = address; + DelegatorAccountAddress = accountAddress; + DelegationPoolAddress = delegationPoolAddress; + RewardAddress = rewardAddress; + Delegatees = delegatees; + } + + public Address DelegatorAddress { get; } + + public Address DelegatorAccountAddress { get; } + + public Address Address + => _address ??= DelegationAddress.DelegatorMetadataAddress( + DelegatorAddress, + DelegatorAccountAddress); + + public Address DelegationPoolAddress { get; } + + public Address RewardAddress { get; } + + public ImmutableSortedSet
Delegatees { get; private set; } + + public List Bencoded + => List.Empty + .Add(DelegationPoolAddress.Bencoded) + .Add(RewardAddress.Bencoded) + .Add(new List(Delegatees.Select(a => a.Bencoded))); + + IValue IBencodable.Bencoded => Bencoded; + + public void AddDelegatee(Address delegatee) + { + Delegatees = Delegatees.Add(delegatee); + } + + public void RemoveDelegatee(Address delegatee) + { + Delegatees = Delegatees.Remove(delegatee); + } + + public override bool Equals(object? obj) + => obj is IDelegator other && Equals(other); + + public virtual bool Equals(IDelegator? other) + => ReferenceEquals(this, other) + || (other is DelegatorMetadata delegator + && GetType() != delegator.GetType() + && DelegatorAddress.Equals(delegator.DelegatorAddress) + && DelegatorAccountAddress.Equals(delegator.DelegatorAccountAddress) + && DelegationPoolAddress.Equals(delegator.DelegationPoolAddress) + && RewardAddress.Equals(delegator.RewardAddress) + && Delegatees.SequenceEqual(delegator.Delegatees)); + + public override int GetHashCode() + => DelegatorAddress.GetHashCode(); + } +} diff --git a/Lib9c/Delegation/IDelegatee.cs b/Lib9c/Delegation/IDelegatee.cs new file mode 100644 index 0000000000..458214a81b --- /dev/null +++ b/Lib9c/Delegation/IDelegatee.cs @@ -0,0 +1,76 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Numerics; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + public interface IDelegatee + { + Address Address { get; } + + Address AccountAddress { get; } + + Currency DelegationCurrency { get; } + + ImmutableSortedSet RewardCurrencies { get; } + + Address DelegationPoolAddress { get; } + + Address RewardRemainderPoolAddress { get; } + + long UnbondingPeriod { get; } + + int MaxUnbondLockInEntries { get; } + + int MaxRebondGraceEntries { get; } + + Address RewardPoolAddress { get; } + + ImmutableSortedSet
Delegators { get; } + + FungibleAssetValue TotalDelegated { get; } + + BigInteger TotalShares { get; } + + bool Jailed { get; } + + long JailedUntil { get; } + + bool Tombstoned { get; } + + BigInteger ShareFromFAV(FungibleAssetValue fav); + + FungibleAssetValue FAVFromShare(BigInteger share); + + BigInteger Bond(IDelegator delegator, FungibleAssetValue fav, long height); + + FungibleAssetValue Unbond(IDelegator delegator, BigInteger share, long height); + + void DistributeReward(IDelegator delegator, long height); + + void CollectRewards(long height); + + void Slash(BigInteger slashFactor, long infractionHeight, long height); + + void Jail(long releaseHeight); + + void Unjail(long height); + + void Tombstone(); + + Address BondAddress(Address delegatorAddress); + + Address UnbondLockInAddress(Address delegatorAddress); + + Address RebondGraceAddress(Address delegatorAddress); + + Address CurrentLumpSumRewardsRecordAddress(); + + Address LumpSumRewardsRecordAddress(long height); + + event EventHandler? DelegationChanged; + } +} diff --git a/Lib9c/Delegation/IDelegateeMetadata.cs b/Lib9c/Delegation/IDelegateeMetadata.cs new file mode 100644 index 0000000000..8a135ef541 --- /dev/null +++ b/Lib9c/Delegation/IDelegateeMetadata.cs @@ -0,0 +1,52 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Numerics; +using Bencodex; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + public interface IDelegateeMetadata : IBencodable + { + Address DelegateeAddress { get; } + + Address DelegateeAccountAddress { get; } + + Address Address { get; } + + Currency DelegationCurrency { get; } + + ImmutableSortedSet RewardCurrencies { get; } + + Address DelegationPoolAddress { get; } + + Address RewardRemainderPoolAddress { get; } + + long UnbondingPeriod { get; } + + int MaxUnbondLockInEntries { get; } + + int MaxRebondGraceEntries { get; } + + Address RewardPoolAddress { get; } + + ImmutableSortedSet
Delegators { get; } + + FungibleAssetValue TotalDelegatedFAV { get; } + + BigInteger TotalShares { get; } + + bool Jailed { get; } + + long JailedUntil { get; } + + bool Tombstoned { get; } + + BigInteger ShareFromFAV(FungibleAssetValue fav); + + FungibleAssetValue FAVFromShare(BigInteger share); + } +} diff --git a/Lib9c/Delegation/IDelegationRepository.cs b/Lib9c/Delegation/IDelegationRepository.cs new file mode 100644 index 0000000000..47c988a0b5 --- /dev/null +++ b/Lib9c/Delegation/IDelegationRepository.cs @@ -0,0 +1,67 @@ +#nullable enable +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + public interface IDelegationRepository + { + Address DelegateeAccountAddress { get; } + + Address DelegatorAccountAddress { get; } + + IWorld World { get; } + + IActionContext ActionContext { get; } + + IDelegatee GetDelegatee(Address address); + + IDelegator GetDelegator(Address address); + + DelegateeMetadata GetDelegateeMetadata(Address delegateeAddress); + + DelegatorMetadata GetDelegatorMetadata(Address delegatorAddress); + + Bond GetBond(IDelegatee delegatee, Address delegatorAddress); + + UnbondLockIn GetUnbondLockIn(IDelegatee delegatee, Address delegatorAddress); + + UnbondLockIn GetUnlimitedUnbondLockIn(Address address); + + RebondGrace GetRebondGrace(IDelegatee delegatee, Address delegatorAddress); + + RebondGrace GetUnlimitedRebondGrace(Address address); + + UnbondingSet GetUnbondingSet(); + + LumpSumRewardsRecord? GetLumpSumRewardsRecord(IDelegatee delegatee, long height); + + LumpSumRewardsRecord? GetCurrentLumpSumRewardsRecord(IDelegatee delegatee); + + FungibleAssetValue GetBalance(Address address, Currency currency); + + void SetDelegatee(IDelegatee delegatee); + + void SetDelegator(IDelegator delegator); + + void SetDelegateeMetadata(DelegateeMetadata delegateeMetadata); + + void SetDelegatorMetadata(DelegatorMetadata delegatorMetadata); + + void SetBond(Bond bond); + + void SetUnbondLockIn(UnbondLockIn unbondLockIn); + + void SetRebondGrace(RebondGrace rebondGrace); + + void SetUnbondingSet(UnbondingSet unbondingSet); + + void SetLumpSumRewardsRecord(LumpSumRewardsRecord lumpSumRewardsRecord); + + void TransferAsset(Address sender, Address recipient, FungibleAssetValue value); + + void UpdateWorld(IWorld world); + } +} diff --git a/Lib9c/Delegation/IDelegator.cs b/Lib9c/Delegation/IDelegator.cs new file mode 100644 index 0000000000..ee8e375982 --- /dev/null +++ b/Lib9c/Delegation/IDelegator.cs @@ -0,0 +1,47 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Numerics; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + public interface IDelegator + { + Address Address { get; } + + Address AccountAddress { get; } + + Address DelegationPoolAddress { get; } + + Address RewardAddress { get; } + + ImmutableSortedSet
Delegatees { get; } + + void Delegate( + IDelegatee delegatee, + FungibleAssetValue fav, + long height); + + void Undelegate( + IDelegatee delegatee, + BigInteger share, + long height); + + void Redelegate( + IDelegatee srcDelegatee, + IDelegatee dstDelegatee, + BigInteger share, + long height); + + void CancelUndelegate( + IDelegatee delegatee, + FungibleAssetValue fav, + long height); + + void ClaimReward( + IDelegatee delegatee, + long height); + } +} diff --git a/Lib9c/Delegation/IDelegatorMetadata.cs b/Lib9c/Delegation/IDelegatorMetadata.cs new file mode 100644 index 0000000000..c2a15deba7 --- /dev/null +++ b/Lib9c/Delegation/IDelegatorMetadata.cs @@ -0,0 +1,23 @@ +using System.Collections.Immutable; +using Bencodex; +using Libplanet.Crypto; + +namespace Nekoyume.Delegation +{ + public interface IDelegatorMetadata : IBencodable + { + Address DelegatorAddress { get; } + + Address DelegatorAccountAddress { get; } + + Address Address { get; } + + Address DelegationPoolAddress { get; } + + ImmutableSortedSet
Delegatees { get; } + + public void AddDelegatee(Address delegatee); + + public void RemoveDelegatee(Address delegatee); + } +} diff --git a/Lib9c/Delegation/IUnbonding.cs b/Lib9c/Delegation/IUnbonding.cs new file mode 100644 index 0000000000..4248733506 --- /dev/null +++ b/Lib9c/Delegation/IUnbonding.cs @@ -0,0 +1,25 @@ +using System.Numerics; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + public interface IUnbonding + { + Address Address { get; } + + long LowestExpireHeight { get; } + + bool IsFull { get; } + + bool IsEmpty { get; } + + IUnbonding Release(long height); + + IUnbonding Slash( + BigInteger slashFactor, + long infractionHeight, + long height, + out FungibleAssetValue? slashedFAV); + } +} diff --git a/Lib9c/Delegation/LumpSumRewardsRecord.cs b/Lib9c/Delegation/LumpSumRewardsRecord.cs new file mode 100644 index 0000000000..e2e79b3e4b --- /dev/null +++ b/Lib9c/Delegation/LumpSumRewardsRecord.cs @@ -0,0 +1,198 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using Bencodex.Types; +using Bencodex; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + public class LumpSumRewardsRecord : IBencodable, IEquatable + { + private readonly IComparer _currencyComparer = new CurrencyComparer(); + + public LumpSumRewardsRecord( + Address address, + long startHeight, + BigInteger totalShares, + ImmutableSortedSet
delegators, + IEnumerable currencies) + : this( + address, + startHeight, + totalShares, + delegators, + currencies, + null) + { + } + + public LumpSumRewardsRecord( + Address address, + long startHeight, + BigInteger totalShares, + ImmutableSortedSet
delegators, + IEnumerable currencies, + long? lastStartHeight) + : this( + address, + startHeight, + totalShares, + delegators, + currencies.Select(c => c * 0), + lastStartHeight) + { + } + + public LumpSumRewardsRecord( + Address address, + long startHeight, + BigInteger totalShares, + ImmutableSortedSet
delegators, + IEnumerable lumpSumRewards, + long? lastStartHeight) + { + Address = address; + StartHeight = startHeight; + TotalShares = totalShares; + Delegators = delegators; + + if (!lumpSumRewards.Select(f => f.Currency).All(new HashSet().Add)) + { + throw new ArgumentException("Duplicated currency in lump sum rewards."); + } + + LumpSumRewards = lumpSumRewards.ToImmutableDictionary(f => f.Currency, f => f); + LastStartHeight = lastStartHeight; + } + + public LumpSumRewardsRecord(Address address, IValue bencoded) + : this(address, (List)bencoded) + { + } + + public LumpSumRewardsRecord(Address address, List bencoded) + : this( + address, + (Integer)bencoded[0], + (Integer)bencoded[1], + ((List)bencoded[2]).Select(a => new Address(a)).ToImmutableSortedSet(), + ((List)bencoded[3]).Select(v => new FungibleAssetValue(v)), + (Integer?)bencoded.ElementAtOrDefault(4)) + { + } + + private LumpSumRewardsRecord( + Address address, + long startHeight, + BigInteger totalShares, + ImmutableSortedSet
delegators, + ImmutableDictionary lumpSumRewards, + long? lastStartHeight) + { + Address = address; + StartHeight = startHeight; + TotalShares = totalShares; + Delegators = delegators; + LumpSumRewards = lumpSumRewards; + LastStartHeight = lastStartHeight; + } + + public Address Address { get; } + + public long StartHeight { get; } + + public BigInteger TotalShares { get; } + + public ImmutableDictionary LumpSumRewards { get; } + + public ImmutableSortedSet
Delegators { get; } + + public long? LastStartHeight { get; } + + public List Bencoded + { + get + { + var bencoded = List.Empty + .Add(StartHeight) + .Add(TotalShares) + .Add(new List(Delegators.Select(a => a.Bencoded))) + .Add(new List(LumpSumRewards + .OrderBy(r => r.Key, _currencyComparer) + .Select(r => r.Value.Serialize()))); + + return LastStartHeight is long lastStartHeight + ? bencoded.Add(lastStartHeight) + : bencoded; + } + } + + IValue IBencodable.Bencoded => Bencoded; + + public LumpSumRewardsRecord MoveAddress(Address address) + => new LumpSumRewardsRecord( + address, + StartHeight, + TotalShares, + Delegators, + LumpSumRewards, + LastStartHeight); + + public LumpSumRewardsRecord AddLumpSumRewards(IEnumerable rewards) + => rewards.Aggregate(this, (accum, next) => AddLumpSumRewards(accum, next)); + + public LumpSumRewardsRecord AddLumpSumRewards(FungibleAssetValue rewards) + => AddLumpSumRewards(this, rewards); + + public static LumpSumRewardsRecord AddLumpSumRewards(LumpSumRewardsRecord record, FungibleAssetValue rewards) + => new LumpSumRewardsRecord( + record.Address, + record.StartHeight, + record.TotalShares, + record.Delegators, + record.LumpSumRewards.TryGetValue(rewards.Currency, out var cumulative) + ? record.LumpSumRewards.SetItem(rewards.Currency, cumulative + rewards) + : throw new ArgumentException($"Invalid reward currency: {rewards.Currency}"), + record.LastStartHeight); + + public LumpSumRewardsRecord RemoveDelegator(Address delegator) + => new LumpSumRewardsRecord( + Address, + StartHeight, + TotalShares, + Delegators.Remove(delegator), + LumpSumRewards, + LastStartHeight); + + public ImmutableSortedDictionary RewardsDuringPeriod(BigInteger share) + => LumpSumRewards.Keys.Select(k => RewardsDuringPeriod(share, k)) + .ToImmutableSortedDictionary(f => f.Currency, f => f, _currencyComparer); + + public FungibleAssetValue RewardsDuringPeriod(BigInteger share, Currency currency) + => LumpSumRewards.TryGetValue(currency, out var reward) + ? (reward * share).DivRem(TotalShares).Quotient + : throw new ArgumentException($"Invalid reward currency: {currency}"); + + + public override bool Equals(object? obj) + => obj is LumpSumRewardsRecord other && Equals(other); + + public bool Equals(LumpSumRewardsRecord? other) + => ReferenceEquals(this, other) + || (other is LumpSumRewardsRecord lumpSumRewardRecord + && Address == lumpSumRewardRecord.Address + && StartHeight == lumpSumRewardRecord.StartHeight + && TotalShares == lumpSumRewardRecord.TotalShares + && LumpSumRewards.Equals(lumpSumRewardRecord.LumpSumRewards) + && LastStartHeight == lumpSumRewardRecord.LastStartHeight + && Delegators.SequenceEqual(lumpSumRewardRecord.Delegators)); + + public override int GetHashCode() + => Address.GetHashCode(); + } +} diff --git a/Lib9c/Delegation/RebondGrace.cs b/Lib9c/Delegation/RebondGrace.cs new file mode 100644 index 0000000000..f8ab128390 --- /dev/null +++ b/Lib9c/Delegation/RebondGrace.cs @@ -0,0 +1,258 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + public sealed class RebondGrace : IUnbonding, IBencodable, IEquatable + { + private static readonly IComparer _entryComparer + = new UnbondingEntry.Comparer(); + + private readonly IDelegationRepository? _repository; + + public RebondGrace(Address address, int maxEntries, IDelegationRepository? repository = null) + : this( + address, + maxEntries, + ImmutableSortedDictionary>.Empty, + repository) + { + } + + public RebondGrace(Address address, int maxEntries, IValue bencoded, IDelegationRepository? repository = null) + : this(address, maxEntries, (List)bencoded, repository) + { + } + + public RebondGrace(Address address, int maxEntries, List bencoded, IDelegationRepository? repository = null) + : this( + address, + maxEntries, + bencoded.Select(kv => kv is List list + ? new KeyValuePair>( + (Integer)list[0], + ((List)list[1]).Select(e => new UnbondingEntry(e)).ToImmutableList()) + : throw new InvalidCastException( + $"Unable to cast object of type '{kv.GetType()}' to type '{typeof(List)}'.")) + .ToImmutableSortedDictionary(), + repository) + { + } + + public RebondGrace( + Address address, + int maxEntries, + IEnumerable entries, + IDelegationRepository? repository = null) + : this(address, maxEntries, repository) + { + foreach (var entry in entries) + { + AddEntry(entry); + } + } + + private RebondGrace( + Address address, + int maxEntries, + ImmutableSortedDictionary> entries, + IDelegationRepository? repository) + { + if (maxEntries < 0) + { + throw new ArgumentOutOfRangeException( + nameof(maxEntries), + maxEntries, + "The max entries must be greater than or equal to zero."); + } + + Address = address; + MaxEntries = maxEntries; + Entries = entries; + _repository = repository; + } + + public Address Address { get; } + + public int MaxEntries { get; } + + public Address DelegateeAddress { get; } + + public Address DelegatorAddress { get; } + + public IDelegationRepository? Repository => _repository; + + public long LowestExpireHeight => Entries.First().Key; + + public bool IsFull => Entries.Values.Sum(e => e.Count) >= MaxEntries; + + public bool IsEmpty => Entries.IsEmpty; + + // TODO: Use better custom collection type + public ImmutableSortedDictionary> Entries { get; } + + public ImmutableArray FlattenedEntries + => Entries.Values.SelectMany(e => e).ToImmutableArray(); + + public List Bencoded + => new List( + Entries.Select( + sortedDict => new List( + (Integer)sortedDict.Key, + new List(sortedDict.Value.Select(e => e.Bencoded))))); + + IValue IBencodable.Bencoded => Bencoded; + + public RebondGrace Release(long height) + { + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), + height, + "The height must be greater than zero."); + } + + var updatedEntries = Entries; + foreach (var (expireHeight, entries) in updatedEntries) + { + if (expireHeight <= height) + { + updatedEntries = updatedEntries.Remove(expireHeight); + } + else + { + break; + } + } + + return UpdateEntries(updatedEntries); + } + + IUnbonding IUnbonding.Release(long height) => Release(height); + + public RebondGrace Slash( + BigInteger slashFactor, + long infractionHeight, + long height, + out FungibleAssetValue? slashedFAV) + { + CannotMutateRelationsWithoutRepository(); + + var slashed = new SortedDictionary(); + var updatedEntries = Entries; + var entriesToSlash = Entries.TakeWhile(e => e.Key >= infractionHeight); + foreach (var (expireHeight, entries) in entriesToSlash) + { + ImmutableList slashedEntries = ImmutableList.Empty; + foreach (var entry in entries) + { + var slashedEntry = entry.Slash(slashFactor, infractionHeight, out var slashedSingle); + int index = slashedEntries.BinarySearch(slashedEntry, _entryComparer); + slashedEntries = slashedEntries.Insert(index < 0 ? ~index : index, slashedEntry); + if (slashed.TryGetValue(entry.UnbondeeAddress, out var value)) + { + slashed[entry.UnbondeeAddress] = value + slashedSingle; + } + else + { + slashed[entry.UnbondeeAddress] = slashedSingle; + } + } + + updatedEntries = Entries.SetItem(expireHeight, slashedEntries); + } + + slashedFAV = null; + foreach (var (address, slashedEach) in slashed) + { + var delegatee = _repository!.GetDelegatee(address); + var delegator = _repository!.GetDelegator(DelegatorAddress); + delegatee.Unbond(delegator, delegatee.ShareFromFAV(slashedEach), height); + slashedFAV = slashedFAV.HasValue ? slashedFAV + slashedEach : slashedEach; + } + + + return UpdateEntries(updatedEntries); + } + + IUnbonding IUnbonding.Slash( + BigInteger slashFactor, + long infractionHeight, + long height, + out FungibleAssetValue? slashedFAV) + => Slash(slashFactor, infractionHeight, height, out slashedFAV); + + public override bool Equals(object? obj) + => obj is RebondGrace other && Equals(other); + + public bool Equals(RebondGrace? other) + => ReferenceEquals(this, other) + || (other is RebondGrace rebondGrace + && Address.Equals(rebondGrace.Address) + && MaxEntries == rebondGrace.MaxEntries + && FlattenedEntries.SequenceEqual(rebondGrace.FlattenedEntries)); + + public override int GetHashCode() + => Address.GetHashCode(); + + internal RebondGrace Grace( + Address rebondeeAddress, + FungibleAssetValue initialGraceFAV, + long creationHeight, + long expireHeight) + { + if (expireHeight < creationHeight) + { + throw new ArgumentException( + "The expire height must be greater than the creation height."); + } + + return AddEntry( + new UnbondingEntry( + rebondeeAddress, initialGraceFAV, creationHeight, expireHeight)); + } + + private RebondGrace AddEntry(UnbondingEntry entry) + { + if (IsFull) + { + throw new InvalidOperationException("Cannot add more entries."); + } + + if (Entries.TryGetValue(entry.ExpireHeight, out var entries)) + { + int index = entries.BinarySearch(entry, _entryComparer); + return UpdateEntries( + Entries.SetItem( + entry.ExpireHeight, + entries.Insert(index < 0 ? ~index : index, entry))); + } + + return UpdateEntries( + Entries.Add( + entry.ExpireHeight, ImmutableList.Empty.Add(entry))); + } + + private RebondGrace UpdateEntries( + ImmutableSortedDictionary> entries) + => new RebondGrace(Address, MaxEntries, entries, _repository); + + private void CannotMutateRelationsWithoutRepository() + { + if (_repository is null) + { + throw new InvalidOperationException( + "Cannot mutate without repository."); + } + } + } +} diff --git a/Lib9c/Delegation/UnbondLockIn.cs b/Lib9c/Delegation/UnbondLockIn.cs new file mode 100644 index 0000000000..dd86120b0d --- /dev/null +++ b/Lib9c/Delegation/UnbondLockIn.cs @@ -0,0 +1,363 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Model.Stake; + +namespace Nekoyume.Delegation +{ + public sealed class UnbondLockIn : IUnbonding, IBencodable, IEquatable + { + private static readonly IComparer _entryComparer + = new UnbondingEntry.Comparer(); + + private readonly IDelegationRepository? _repository; + + public UnbondLockIn( + Address address, + int maxEntries, + Address delegateeAddress, + Address delegatorAddress, + IDelegationRepository? repository) + : this( + address, + maxEntries, + delegateeAddress, + delegatorAddress, + ImmutableSortedDictionary>.Empty, + repository) + { + _repository = repository; + } + + public UnbondLockIn( + Address address, int maxEntries, IValue bencoded, IDelegationRepository? repository = null) + : this(address, maxEntries, (List)bencoded, repository) + { + } + + public UnbondLockIn( + Address address, int maxEntries, List bencoded, IDelegationRepository? repository = null) + : this( + address, + maxEntries, + new Address(bencoded[0]), + new Address(bencoded[1]), + ((List)bencoded[2]).Select(kv => kv is List list + ? new KeyValuePair>( + (Integer)list[0], + ((List)list[1]).Select(e => new UnbondingEntry(e)).ToImmutableList()) + : throw new InvalidCastException( + $"Unable to cast object of type '{kv.GetType()}' " + + $"to type '{typeof(List)}'.")) + .ToImmutableSortedDictionary(), + repository) + { + } + + public UnbondLockIn( + Address address, + int maxEntries, + Address delegateeAddress, + Address delegatorAddress, + IEnumerable entries, + IDelegationRepository? repository = null) + : this( + address, + maxEntries, + delegateeAddress, + delegatorAddress, + repository) + { + foreach (var entry in entries) + { + AddEntry(entry); + } + } + + private UnbondLockIn( + Address address, + int maxEntries, + Address delegateeAddress, + Address delegatorAddress, + ImmutableSortedDictionary> entries, + IDelegationRepository? repository) + { + if (maxEntries < 0) + { + throw new ArgumentOutOfRangeException( + nameof(maxEntries), + maxEntries, + "The max entries must be greater than or equal to zero."); + } + + Address = address; + MaxEntries = maxEntries; + Entries = entries; + DelegateeAddress = delegateeAddress; + DelegatorAddress = delegatorAddress; + _repository = repository; + } + + public Address Address { get; } + + public int MaxEntries { get; } + + public Address DelegateeAddress { get; } + + public Address DelegatorAddress { get; } + + // TODO: Use better custom collection type + public ImmutableSortedDictionary> Entries { get; } + + public long LowestExpireHeight => Entries.First().Key; + + public bool IsFull => Entries.Values.Sum(e => e.Count) >= MaxEntries; + + public bool IsEmpty => Entries.IsEmpty; + + public ImmutableArray FlattenedEntries + => Entries.Values.SelectMany(e => e).ToImmutableArray(); + + public List Bencoded + => List.Empty + .Add(DelegateeAddress.Bencoded) + .Add(DelegatorAddress.Bencoded) + .Add(new List( + Entries.Select( + sortedDict => new List( + (Integer)sortedDict.Key, + new List(sortedDict.Value.Select(e => e.Bencoded)))))); + + IValue IBencodable.Bencoded => Bencoded; + + public UnbondLockIn Release(long height) + { + CannotMutateRelationsWithoutRepository(); + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), + height, + "The height must be greater than zero."); + } + + var updatedEntries = Entries; + FungibleAssetValue? releasingFAV = null; + foreach (var (expireHeight, entries) in updatedEntries) + { + if (expireHeight <= height) + { + FungibleAssetValue entriesFAV = entries + .Select(e => e.UnbondingFAV) + .Aggregate((accum, next) => accum + next); + releasingFAV = releasingFAV.HasValue + ? releasingFAV.Value + entriesFAV + : entriesFAV; + updatedEntries = updatedEntries.Remove(expireHeight); + } + else + { + break; + } + } + + if (releasingFAV.HasValue) + { + if (DelegateeAddress != Addresses.NonValidatorDelegatee) + { + var delegateeMetadata = _repository!.GetDelegateeMetadata(DelegateeAddress); + var delegatorMetadata = _repository.GetDelegatorMetadata(DelegatorAddress); + _repository!.TransferAsset( + delegateeMetadata.DelegationPoolAddress, + delegatorMetadata.DelegationPoolAddress, + releasingFAV.Value); + } + else + { + var stakeStateAddress = StakeState.DeriveAddress(DelegatorAddress); + _repository!.TransferAsset( + Addresses.NonValidatorDelegatee, + stakeStateAddress, + releasingFAV.Value); + } + } + + return UpdateEntries(updatedEntries); + } + + IUnbonding IUnbonding.Release(long height) => Release(height); + + public UnbondLockIn Slash( + BigInteger slashFactor, + long infractionHeight, + long height, + out FungibleAssetValue? slashedFAV) + { + slashedFAV = null; + var updatedEntries = Entries; + var entriesToSlash = Entries.TakeWhile(e => e.Key >= infractionHeight); + foreach (var (expireHeight, entries) in entriesToSlash) + { + ImmutableList slashedEntries = ImmutableList.Empty; + foreach (var entry in entries) + { + var slashedEntry = entry.Slash(slashFactor, infractionHeight, out var slashedSingle); + int index = slashedEntries.BinarySearch(slashedEntry, _entryComparer); + slashedEntries = slashedEntries.Insert(index < 0 ? ~index : index, slashedEntry); + slashedFAV = slashedFAV.HasValue + ? slashedFAV.Value + slashedSingle + : slashedSingle; + } + + updatedEntries = Entries.SetItem(expireHeight, slashedEntries); + } + + return UpdateEntries(updatedEntries); + } + + IUnbonding IUnbonding.Slash( + BigInteger slashFactor, + long infractionHeight, + long height, + out FungibleAssetValue? slashedFAV) + => Slash(slashFactor, infractionHeight, height, out slashedFAV); + + public override bool Equals(object? obj) + => obj is UnbondLockIn other && Equals(other); + + public bool Equals(UnbondLockIn? other) + => ReferenceEquals(this, other) + || (other is UnbondLockIn unbondLockIn + && Address.Equals(unbondLockIn.Address) + && MaxEntries == unbondLockIn.MaxEntries + && DelegateeAddress.Equals(unbondLockIn.DelegateeAddress) + && DelegatorAddress.Equals(unbondLockIn.DelegatorAddress) + && FlattenedEntries.SequenceEqual(unbondLockIn.FlattenedEntries)); + + public override int GetHashCode() + => Address.GetHashCode(); + + internal UnbondLockIn LockIn( + FungibleAssetValue lockInFAV, long creationHeight, long expireHeight) + { + if (expireHeight < creationHeight) + { + throw new ArgumentException("The expire height must be greater than the creation height."); + } + + return AddEntry(new UnbondingEntry(DelegateeAddress, lockInFAV, creationHeight, expireHeight)); + } + + internal UnbondLockIn Cancel(FungibleAssetValue cancellingFAV, long height) + { + if (cancellingFAV.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(cancellingFAV), + cancellingFAV, + "The cancelling FAV must be greater than zero."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), + height, + "The height must be greater than zero."); + } + + if (Cancellable(height) < cancellingFAV) + { + throw new InvalidOperationException("Cannot cancel more than locked-in FAV."); + } + + var updatedEntries = Entries; + foreach (var (expireHeight, entries) in updatedEntries.Reverse()) + { + if (expireHeight <= height) + { + throw new InvalidOperationException("Cannot cancel released undelegation."); + } + + foreach (var entry in entries.Select((value, index) => (value, index)).Reverse()) + { + if (cancellingFAV.Sign == 0) + { + break; + } + + if (entry.value.UnbondingFAV <= cancellingFAV) + { + cancellingFAV -= entry.value.UnbondingFAV; + updatedEntries = updatedEntries.SetItem( + expireHeight, + updatedEntries[expireHeight].RemoveAt(entry.index)); + } + else + { + var cancelledEntry = entry.value.Cancel(cancellingFAV); + cancellingFAV -= entry.value.UnbondingFAV; + updatedEntries = updatedEntries.SetItem( + expireHeight, + updatedEntries[expireHeight].SetItem(entry.index, cancelledEntry)); + } + } + + if (updatedEntries[expireHeight].IsEmpty) + { + updatedEntries = updatedEntries.Remove(expireHeight); + } + } + + return UpdateEntries(updatedEntries); + } + + internal FungibleAssetValue Cancellable(long height) + => Entries + .Where(kv => kv.Key > height) + .SelectMany(kv => kv.Value) + .Select(e => e.UnbondingFAV) + .Aggregate((accum, next) => accum + next); + + private UnbondLockIn UpdateEntries( + ImmutableSortedDictionary> entries) + => new UnbondLockIn(Address, MaxEntries, DelegateeAddress, DelegatorAddress, entries, _repository); + + private UnbondLockIn AddEntry(UnbondingEntry entry) + { + if (IsFull) + { + throw new InvalidOperationException("Cannot add more entries."); + } + + if (Entries.TryGetValue(entry.ExpireHeight, out var entries)) + { + int index = entries.BinarySearch(entry, _entryComparer); + return UpdateEntries( + Entries.SetItem( + entry.ExpireHeight, + entries.Insert(index < 0 ? ~index : index, entry))); + } + + return UpdateEntries( + Entries.Add( + entry.ExpireHeight, ImmutableList.Empty.Add(entry))); + } + + private void CannotMutateRelationsWithoutRepository() + { + if (_repository is null) + { + throw new InvalidOperationException( + "Cannot mutate without repository."); + } + } + } +} diff --git a/Lib9c/Delegation/UnbondingEntry.cs b/Lib9c/Delegation/UnbondingEntry.cs new file mode 100644 index 0000000000..66484a382b --- /dev/null +++ b/Lib9c/Delegation/UnbondingEntry.cs @@ -0,0 +1,243 @@ +#nullable enable +using System; +using System.Numerics; +using System.Collections.Generic; +using Bencodex.Types; +using Bencodex; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Delegation +{ + public class UnbondingEntry : IBencodable, IEquatable + { + private int? _cachedHashCode; + + public UnbondingEntry( + Address unbondeeAddress, + FungibleAssetValue unbondingFAV, + long creationHeight, + long expireHeight) + : this(unbondeeAddress, unbondingFAV, unbondingFAV, creationHeight, expireHeight) + { + } + + public UnbondingEntry(IValue bencoded) + : this((List)bencoded) + { + } + + private UnbondingEntry(List bencoded) + : this( + new Address(bencoded[0]), + new FungibleAssetValue(bencoded[1]), + new FungibleAssetValue(bencoded[2]), + (Integer)bencoded[3], + (Integer)bencoded[4]) + { + } + + public UnbondingEntry( + Address unbondeeAddress, + FungibleAssetValue initialUnbondingFAV, + FungibleAssetValue unbondingFAV, + long creationHeight, + long expireHeight) + { + if (initialUnbondingFAV.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(initialUnbondingFAV), + initialUnbondingFAV, + "The initial unbonding FAV must be greater than zero."); + } + + if (unbondingFAV.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(unbondingFAV), + unbondingFAV, + "The unbonding FAV must be greater than zero."); + } + + if (unbondingFAV > initialUnbondingFAV) + { + throw new ArgumentOutOfRangeException( + nameof(unbondingFAV), + unbondingFAV, + "The unbonding FAV must be less than or equal to the initial unbonding FAV."); + } + + if (creationHeight < 0) + { + throw new ArgumentOutOfRangeException( + nameof(creationHeight), + creationHeight, + "The creation height must be greater than or equal to zero."); + } + + if (expireHeight < creationHeight) + { + throw new ArgumentOutOfRangeException( + nameof(expireHeight), + expireHeight, + "The expire height must be greater than the creation height."); + } + + UnbondeeAddress = unbondeeAddress; + InitialUnbondingFAV = initialUnbondingFAV; + UnbondingFAV = unbondingFAV; + CreationHeight = creationHeight; + ExpireHeight = expireHeight; + } + + public Address UnbondeeAddress { get; } + + public FungibleAssetValue InitialUnbondingFAV { get; } + + public FungibleAssetValue UnbondingFAV { get; } + + public long CreationHeight { get; } + + public long ExpireHeight { get; } + + public List Bencoded => List.Empty + .Add(UnbondeeAddress.Bencoded) + .Add(InitialUnbondingFAV.Serialize()) + .Add(UnbondingFAV.Serialize()) + .Add(CreationHeight) + .Add(ExpireHeight); + + IValue IBencodable.Bencoded => Bencoded; + + public override bool Equals(object? obj) + => obj is UnbondingEntry other && Equals(other); + + public bool Equals(UnbondingEntry? other) + => ReferenceEquals(this, other) + || (other is UnbondingEntry rebondGraceEntry + && UnbondeeAddress.Equals(rebondGraceEntry.UnbondeeAddress) + && InitialUnbondingFAV.Equals(rebondGraceEntry.InitialUnbondingFAV) + && UnbondingFAV.Equals(rebondGraceEntry.UnbondingFAV) + && CreationHeight == rebondGraceEntry.CreationHeight + && ExpireHeight == rebondGraceEntry.ExpireHeight); + + public override int GetHashCode() + { + if (_cachedHashCode is int cached) + { + return cached; + } + + int hash = HashCode.Combine( + UnbondeeAddress, + InitialUnbondingFAV, + UnbondingFAV, + CreationHeight, + ExpireHeight); + + _cachedHashCode = hash; + return hash; + } + + public UnbondingEntry Slash( + BigInteger slashFactor, long infractionHeight, out FungibleAssetValue slashedFAV) + { + if (CreationHeight > infractionHeight || + ExpireHeight < infractionHeight) + { + throw new ArgumentOutOfRangeException( + nameof(infractionHeight), + infractionHeight, + "The infraction height must be between in creation height and expire height of entry."); + } + + var favToSlash = InitialUnbondingFAV.DivRem(slashFactor, out var rem); + if (rem.Sign > 0) + { + favToSlash += FungibleAssetValue.FromRawValue(rem.Currency, 1); + } + + slashedFAV = favToSlash < UnbondingFAV + ? favToSlash + : UnbondingFAV; + + return new UnbondingEntry( + UnbondeeAddress, + InitialUnbondingFAV, + UnbondingFAV - slashedFAV, + CreationHeight, + ExpireHeight); + } + + internal UnbondingEntry Cancel(FungibleAssetValue cancellingFAV) + { + if (cancellingFAV.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(cancellingFAV), + cancellingFAV, + "The cancelling FAV must be greater than zero."); + } + + if (UnbondingFAV <= cancellingFAV) + { + throw new InvalidOperationException("Cannot cancel more than unbonding FAV."); + } + + return new UnbondingEntry( + UnbondeeAddress, + InitialUnbondingFAV - cancellingFAV, + UnbondingFAV - cancellingFAV, + CreationHeight, + ExpireHeight); + } + + public class Comparer : IComparer + { + public int Compare(UnbondingEntry? x, UnbondingEntry? y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (x is null) + { + return -1; + } + + if (y is null) + { + return 1; + } + + int comparison = x.ExpireHeight.CompareTo(y.ExpireHeight); + if (comparison != 0) + { + return comparison; + } + + comparison = x.CreationHeight.CompareTo(y.CreationHeight); + if (comparison != 0) + { + return comparison; + } + + comparison = -x.InitialUnbondingFAV.CompareTo(y.InitialUnbondingFAV); + if (comparison != 0) + { + return comparison; + } + + comparison = -x.UnbondingFAV.CompareTo(y.UnbondingFAV); + if (comparison != 0) + { + return comparison; + } + + return x.UnbondeeAddress.CompareTo(y.UnbondeeAddress); + } + } + } +} diff --git a/Lib9c/Delegation/UnbondingFactory.cs b/Lib9c/Delegation/UnbondingFactory.cs new file mode 100644 index 0000000000..67f549865b --- /dev/null +++ b/Lib9c/Delegation/UnbondingFactory.cs @@ -0,0 +1,31 @@ +using System; +using Bencodex.Types; + +namespace Nekoyume.Delegation +{ + public static class UnbondingFactory + { + public static IUnbonding GetUnbondingFromRef( + UnbondingRef reference, IDelegationRepository repository) + => reference.UnbondingType switch + { + UnbondingType.UnbondLockIn => repository.GetUnlimitedUnbondLockIn(reference.Address), + UnbondingType.RebondGrace => repository.GetUnlimitedRebondGrace(reference.Address), + _ => throw new ArgumentException("Invalid unbonding type.") + }; + + public static IUnbonding GetUnbondingFromRef( + IValue bencoded, IDelegationRepository repository) + => GetUnbondingFromRef(new UnbondingRef(bencoded), repository); + + public static UnbondingRef ToReference(IUnbonding unbonding) + => unbonding switch + { + UnbondLockIn unbondLockIn + => new UnbondingRef(unbonding.Address, UnbondingType.UnbondLockIn), + RebondGrace rebondGrace + => new UnbondingRef(unbonding.Address, UnbondingType.RebondGrace), + _ => throw new ArgumentException("Invalid unbonding type.") + }; + } +} diff --git a/Lib9c/Delegation/UnbondingRef.cs b/Lib9c/Delegation/UnbondingRef.cs new file mode 100644 index 0000000000..d14d256e4f --- /dev/null +++ b/Lib9c/Delegation/UnbondingRef.cs @@ -0,0 +1,95 @@ +#nullable enable +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; +using System; + +namespace Nekoyume.Delegation +{ + public class UnbondingRef : IEquatable, IComparable, IComparable, IBencodable + { + public UnbondingRef(Address address, UnbondingType unbondingType) + { + Address = address; + UnbondingType = unbondingType; + } + + public UnbondingRef(IValue bencoded) + : this((List)bencoded) + { + } + + public UnbondingRef(List list) + : this(new Address((Binary)list[0]), (UnbondingType)(int)(Integer)list[1]) + { + } + + public Address Address { get; } + + public UnbondingType UnbondingType { get; } + + public List Bencoded => List.Empty + .Add(Address.Bencoded) + .Add((int)UnbondingType); + + public static bool operator ==(UnbondingRef? left, UnbondingRef? right) + => left?.Equals(right) ?? right is null; + + public static bool operator !=(UnbondingRef? left, UnbondingRef? right) + => !(left == right); + + public static bool operator <(UnbondingRef left, UnbondingRef right) + => left.CompareTo(right) < 0; + + public static bool operator <=(UnbondingRef left, UnbondingRef right) + => left.CompareTo(right) <= 0; + + public static bool operator >(UnbondingRef left, UnbondingRef right) + => left.CompareTo(right) > 0; + + public static bool operator >=(UnbondingRef left, UnbondingRef right) + => left.CompareTo(right) >= 0; + + IValue IBencodable.Bencoded => Bencoded; + + public int CompareTo(object? obj) + => obj is UnbondingRef other + ? CompareTo(other) + : throw new ArgumentException("Object is not a UnbondingRef."); + + public int CompareTo(UnbondingRef? other) + { + if (ReferenceEquals(this, other)) + { + return 0; + } + + if (other is null) + { + return 1; + } + + int addressComparison = Address.CompareTo(other.Address); + if (addressComparison != 0) + { + return addressComparison; + } + + return UnbondingType.CompareTo(other.UnbondingType); + } + + public override bool Equals(object? obj) + => obj is UnbondingRef other && Equals(other); + + public bool Equals(UnbondingRef? other) + => ReferenceEquals(this, other) + || (other is UnbondingRef unbondingRef + && Address.Equals(unbondingRef.Address) + && UnbondingType == unbondingRef.UnbondingType); + + public override int GetHashCode() + { + return HashCode.Combine(Address, UnbondingType); + } + } +} diff --git a/Lib9c/Delegation/UnbondingSet.cs b/Lib9c/Delegation/UnbondingSet.cs new file mode 100644 index 0000000000..98a4b53fd5 --- /dev/null +++ b/Lib9c/Delegation/UnbondingSet.cs @@ -0,0 +1,173 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; + +namespace Nekoyume.Delegation +{ + public sealed class UnbondingSet : IBencodable + { + private readonly IDelegationRepository _repository; + private ImmutableSortedDictionary _lowestExpireHeights; + + public UnbondingSet(IDelegationRepository repository) + : this( + ImmutableSortedDictionary>.Empty, + ImmutableSortedDictionary.Empty, + repository) + { + } + + public UnbondingSet(IValue bencoded, IDelegationRepository repository) + : this((List)bencoded, repository) + { + } + + public UnbondingSet(List bencoded, IDelegationRepository repository) + : this( + ((List)bencoded[0]).Select( + kv => new KeyValuePair>( + (Integer)((List)kv)[0], + ((List)((List)kv)[1]).Select(a => new UnbondingRef(a)).ToImmutableSortedSet())) + .ToImmutableSortedDictionary(), + ((List)bencoded[1]).Select( + kv => new KeyValuePair( + new UnbondingRef(((List)kv)[0]), + (Integer)((List)kv)[1])) + .ToImmutableSortedDictionary(), + repository) + { + } + + private UnbondingSet( + ImmutableSortedDictionary> unbondings, + ImmutableSortedDictionary lowestExpireHeights, + IDelegationRepository repository) + { + UnbondingRefs = unbondings; + _lowestExpireHeights = lowestExpireHeights; + _repository = repository; + } + + public static Address Address => new Address( + ImmutableArray.Create( + 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55)); + + public ImmutableSortedDictionary> UnbondingRefs { get; } + + public ImmutableArray FlattenedUnbondingRefs + => UnbondingRefs.Values.SelectMany(e => e).ToImmutableArray(); + + public IDelegationRepository Repository => _repository; + + public List Bencoded + => List.Empty + .Add(new List( + UnbondingRefs.Select( + sortedDict => new List( + (Integer)sortedDict.Key, + new List(sortedDict.Value.Select(a => a.Bencoded)))))) + .Add(new List( + _lowestExpireHeights.Select( + sortedDict => new List( + sortedDict.Key.Bencoded, + (Integer)sortedDict.Value)))); + + IValue IBencodable.Bencoded => Bencoded; + + public bool IsEmpty => UnbondingRefs.IsEmpty; + + public ImmutableArray UnbondingsToRelease(long height) + => UnbondingRefs + .TakeWhile(kv => kv.Key <= height) + .SelectMany(kv => kv.Value) + .Select(unbondingRef => UnbondingFactory.GetUnbondingFromRef(unbondingRef, _repository)) + .ToImmutableArray(); + + public UnbondingSet SetUnbondings(IEnumerable unbondings) + { + UnbondingSet result = this; + foreach (var unbonding in unbondings) + { + result = SetUnbonding(unbonding); + } + + return result; + } + + public UnbondingSet SetUnbonding(IUnbonding unbonding) + { + if (unbonding.IsEmpty) + { + try + { + return RemoveUnbonding(unbonding); + } + catch (ArgumentException) + { + return this; + } + } + + UnbondingRef unbondigRef = UnbondingFactory.ToReference(unbonding); + + if (_lowestExpireHeights.TryGetValue(unbondigRef, out var lowestExpireHeight)) + { + if (lowestExpireHeight == unbonding.LowestExpireHeight) + { + return this; + } + + var refs = UnbondingRefs[lowestExpireHeight]; + return new UnbondingSet( + UnbondingRefs.SetItem( + unbonding.LowestExpireHeight, + refs.Add(unbondigRef)), + _lowestExpireHeights.SetItem( + unbondigRef, unbonding.LowestExpireHeight), + _repository); + } + + return new UnbondingSet( + UnbondingRefs.SetItem( + unbonding.LowestExpireHeight, + ImmutableSortedSet.Empty.Add(unbondigRef)), + _lowestExpireHeights.SetItem( + unbondigRef, unbonding.LowestExpireHeight), + _repository); + } + + private UnbondingSet RemoveUnbonding(IUnbonding unbonding) + { + UnbondingRef unbondigRef = UnbondingFactory.ToReference(unbonding); + + if (_lowestExpireHeights.TryGetValue(unbondigRef, out var expireHeight) + && UnbondingRefs.TryGetValue(expireHeight, out var refs)) + { + refs = refs.Remove(unbondigRef); + + if (refs.IsEmpty) + { + return new UnbondingSet( + UnbondingRefs.Remove(expireHeight), + _lowestExpireHeights.Remove(unbondigRef), + _repository); + } + + return new UnbondingSet( + UnbondingRefs.SetItem(expireHeight, refs), + _lowestExpireHeights.Remove(unbondigRef), + _repository); + } + else + { + throw new ArgumentException("The address is not in the unbonding set."); + } + } + } +} diff --git a/Lib9c/Delegation/UnbondingType.cs b/Lib9c/Delegation/UnbondingType.cs new file mode 100644 index 0000000000..324b5f90e9 --- /dev/null +++ b/Lib9c/Delegation/UnbondingType.cs @@ -0,0 +1,9 @@ +#nullable enable +namespace Nekoyume.Delegation +{ + public enum UnbondingType + { + UnbondLockIn, + RebondGrace, + } +} diff --git a/Lib9c/Model/Guild/Guild.cs b/Lib9c/Model/Guild/Guild.cs index 7f38a88579..c30d184cd2 100644 --- a/Lib9c/Model/Guild/Guild.cs +++ b/Lib9c/Model/Guild/Guild.cs @@ -1,26 +1,45 @@ +#nullable enable using System; using Bencodex; using Bencodex.Types; using Libplanet.Crypto; using Nekoyume.Action; using Nekoyume.TypedAddress; +using Nekoyume.ValidatorDelegation; namespace Nekoyume.Model.Guild { - public class Guild : IEquatable, IBencodable + public class Guild : IBencodable, IEquatable { private const string StateTypeName = "guild"; - private const long StateVersion = 1; + private const long StateVersion = 2; public readonly AgentAddress GuildMasterAddress; - public Guild(AgentAddress guildMasterAddress) + public readonly Address ValidatorAddress; + + public Guild( + GuildAddress address, + AgentAddress guildMasterAddress, + Address validatorAddress, + GuildRepository repository) { + Address = address; GuildMasterAddress = guildMasterAddress; + ValidatorAddress = validatorAddress; + Repository = repository; } - public Guild(List list) : this(new AgentAddress(list[2])) + public Guild( + GuildAddress address, + IValue bencoded, + GuildRepository repository) { + if (bencoded is not List list) + { + throw new InvalidCastException(); + } + if (list[0] is not Text text || text != StateTypeName || list[1] is not Integer integer) { throw new InvalidCastException(); @@ -30,23 +49,54 @@ public Guild(List list) : this(new AgentAddress(list[2])) { throw new FailedLoadStateException("Un-deserializable state."); } + + if (integer == 1) + { + throw new FailedLoadStateException("Does not support version 1."); + } + + Address = address; + GuildMasterAddress = new AgentAddress(list[2]); + ValidatorAddress = new AgentAddress(list[3]); + Repository = repository; } + public GuildAddress Address { get; } + + public GuildRepository Repository { get; } + public List Bencoded => List.Empty .Add(StateTypeName) .Add(StateVersion) - .Add(GuildMasterAddress.Bencoded); + .Add(GuildMasterAddress.Bencoded) + .Add(ValidatorAddress.Bencoded); IValue IBencodable.Bencoded => Bencoded; - public bool Equals(Guild other) + public void ClaimReward(Address validatorAddress, long height) + { + var guildDelegatee = Repository.GetGuildDelegatee(validatorAddress); + var guildDelegator = Repository.GetGuildDelegator(Address); + guildDelegator.ClaimReward(guildDelegatee, height); + + var validatorRepository = new ValidatorRepository(Repository); + var validatorDelegatee = validatorRepository.GetValidatorDelegatee(validatorAddress); + var validatorDelegator = validatorRepository.GetValidatorDelegator(Address); + validatorDelegator.ClaimReward(validatorDelegatee, height); + + Repository.UpdateWorld(validatorRepository.World); + } + + public bool Equals(Guild? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return GuildMasterAddress.Equals(other.GuildMasterAddress); + return Address.Equals(other.Address) + && GuildMasterAddress.Equals(other.GuildMasterAddress) + && ValidatorAddress.Equals(other.ValidatorAddress); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; @@ -56,7 +106,7 @@ public override bool Equals(object obj) public override int GetHashCode() { - return GuildMasterAddress.GetHashCode(); + return HashCode.Combine(Address, GuildMasterAddress, ValidatorAddress); } } } diff --git a/Lib9c/Model/Guild/GuildApplication.cs b/Lib9c/Model/Guild/GuildApplication.cs deleted file mode 100644 index 3f6c7e3a68..0000000000 --- a/Lib9c/Model/Guild/GuildApplication.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Bencodex; -using Bencodex.Types; -using Nekoyume.Action; -using Nekoyume.TypedAddress; - -namespace Nekoyume.Model.Guild -{ - public class GuildApplication : IBencodable - { - private const string StateTypeName = "guild_application"; - private const long StateVersion = 1; - - public readonly GuildAddress GuildAddress; - - public GuildApplication(GuildAddress guildAddress) - { - GuildAddress = guildAddress; - } - - public GuildApplication(List list) : this(new GuildAddress(list[2])) - { - if (list[0] is not Text text || text != StateTypeName || list[1] is not Integer integer) - { - throw new InvalidCastException(); - } - - if (integer > StateVersion) - { - throw new FailedLoadStateException("Un-deserializable state."); - } - } - - public List Bencoded => new( - (Text)StateTypeName, - (Integer)StateVersion, - GuildAddress.Bencoded); - - IValue IBencodable.Bencoded => Bencoded; - } -} diff --git a/Lib9c/Model/Guild/GuildDelegatee.cs b/Lib9c/Model/Guild/GuildDelegatee.cs new file mode 100644 index 0000000000..079fe52377 --- /dev/null +++ b/Lib9c/Model/Guild/GuildDelegatee.cs @@ -0,0 +1,56 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Delegation; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Model.Guild +{ + public class GuildDelegatee + : Delegatee, IEquatable + { + public GuildDelegatee( + Address address, + IEnumerable rewardCurrencies, + GuildRepository repository) + : base( + address: address, + accountAddress: repository.DelegateeAccountAddress, + delegationCurrency: ValidatorDelegatee.ValidatorDelegationCurrency, + rewardCurrencies: rewardCurrencies, + delegationPoolAddress: ValidatorDelegatee.InactiveDelegationPoolAddress, + rewardPoolAddress: DelegationAddress.RewardPoolAddress(address, repository.DelegateeAccountAddress), + rewardRemainderPoolAddress: Addresses.CommunityPool, + slashedPoolAddress: Addresses.CommunityPool, + unbondingPeriod: ValidatorDelegatee.ValidatorUnbondingPeriod, + maxUnbondLockInEntries: ValidatorDelegatee.ValidatorMaxUnbondLockInEntries, + maxRebondGraceEntries: ValidatorDelegatee.ValidatorMaxRebondGraceEntries, + repository: repository) + { + } + + public GuildDelegatee( + Address address, + GuildRepository repository) + : base( + address: address, + repository: repository) + { + } + + public void Activate() + { + Metadata.DelegationPoolAddress = ValidatorDelegatee.ActiveDelegationPoolAddress; + } + + public void Deactivate() + { + Metadata.DelegationPoolAddress = ValidatorDelegatee.InactiveDelegationPoolAddress; + } + + public bool Equals(GuildDelegatee? other) + => Metadata.Equals(other?.Metadata); + } +} diff --git a/Lib9c/Model/Guild/GuildDelegator.cs b/Lib9c/Model/Guild/GuildDelegator.cs new file mode 100644 index 0000000000..5184114888 --- /dev/null +++ b/Lib9c/Model/Guild/GuildDelegator.cs @@ -0,0 +1,34 @@ +#nullable enable +using System; +using Libplanet.Crypto; +using Nekoyume.Delegation; + +namespace Nekoyume.Model.Guild +{ + public class GuildDelegator + : Delegator, IEquatable + { + public GuildDelegator( + Address address, + Address delegationPoolAddress, + GuildRepository repository) + : base( + address: address, + accountAddress: repository.DelegatorAccountAddress, + delegationPoolAddress: delegationPoolAddress, + rewardAddress: address, + repository: repository) + { + } + + public GuildDelegator( + Address address, + GuildRepository repository) + : base(address: address, repository: repository) + { + } + + public bool Equals(GuildDelegator? other) + => Metadata.Equals(other?.Metadata); + } +} diff --git a/Lib9c/Model/Guild/GuildParticipant.cs b/Lib9c/Model/Guild/GuildParticipant.cs index 4b40798073..bdf782cd5b 100644 --- a/Lib9c/Model/Guild/GuildParticipant.cs +++ b/Lib9c/Model/Guild/GuildParticipant.cs @@ -1,26 +1,45 @@ +#nullable enable using System; +using System.Numerics; using Bencodex; using Bencodex.Types; using Libplanet.Crypto; +using Libplanet.Types.Assets; using Nekoyume.Action; using Nekoyume.TypedAddress; +using Nekoyume.ValidatorDelegation; namespace Nekoyume.Model.Guild { + // It Does not inherit from `Delegator`, since `Validator` related functionalities + // will be moved to lower level library. public class GuildParticipant : IBencodable, IEquatable { private const string StateTypeName = "guild_participant"; - private const long StateVersion = 1; + private const long StateVersion = 2; public readonly GuildAddress GuildAddress; - public GuildParticipant(GuildAddress guildAddress) + public GuildParticipant( + AgentAddress address, + GuildAddress guildAddress, + GuildRepository repository) { + Address = address; GuildAddress = guildAddress; + Repository = repository; } - public GuildParticipant(List list) : this(new GuildAddress(list[2])) + public GuildParticipant( + AgentAddress address, + IValue bencoded, + GuildRepository repository) { + if (bencoded is not List list) + { + throw new InvalidCastException(); + } + if (list[0] is not Text text || text != StateTypeName || list[1] is not Integer integer) { throw new InvalidCastException(); @@ -30,8 +49,27 @@ public GuildParticipant(List list) : this(new GuildAddress(list[2])) { throw new FailedLoadStateException("Un-deserializable state."); } + + if (integer == 1) + { + throw new FailedLoadStateException("State version 1 is not supported."); + } + + Address = address; + GuildAddress = new GuildAddress(list[2]); + Repository = repository; } + public AgentAddress Address { get; } + + public Address DelegationPoolAddress + => Repository.GetGuildDelegator(Address).DelegationPoolAddress; + + public Address RewardAddress + => Repository.GetGuildDelegator(Address).RewardAddress; + + public GuildRepository Repository { get; } + public List Bencoded => List.Empty .Add(StateTypeName) .Add(StateVersion) @@ -39,14 +77,99 @@ public GuildParticipant(List list) : this(new GuildAddress(list[2])) IValue IBencodable.Bencoded => Bencoded; - public bool Equals(GuildParticipant other) + public void Delegate(Guild guild, FungibleAssetValue fav, long height) + { + if (fav.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(fav), fav, "Fungible asset value must be positive."); + } + + var guildDelegatee = Repository.GetGuildDelegatee(guild.ValidatorAddress); + var guildDelegator = Repository.GetGuildDelegator(Address); + guildDelegator.Delegate(guildDelegatee, fav, height); + + var validatorRepository = new ValidatorRepository(Repository); + var validatorDelegatee = validatorRepository.GetValidatorDelegatee(guild.ValidatorAddress); + var validatorDelegator = validatorRepository.GetValidatorDelegator(guild.Address); + validatorDelegatee.Bond(validatorDelegator, fav, height); + + Repository.UpdateWorld(validatorRepository.World); + } + + public void Undelegate(Guild guild, BigInteger share, long height) + { + if (share.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(share), share, "Share must be positive."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), height, "Height must be positive."); + } + + var guildDelegatee = Repository.GetGuildDelegatee(guild.ValidatorAddress); + var guildDelegator = Repository.GetGuildDelegator(Address); + guildDelegator.Undelegate(guildDelegatee, share, height); + + var validatorRepository = new ValidatorRepository(Repository); + var validatorDelegatee = validatorRepository.GetValidatorDelegatee(guild.ValidatorAddress); + var validatorDelegator = validatorRepository.GetValidatorDelegator(guild.Address); + validatorDelegatee.Unbond(validatorDelegator, share, height); + + Repository.UpdateWorld(validatorRepository.World); + } + + public void Redelegate( + Guild srcGuild, Guild dstGuild, BigInteger share, long height) + { + if (share.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(share), share, "Share must be positive."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(height), height, "Height must be positive."); + } + + var srcGuildDelegatee = Repository.GetGuildDelegatee(srcGuild.ValidatorAddress); + var dstGuildDelegator = Repository.GetGuildDelegatee(dstGuild.ValidatorAddress); + var guildDelegator = Repository.GetGuildDelegator(Address); + guildDelegator.Redelegate(srcGuildDelegatee, dstGuildDelegator, share, height); + + var validatorRepository = new ValidatorRepository(Repository); + var srcValidatorDelegatee = validatorRepository.GetValidatorDelegatee(srcGuild.ValidatorAddress); + var srcValidatorDelegator = validatorRepository.GetValidatorDelegator(srcGuild.Address); + var fav = srcValidatorDelegatee.Unbond(srcValidatorDelegator, share, height); + var dstValidatorDelegatee = validatorRepository.GetValidatorDelegatee(dstGuild.ValidatorAddress); + var dstValidatorDelegator = validatorRepository.GetValidatorDelegator(dstGuild.Address); + dstValidatorDelegatee.Bond(dstValidatorDelegator, fav, height); + + Repository.UpdateWorld(validatorRepository.World); + } + + public void ClaimReward(Guild guild, long height) + { + var guildDelegatee = Repository.GetGuildDelegatee(guild.ValidatorAddress); + var guildDelegator = Repository.GetGuildDelegator(Address); + guildDelegator.ClaimReward(guildDelegatee, height); + } + + public bool Equals(GuildParticipant? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return GuildAddress.Equals(other.GuildAddress); + return Address.Equals(other.Address) + && GuildAddress.Equals(other.GuildAddress); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; @@ -56,7 +179,7 @@ public override bool Equals(object obj) public override int GetHashCode() { - return GuildAddress.GetHashCode(); + return HashCode.Combine(Address, GuildAddress); } } } diff --git a/Lib9c/Model/Guild/GuildRejoinCooldown.cs b/Lib9c/Model/Guild/GuildRejoinCooldown.cs new file mode 100644 index 0000000000..c2cbbc4fbc --- /dev/null +++ b/Lib9c/Model/Guild/GuildRejoinCooldown.cs @@ -0,0 +1,50 @@ +#nullable enable +using System; +using Bencodex; +using Bencodex.Types; +using Nekoyume.TypedAddress; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Model.Guild +{ + public class GuildRejoinCooldown : IBencodable, IEquatable + { + public GuildRejoinCooldown(AgentAddress agentAddress, long quitHeight) + { + AgentAddress = agentAddress; + ReleaseHeight = quitHeight + ValidatorDelegatee.ValidatorUnbondingPeriod; + } + + public GuildRejoinCooldown(AgentAddress agentAddress, IValue bencoded) + : this(agentAddress, ((Integer)bencoded)) + { + } + + public GuildRejoinCooldown(AgentAddress agentAddress, Integer bencoded) + { + AgentAddress = agentAddress; + ReleaseHeight = bencoded; + } + + public AgentAddress AgentAddress { get; } + + public long ReleaseHeight { get; } + + public Integer Bencoded => new Integer(ReleaseHeight); + + IValue IBencodable.Bencoded => Bencoded; + + public long Cooldown(long currentHeight) + { + return Math.Max(0, ReleaseHeight - currentHeight); + } + + public bool Equals(GuildRejoinCooldown? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return AgentAddress.Equals(other.AgentAddress) + && ReleaseHeight == other.ReleaseHeight; + } + } +} diff --git a/Lib9c/Model/Guild/GuildRepository.cs b/Lib9c/Model/Guild/GuildRepository.cs new file mode 100644 index 0000000000..f1394e99e5 --- /dev/null +++ b/Lib9c/Model/Guild/GuildRepository.cs @@ -0,0 +1,126 @@ +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Action; +using Libplanet.Crypto; +using Nekoyume.Action; +using Nekoyume.Delegation; +using Nekoyume.TypedAddress; +using Nekoyume.Model.Stake; + +namespace Nekoyume.Model.Guild +{ + public class GuildRepository : DelegationRepository + { + private readonly Address guildAddress = Addresses.Guild; + private readonly Address guildParticipantAddress = Addresses.GuildParticipant; + + private IAccount _guildAccount; + private IAccount _guildParticipantAccount; + + public GuildRepository(IDelegationRepository repository) + : this(repository.World, repository.ActionContext) + { + } + + public GuildRepository(IWorld world, IActionContext actionContext) + : base( + world: world, + actionContext: actionContext, + delegateeAccountAddress: Addresses.GuildDelegateeMetadata, + delegatorAccountAddress: Addresses.GuildDelegatorMetadata, + delegateeMetadataAccountAddress: Addresses.GuildDelegateeMetadata, + delegatorMetadataAccountAddress: Addresses.GuildDelegatorMetadata, + bondAccountAddress: Addresses.GuildBond, + unbondLockInAccountAddress: Addresses.GuildUnbondLockIn, + rebondGraceAccountAddress: Addresses.GuildRebondGrace, + unbondingSetAccountAddress: Addresses.GuildUnbondingSet, + lumpSumRewardRecordAccountAddress: Addresses.GuildLumpSumRewardsRecord) + { + _guildAccount = world.GetAccount(guildAddress); + _guildParticipantAccount = world.GetAccount(guildParticipantAddress); + } + + public override IWorld World => base.World + .SetAccount(guildAddress, _guildAccount) + .SetAccount(guildParticipantAddress, _guildParticipantAccount); + + public GuildDelegatee GetGuildDelegatee(Address address) + => new GuildDelegatee(address, this); + + public override IDelegatee GetDelegatee(Address address) + => GetGuildDelegatee(address); + + public GuildDelegator GetGuildDelegator(Address address) + { + try + { + return new GuildDelegator(address, this); + } + catch (FailedLoadStateException) + { + return new GuildDelegator( + address, + StakeState.DeriveAddress(address), + this); + } + } + public override IDelegator GetDelegator(Address address) + => GetGuildDelegator(address); + + public void SetGuildDelgatee(GuildDelegatee guildDelegatee) + { + SetDelegateeMetadata(guildDelegatee.Metadata); + } + + public override void SetDelegatee(IDelegatee delegatee) + => SetGuildDelgatee(delegatee as GuildDelegatee); + + public void SetGuildDelegator(GuildDelegator guildDelegator) + { + SetDelegatorMetadata(guildDelegator.Metadata); + } + + public override void SetDelegator(IDelegator delegator) + => SetGuildDelegator(delegator as GuildDelegator); + + public Guild GetGuild(Address address) + => _guildAccount.GetState(address) is IValue bencoded + ? new Guild( + new GuildAddress(address), + bencoded, + this) + : throw new FailedLoadStateException("Guild does not exist."); + + public void SetGuild(Guild guild) + { + _guildAccount = _guildAccount.SetState( + guild.Address, guild.Bencoded); + } + + public GuildParticipant GetGuildParticipant(Address address) + => _guildParticipantAccount.GetState(address) is IValue bencoded + ? new GuildParticipant( + new AgentAddress(address), + bencoded, + this) + : throw new FailedLoadStateException("Guild participant does not exist."); + + public void SetGuildParticipant(GuildParticipant guildParticipant) + { + _guildParticipantAccount = _guildParticipantAccount.SetState( + guildParticipant.Address, guildParticipant.Bencoded); + } + + public void RemoveGuildParticipant(Address guildParticipantAddress) + { + _guildParticipantAccount = _guildParticipantAccount.RemoveState(guildParticipantAddress); + } + + public override void UpdateWorld(IWorld world) + { + base.UpdateWorld(world); + _guildAccount = world.GetAccount(guildAddress); + _guildParticipantAccount = world.GetAccount(guildParticipantAddress); + } + } +} diff --git a/Lib9c/Model/Stake/Contract.cs b/Lib9c/Model/Stake/Contract.cs index 738ed66b0b..6a5464712b 100644 --- a/Lib9c/Model/Stake/Contract.cs +++ b/Lib9c/Model/Stake/Contract.cs @@ -18,6 +18,7 @@ public const string StakeRegularRewardSheetPrefix public string StakeRegularFixedRewardSheetTableName { get; } public string StakeRegularRewardSheetTableName { get; } public long RewardInterval { get; } + [Obsolete("Not used because of guild system")] public long LockupInterval { get; } public Contract(StakePolicySheet stakePolicySheet) : this( diff --git a/Lib9c/Model/Stake/StakeStateV2.cs b/Lib9c/Model/Stake/StakeState.cs similarity index 73% rename from Lib9c/Model/Stake/StakeStateV2.cs rename to Lib9c/Model/Stake/StakeState.cs index 7b72cbe23e..6ac1e628c7 100644 --- a/Lib9c/Model/Stake/StakeStateV2.cs +++ b/Lib9c/Model/Stake/StakeState.cs @@ -5,14 +5,15 @@ namespace Nekoyume.Model.Stake { - public readonly struct StakeStateV2 : IState + public readonly struct StakeState : IState { public const string StateTypeName = "stake_state"; - public const int StateTypeVersion = 2; + public const int LatestStateTypeVersion = 3; public static Address DeriveAddress(Address address) => - StakeState.DeriveAddress(address); + LegacyStakeState.DeriveAddress(address); + public readonly int StateVersion; public readonly Contract Contract; public readonly long StartedBlockIndex; public readonly long ReceivedBlockIndex; @@ -31,10 +32,11 @@ out _ public long ClaimableBlockIndex => ClaimedBlockIndex + Contract.RewardInterval; - public StakeStateV2( + public StakeState( Contract contract, long startedBlockIndex, - long receivedBlockIndex = 0) + long receivedBlockIndex = 0, + int stateVersion = LatestStateTypeVersion) { if (startedBlockIndex < 0) { @@ -55,20 +57,23 @@ public StakeStateV2( Contract = contract ?? throw new ArgumentNullException(nameof(contract)); StartedBlockIndex = startedBlockIndex; ReceivedBlockIndex = receivedBlockIndex; + StateVersion = stateVersion; } - public StakeStateV2( - StakeState stakeState, + // Migration constructor V1 to V2. + public StakeState( + LegacyStakeState legacyStakeState, Contract contract ) : this( contract, - stakeState?.StartedBlockIndex ?? throw new ArgumentNullException(nameof(stakeState)), - stakeState.ReceivedBlockIndex + legacyStakeState?.StartedBlockIndex ?? throw new ArgumentNullException(nameof(legacyStakeState)), + legacyStakeState.ReceivedBlockIndex, + stateVersion: 2 ) { } - public StakeStateV2(IValue serialized) + public StakeState(IValue serialized) { if (serialized is not List list) { @@ -80,7 +85,8 @@ public StakeStateV2(IValue serialized) if (list[0] is not Text stateTypeNameValue || stateTypeNameValue != StateTypeName || list[1] is not Integer stateTypeVersionValue || - stateTypeVersionValue.Value != StateTypeVersion) + stateTypeVersionValue.Value == 1 || + stateTypeVersionValue.Value > LatestStateTypeVersion) { throw new ArgumentException( nameof(serialized), @@ -89,6 +95,7 @@ list[1] is not Integer stateTypeVersionValue || const int reservedCount = 2; + StateVersion = stateTypeVersionValue; Contract = new Contract(list[reservedCount]); StartedBlockIndex = (Integer)list[reservedCount + 1]; ReceivedBlockIndex = (Integer)list[reservedCount + 2]; @@ -96,22 +103,23 @@ list[1] is not Integer stateTypeVersionValue || public IValue Serialize() => new List( (Text)StateTypeName, - (Integer)StateTypeVersion, + (Integer)StateVersion, Contract.Serialize(), (Integer)StartedBlockIndex, (Integer)ReceivedBlockIndex ); - public bool Equals(StakeStateV2 other) + public bool Equals(StakeState other) { return Equals(Contract, other.Contract) && StartedBlockIndex == other.StartedBlockIndex && - ReceivedBlockIndex == other.ReceivedBlockIndex; + ReceivedBlockIndex == other.ReceivedBlockIndex && + StateVersion == other.StateVersion; } public override bool Equals(object obj) { - return obj is StakeStateV2 other && Equals(other); + return obj is StakeState other && Equals(other); } public override int GetHashCode() @@ -121,16 +129,17 @@ public override int GetHashCode() var hashCode = (Contract != null ? Contract.GetHashCode() : 0); hashCode = (hashCode * 397) ^ StartedBlockIndex.GetHashCode(); hashCode = (hashCode * 397) ^ ReceivedBlockIndex.GetHashCode(); + hashCode = (hashCode * 397) ^ StateVersion.GetHashCode(); return hashCode; } } - public static bool operator ==(StakeStateV2 left, StakeStateV2 right) + public static bool operator ==(StakeState left, StakeState right) { return left.Equals(right); } - public static bool operator !=(StakeStateV2 left, StakeStateV2 right) + public static bool operator !=(StakeState left, StakeState right) { return !(left == right); } diff --git a/Lib9c/Model/Stake/StakeStateUtils.cs b/Lib9c/Model/Stake/StakeStateUtils.cs index 55ab65894d..92a4b383c6 100644 --- a/Lib9c/Model/Stake/StakeStateUtils.cs +++ b/Lib9c/Model/Stake/StakeStateUtils.cs @@ -1,6 +1,11 @@ +using System; +using System.Diagnostics.CodeAnalysis; using Bencodex.Types; +using Lib9c; +using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; +using Libplanet.Types.Assets; using Nekoyume.Model.State; using Nekoyume.Module; @@ -8,40 +13,40 @@ namespace Nekoyume.Model.Stake { public static class StakeStateUtils { - public static bool TryMigrate( + public static bool TryMigrateV1ToV2( IWorldState state, Address stakeStateAddr, - out StakeStateV2 stakeStateV2) + out StakeState stakeState) { var nullableStateState = - Migrate(state.GetLegacyState(stakeStateAddr), state.GetGameConfigState()); + MigrateV1ToV2(state.GetLegacyState(stakeStateAddr), state.GetGameConfigState()); if (nullableStateState is null) { - stakeStateV2 = default; + stakeState = default; return false; } - stakeStateV2 = nullableStateState.Value; + stakeState = nullableStateState.Value; return true; } - public static bool TryMigrate( + public static bool TryMigrateV1ToV2( IValue serialized, GameConfigState gameConfigState, - out StakeStateV2 stakeStateV2) + out StakeState stakeState) { - var nullableStateState = Migrate(serialized, gameConfigState); + var nullableStateState = MigrateV1ToV2(serialized, gameConfigState); if (nullableStateState is null) { - stakeStateV2 = default; + stakeState = default; return false; } - stakeStateV2 = nullableStateState.Value; + stakeState = nullableStateState.Value; return true; } - public static StakeStateV2? Migrate( + public static StakeState? MigrateV1ToV2( IValue serialized, GameConfigState gameConfigState) { @@ -53,7 +58,7 @@ public static bool TryMigrate( // NOTE: StakeStateV2 is serialized as Bencodex List. if (serialized is List list) { - return new StakeStateV2(list); + return new StakeState(list); } // NOTE: StakeState is serialized as Bencodex Dictionary. @@ -86,7 +91,7 @@ public static bool TryMigrate( // - StakeStateV2.Contract.LockupInterval is StakeState.LockupInterval. // - StakeStateV2.StartedBlockIndex is StakeState.StartedBlockIndex. // - StakeStateV2.ReceivedBlockIndex is StakeState.ReceivedBlockIndex. - var stakeStateV1 = new StakeState(dict); + var stakeStateV1 = new LegacyStakeState(dict); var stakeRegularFixedRewardSheetTableName = stakeStateV1.StartedBlockIndex < gameConfigState.StakeRegularFixedRewardSheet_V2_StartBlockIndex @@ -118,13 +123,44 @@ public static bool TryMigrate( stakeRegularRewardSheetTableName = "StakeRegularRewardSheet_V5"; } - return new StakeStateV2( + return new StakeState( stakeStateV1, new Contract( stakeRegularFixedRewardSheetTableName: stakeRegularFixedRewardSheetTableName, stakeRegularRewardSheetTableName: stakeRegularRewardSheetTableName, - rewardInterval: StakeState.RewardInterval, - lockupInterval: StakeState.LockupInterval)); + rewardInterval: LegacyStakeState.RewardInterval, + lockupInterval: LegacyStakeState.LockupInterval)); + } + + public static bool TryMigrateV2ToV3( + IActionContext context, + IWorld world, + Address stakeStateAddr, + StakeState stakeState, + [NotNullWhen(true)] + out (IWorld world, StakeState newStakeState)? result + ) + { + if (stakeState.StateVersion != 2) + { + result = null; + return false; + } + + var goldCurrency = world.GetGoldCurrency(); + var goldBalance = world.GetBalance(stakeStateAddr, goldCurrency); + var newStakeState = new StakeState( + stakeState.Contract, + stakeState.StartedBlockIndex, + stakeState.ReceivedBlockIndex, + stateVersion: 3); + + result = ( + world.MintAsset(context, stakeStateAddr, + FungibleAssetValue.Parse(Currencies.GuildGold, + goldBalance.GetQuantityString(true))) + .SetLegacyState(stakeStateAddr, newStakeState.Serialize()), newStakeState); + return true; } } } diff --git a/Lib9c/Model/State/StakeState.cs b/Lib9c/Model/State/LegacyStakeState.cs similarity index 98% rename from Lib9c/Model/State/StakeState.cs rename to Lib9c/Model/State/LegacyStakeState.cs index 95f46bbc87..75483527da 100644 --- a/Lib9c/Model/State/StakeState.cs +++ b/Lib9c/Model/State/LegacyStakeState.cs @@ -9,7 +9,7 @@ namespace Nekoyume.Model.State { - public class StakeState : State + public class LegacyStakeState : State { public class StakeAchievements { @@ -68,14 +68,14 @@ public void Achieve(int level, int step) public StakeAchievements Achievements { get; private set; } - public StakeState(Address address, long startedBlockIndex) : base(address) + public LegacyStakeState(Address address, long startedBlockIndex) : base(address) { StartedBlockIndex = startedBlockIndex; CancellableBlockIndex = startedBlockIndex + LockupInterval; Achievements = new StakeAchievements(); } - public StakeState( + public LegacyStakeState( Address address, long startedBlockIndex, long receivedBlockIndex, @@ -89,7 +89,7 @@ StakeAchievements achievements Achievements = achievements; } - public StakeState(Dictionary serialized) : base(serialized) + public LegacyStakeState(Dictionary serialized) : base(serialized) { CancellableBlockIndex = (long)serialized[CancellableBlockIndexKey].ToBigInteger(); StartedBlockIndex = (long)serialized[StartedBlockIndexKey].ToBigInteger(); diff --git a/Lib9c/Module/Guild/GuildApplicationModule.cs b/Lib9c/Module/Guild/GuildApplicationModule.cs deleted file mode 100644 index 3698c2f446..0000000000 --- a/Lib9c/Module/Guild/GuildApplicationModule.cs +++ /dev/null @@ -1,131 +0,0 @@ -#nullable enable -using System; -using System.Diagnostics.CodeAnalysis; -using Bencodex.Types; -using Libplanet.Action.State; -using Libplanet.Crypto; -using Nekoyume.Action; -using Nekoyume.Extensions; -using Nekoyume.Model.Guild; -using Nekoyume.TypedAddress; - -namespace Nekoyume.Module.Guild -{ - public static class GuildApplicationModule - { - public static Model.Guild.GuildApplication GetGuildApplication(this IWorldState worldState, AgentAddress agentAddress) - { - var value = worldState.GetAccountState(Addresses.GuildApplication).GetState(agentAddress); - if (value is List list) - { - return new Model.Guild.GuildApplication(list); - } - - throw new FailedLoadStateException("There is no such guild."); - } - - public static bool TryGetGuildApplication(this IWorldState worldState, - AgentAddress agentAddress, [NotNullWhen(true)] out Model.Guild.GuildApplication? guildApplication) - { - try - { - guildApplication = GetGuildApplication(worldState, agentAddress); - return true; - } - catch - { - guildApplication = null; - return false; - } - } - - public static IWorld ApplyGuild( - this IWorld world, AgentAddress signer, GuildAddress guildAddress) - { - if (world.GetJoinedGuild(signer) is not null) - { - throw new InvalidOperationException("The signer is already joined in a guild."); - } - - // NOTE: Check there is such guild. - if (!world.TryGetGuild(guildAddress, out _)) - { - throw new InvalidOperationException("The guild does not exist."); - } - - if (world.IsBanned(guildAddress, signer)) - { - throw new InvalidOperationException("The signer is banned from the guild."); - } - - return world.MutateAccount(Addresses.GuildApplication, - account => - account.SetState(signer, new GuildApplication(guildAddress).Bencoded)); - } - - public static IWorld CancelGuildApplication( - this IWorld world, AgentAddress agentAddress) - { - if (!world.TryGetGuildApplication(agentAddress, out _)) - { - throw new InvalidOperationException("It may not apply any guild."); - } - - return world.RemoveGuildApplication(agentAddress); - } - - public static IWorld AcceptGuildApplication( - this IWorld world, AgentAddress signer, AgentAddress target) - { - if (!world.TryGetGuildApplication(target, out var guildApplication)) - { - throw new InvalidOperationException("It may not apply any guild."); - } - - if (!world.TryGetGuild(guildApplication.GuildAddress, out var guild)) - { - throw new InvalidOperationException( - "There is no such guild now. It may be removed. Please cancel and apply another guild."); - } - - if (signer != guild.GuildMasterAddress) - { - throw new InvalidOperationException("It may not be a guild master."); - } - - return world.RemoveGuildApplication(target) - .JoinGuild(guildApplication.GuildAddress, target); - } - -#pragma warning disable S4144 - public static IWorld RejectGuildApplication( -#pragma warning restore S4144 - this IWorld world, AgentAddress signer, AgentAddress target) - { - if (!world.TryGetGuildApplication(target, out var guildApplication)) - { - throw new InvalidOperationException("It may not apply any guild."); - } - - if (!world.TryGetGuild(guildApplication.GuildAddress, out var guild)) - { - throw new InvalidOperationException( - "There is no such guild now. It may be removed. Please cancel and apply another guild."); - } - - if (signer != guild.GuildMasterAddress) - { - throw new InvalidOperationException("It may not be a guild master."); - } - - return world.RemoveGuildApplication(target); - } - - private static IWorld RemoveGuildApplication(this IWorld world, AgentAddress agentAddress) - { - return world.MutateAccount(Addresses.GuildApplication, - account => account.RemoveState(agentAddress)); - } - - } -} diff --git a/Lib9c/Module/Guild/GuildBanModule.cs b/Lib9c/Module/Guild/GuildBanModule.cs index bcb189d2f0..810fb02fd8 100644 --- a/Lib9c/Module/Guild/GuildBanModule.cs +++ b/Lib9c/Module/Guild/GuildBanModule.cs @@ -1,8 +1,8 @@ using System; using Libplanet.Action.State; using Libplanet.Crypto; -using Libplanet.Store.Trie; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; using Nekoyume.TypedAddress; using Boolean = Bencodex.Types.Boolean; @@ -10,10 +10,10 @@ namespace Nekoyume.Module.Guild { public static class GuildBanModule { - public static bool IsBanned(this IWorldState worldState, GuildAddress guildAddress, Address agentAddress) + public static bool IsBanned(this GuildRepository repository, GuildAddress guildAddress, Address agentAddress) { var accountAddress = Addresses.GetGuildBanAccountAddress(guildAddress); - var value = worldState.GetAccountState(accountAddress) + var value = repository.World.GetAccountState(accountAddress) .GetState(agentAddress); if (value is Boolean boolean) { @@ -28,9 +28,13 @@ public static bool IsBanned(this IWorldState worldState, GuildAddress guildAddre return false; } - public static IWorld Ban(this IWorld world, GuildAddress guildAddress, AgentAddress signer, AgentAddress target) + public static void Ban( + this GuildRepository repository, + GuildAddress guildAddress, + AgentAddress signer, + AgentAddress target) { - if (!world.TryGetGuild(guildAddress, out var guild)) + if (!repository.TryGetGuild(guildAddress, out var guild)) { throw new InvalidOperationException("There is no such guild."); } @@ -45,22 +49,20 @@ public static IWorld Ban(this IWorld world, GuildAddress guildAddress, AgentAddr throw new InvalidOperationException("The guild master cannot be banned."); } - if (world.TryGetGuildApplication(target, out var guildApplication) && guildApplication.GuildAddress == guildAddress) + if (repository.GetJoinedGuild(target) == guildAddress) { - world = world.RejectGuildApplication(signer, target); + repository.LeaveGuild(target); } - if (world.GetJoinedGuild(target) == guildAddress) - { - world = world.LeaveGuild(target); - } - - return world.MutateAccount(Addresses.GetGuildBanAccountAddress(guildAddress), account => account.SetState(target, (Boolean)true)); + repository.UpdateWorld( + repository.World.MutateAccount( + Addresses.GetGuildBanAccountAddress(guildAddress), + account => account.SetState(target, (Boolean)true))); } - public static IWorld Unban(this IWorld world, GuildAddress guildAddress, AgentAddress signer, Address target) + public static void Unban(this GuildRepository repository, GuildAddress guildAddress, AgentAddress signer, Address target) { - if (!world.TryGetGuild(guildAddress, out var guild)) + if (!repository.TryGetGuild(guildAddress, out var guild)) { throw new InvalidOperationException("There is no such guild."); } @@ -70,17 +72,22 @@ public static IWorld Unban(this IWorld world, GuildAddress guildAddress, AgentAd throw new InvalidOperationException("The signer is not a guild master."); } - if (!world.IsBanned(guildAddress, target)) + if (!repository.IsBanned(guildAddress, target)) { throw new InvalidOperationException("The target is not banned."); } - return world.MutateAccount(Addresses.GetGuildBanAccountAddress(guildAddress), - account => account.RemoveState(target)); + repository.UpdateWorld( + repository.World.MutateAccount( + Addresses.GetGuildBanAccountAddress(guildAddress), + account => account.RemoveState(target))); } - public static IWorld RemoveBanList(this IWorld world, GuildAddress guildAddress) => - world.SetAccount(Addresses.GetGuildBanAccountAddress(guildAddress), GetEmptyAccount(world)); + public static void RemoveBanList(this GuildRepository repository, GuildAddress guildAddress) => + repository.UpdateWorld( + repository.World.SetAccount( + Addresses.GetGuildBanAccountAddress(guildAddress), + GetEmptyAccount(repository.World))); private static IAccount GetEmptyAccount(this IWorld world) { diff --git a/Lib9c/Module/Guild/GuildDelegateeModule.cs b/Lib9c/Module/Guild/GuildDelegateeModule.cs new file mode 100644 index 0000000000..d92f827569 --- /dev/null +++ b/Lib9c/Module/Guild/GuildDelegateeModule.cs @@ -0,0 +1,57 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Model.Guild; +using Nekoyume.Module.ValidatorDelegation; +using Nekoyume.ValidatorDelegation; + + +namespace Nekoyume.Module.Guild +{ + public static class GuildDelegateeModule + { + public static bool TryGetGuildDelegatee( + this GuildRepository repository, + Address address, + [NotNullWhen(true)] out GuildDelegatee? validatorDelegateeForGuildParticipant) + { + try + { + validatorDelegateeForGuildParticipant = repository.GetGuildDelegatee(address); + return true; + } + catch + { + validatorDelegateeForGuildParticipant = null; + return false; + } + } + + public static GuildDelegatee CreateGuildDelegatee( + this GuildRepository repository, + Address address) + { + if (repository.TryGetGuildDelegatee(address, out _)) + { + throw new InvalidOperationException("The signer already has a validator delegatee for guild."); + } + + var validatorRepository = new ValidatorRepository(repository.World, repository.ActionContext); + if (!validatorRepository.TryGetValidatorDelegatee(address, out _)) + { + throw new InvalidOperationException("The signer does not have a validator delegatee."); + } + + var guildDelegatee = new GuildDelegatee( + address, + new Currency[] { repository.World.GetGoldCurrency() }, + repository); + + repository.SetGuildDelgatee(guildDelegatee); + + return guildDelegatee; + } + } +} diff --git a/Lib9c/Module/Guild/GuildMemberCounterModule.cs b/Lib9c/Module/Guild/GuildMemberCounterModule.cs index 52cfc90a02..ed60dfc1b3 100644 --- a/Lib9c/Module/Guild/GuildMemberCounterModule.cs +++ b/Lib9c/Module/Guild/GuildMemberCounterModule.cs @@ -2,17 +2,18 @@ using System; using System.Numerics; using Bencodex.Types; -using Libplanet.Action.State; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; using Nekoyume.TypedAddress; namespace Nekoyume.Module.Guild { public static class GuildMemberCounterModule { - public static BigInteger GetGuildMemberCount(this IWorldState world, GuildAddress guildAddress) + public static BigInteger GetGuildMemberCount( + this GuildRepository repository, GuildAddress guildAddress) { - var account = world.GetAccountState(Addresses.GuildMemberCounter); + var account = repository.World.GetAccountState(Addresses.GuildMemberCounter); return account.GetState(guildAddress) switch { Integer i => i.Value, @@ -21,9 +22,12 @@ public static BigInteger GetGuildMemberCount(this IWorldState world, GuildAddres }; } - public static IWorld IncreaseGuildMemberCount(this IWorld world, GuildAddress guildAddress) + public static GuildRepository IncreaseGuildMemberCount( + this GuildRepository repository, GuildAddress guildAddress) { - return world.MutateAccount(Addresses.GuildMemberCounter, account => + repository.UpdateWorld( + repository.World.MutateAccount( + Addresses.GuildMemberCounter, account => { BigInteger count = account.GetState(guildAddress) switch { @@ -33,12 +37,17 @@ public static IWorld IncreaseGuildMemberCount(this IWorld world, GuildAddress gu }; return account.SetState(guildAddress, (Integer)(count + 1)); - }); + })); + + return repository; } - public static IWorld DecreaseGuildMemberCount(this IWorld world, GuildAddress guildAddress) + public static GuildRepository DecreaseGuildMemberCount( + this GuildRepository repository, GuildAddress guildAddress) { - return world.MutateAccount(Addresses.GuildMemberCounter, account => + repository.UpdateWorld( + repository.World.MutateAccount( + Addresses.GuildMemberCounter, account => { BigInteger count = account.GetState(guildAddress) switch { @@ -54,7 +63,9 @@ public static IWorld DecreaseGuildMemberCount(this IWorld world, GuildAddress gu } return account.SetState(guildAddress, (Integer)(count - 1)); - }); + })); + + return repository; } } } diff --git a/Lib9c/Module/Guild/GuildModule.cs b/Lib9c/Module/Guild/GuildModule.cs index 04a422734c..3b57b1d238 100644 --- a/Lib9c/Module/Guild/GuildModule.cs +++ b/Lib9c/Module/Guild/GuildModule.cs @@ -1,33 +1,30 @@ #nullable enable using System; using System.Diagnostics.CodeAnalysis; -using Bencodex.Types; +using Libplanet.Action; using Libplanet.Action.State; -using Nekoyume.Action; +using Libplanet.Crypto; using Nekoyume.Extensions; +using Nekoyume.Model.Guild; +using Nekoyume.Module.ValidatorDelegation; using Nekoyume.TypedAddress; +using Nekoyume.ValidatorDelegation; namespace Nekoyume.Module.Guild { public static class GuildModule { - public static Model.Guild.Guild GetGuild(this IWorldState worldState, GuildAddress guildAddress) - { - var value = worldState.GetAccountState(Addresses.Guild).GetState(guildAddress); - if (value is List list) - { - return new Model.Guild.Guild(list); - } + public static GuildRepository GetGuildRepository(this IWorld world, IActionContext context) + => new GuildRepository(world, context); - throw new FailedLoadStateException("There is no such guild."); - } - - public static bool TryGetGuild(this IWorldState worldState, - GuildAddress guildAddress, [NotNullWhen(true)] out Model.Guild.Guild? guild) + public static bool TryGetGuild( + this GuildRepository repository, + GuildAddress guildAddress, + [NotNullWhen(true)] out Model.Guild.Guild? guild) { try { - guild = GetGuild(worldState, guildAddress); + guild = repository.GetGuild(guildAddress); return true; } catch @@ -37,33 +34,45 @@ public static bool TryGetGuild(this IWorldState worldState, } } - public static IWorld MakeGuild(this IWorld world, GuildAddress guildAddress, AgentAddress signer) + public static GuildRepository MakeGuild( + this GuildRepository repository, + GuildAddress guildAddress, + Address validatorAddress) { - if (world.GetJoinedGuild(signer) is not null) + var signer = new AgentAddress(repository.ActionContext.Signer); + if (repository.GetJoinedGuild(signer) is not null) { throw new InvalidOperationException("The signer already has a guild."); } - if (world.TryGetGuild(guildAddress, out _)) + if (repository.TryGetGuild(guildAddress, out _)) { throw new InvalidOperationException("Duplicated guild address. Please retry."); } - return world.MutateAccount(Addresses.Guild, - account => - account.SetState(guildAddress, - new Model.Guild.Guild(signer).Bencoded)) - .JoinGuild(guildAddress, signer); + var validatorRepository = new ValidatorRepository(repository.World, repository.ActionContext); + if (!validatorRepository.TryGetValidatorDelegatee(validatorAddress, out _)) + { + throw new InvalidOperationException("The validator does not exist."); + } + + var guild = new Model.Guild.Guild(guildAddress, signer, validatorAddress, repository); + repository.SetGuild(guild); + repository.JoinGuild(guildAddress, signer); + + return repository; } - public static IWorld RemoveGuild(this IWorld world, AgentAddress signer) + public static GuildRepository RemoveGuild( + this GuildRepository repository) { - if (world.GetJoinedGuild(signer) is not { } guildAddress) + var signer = new AgentAddress(repository.ActionContext.Signer); + if (repository.GetJoinedGuild(signer) is not { } guildAddress) { throw new InvalidOperationException("The signer does not join any guild."); } - if (!world.TryGetGuild(guildAddress, out var guild)) + if (!repository.TryGetGuild(guildAddress, out var guild)) { throw new InvalidOperationException("There is no such guild."); } @@ -73,15 +82,27 @@ public static IWorld RemoveGuild(this IWorld world, AgentAddress signer) throw new InvalidOperationException("The signer is not a guild master."); } - if (world.GetGuildMemberCount(guildAddress) > 1) + if (repository.GetGuildMemberCount(guildAddress) > 1) { throw new InvalidOperationException("There are remained participants in the guild."); } - return world - .RawLeaveGuild(signer) - .MutateAccount(Addresses.Guild, account => account.RemoveState(guildAddress)) - .RemoveBanList(guildAddress); + var validatorRepository = new ValidatorRepository(repository.World, repository.ActionContext); + var validatorDelegatee = validatorRepository.GetValidatorDelegatee(guild.ValidatorAddress); + var bond = validatorRepository.GetBond(validatorDelegatee, signer); + if (bond.Share > 0) + { + throw new InvalidOperationException("The signer has a bond with the validator."); + } + + repository.RemoveGuildParticipant(signer); + repository.DecreaseGuildMemberCount(guild.Address); + repository.UpdateWorld( + repository.World.MutateAccount( + Addresses.Guild, account => account.RemoveState(guildAddress))); + repository.RemoveBanList(guildAddress); + + return repository; } } } diff --git a/Lib9c/Module/Guild/GuildParticipantModule.cs b/Lib9c/Module/Guild/GuildParticipantModule.cs index e863a6bac9..8d631e1aec 100644 --- a/Lib9c/Module/Guild/GuildParticipantModule.cs +++ b/Lib9c/Module/Guild/GuildParticipantModule.cs @@ -1,11 +1,14 @@ #nullable enable using System; using System.Diagnostics.CodeAnalysis; +using System.Numerics; using Bencodex.Types; -using Libplanet.Action.State; -using Nekoyume.Action; -using Nekoyume.Extensions; +using Lib9c; +using Libplanet.Types.Assets; +using Nekoyume.Action.Guild.Migration.LegacyModels; +using Nekoyume.Model.Guild; using Nekoyume.TypedAddress; +using Nekoyume.ValidatorDelegation; namespace Nekoyume.Module.Guild { @@ -13,78 +16,117 @@ public static class GuildParticipantModule { // Returns `null` when it didn't join any guild. // Returns `GuildAddress` when it joined a guild. - public static GuildAddress? GetJoinedGuild(this IWorldState worldState, AgentAddress agentAddress) + public static GuildAddress? GetJoinedGuild(this GuildRepository repository, AgentAddress agentAddress) { - return worldState.TryGetGuildParticipant(agentAddress, out var guildParticipant) + return repository.TryGetGuildParticipant(agentAddress, out var guildParticipant) ? guildParticipant.GuildAddress : null; } - public static IWorld JoinGuild( - this IWorld world, + public static GuildRepository JoinGuild( + this GuildRepository repository, GuildAddress guildAddress, AgentAddress target) { - var guildParticipant = new Model.Guild.GuildParticipant(guildAddress); - return world.MutateAccount(Addresses.GuildParticipant, - account => account.SetState(target, guildParticipant.Bencoded)) - .IncreaseGuildMemberCount(guildAddress); - } - - public static IWorld LeaveGuild( - this IWorld world, - AgentAddress target) - { - if (world.GetJoinedGuild(target) is not { } guildAddress) + if (repository.TryGetGuildParticipant(target, out _)) { - throw new InvalidOperationException("The signer does not join any guild."); + throw new ArgumentException("The signer already joined a guild."); } - if (!world.TryGetGuild(guildAddress, out var guild)) + if (repository.GetGuildRejoinCooldown(target) is { } cooldown + && cooldown.Cooldown(repository.ActionContext.BlockIndex) > 0L) { throw new InvalidOperationException( - "There is no such guild."); + $"The signer is in the rejoin cooldown period until block {cooldown.ReleaseHeight}"); } - if (guild.GuildMasterAddress == target) + var guildParticipant = new GuildParticipant(target, guildAddress, repository); + var guildGold = repository.GetBalance(guildParticipant.DelegationPoolAddress, Currencies.GuildGold); + repository.SetGuildParticipant(guildParticipant); + repository.IncreaseGuildMemberCount(guildAddress); + if (guildGold.RawValue > 0) { - throw new InvalidOperationException( - "The signer is a guild master. Guild master cannot quit the guild."); + repository.Delegate(target, guildGold); } - return RawLeaveGuild(world, target); + return repository; } - public static IWorld RawLeaveGuild(this IWorld world, AgentAddress target) + public static GuildRepository MoveGuild( + this GuildRepository repository, + AgentAddress guildParticipantAddress, + GuildAddress dstGuildAddress) { - if (!world.TryGetGuildParticipant(target, out var guildParticipant)) + var guildParticipant1 = repository.GetGuildParticipant(guildParticipantAddress); + var srcGuild = repository.GetGuild(guildParticipant1.GuildAddress); + var dstGuild = repository.GetGuild(dstGuildAddress); + var validatorRepository = new ValidatorRepository(repository.World, repository.ActionContext); + var srcValidatorDelegatee = validatorRepository.GetValidatorDelegatee(srcGuild.ValidatorAddress); + var dstValidatorDelegatee = validatorRepository.GetValidatorDelegatee(dstGuild.ValidatorAddress); + if (dstValidatorDelegatee.Tombstoned) + { + throw new InvalidOperationException("The validator of the guild to move to has been tombstoned."); + } + + var guildParticipant2 = new GuildParticipant(guildParticipantAddress, dstGuildAddress, repository); + var bond = validatorRepository.GetBond(srcValidatorDelegatee, guildParticipantAddress); + repository.RemoveGuildParticipant(guildParticipantAddress); + repository.DecreaseGuildMemberCount(guildParticipant1.GuildAddress); + repository.SetGuildParticipant(guildParticipant2); + repository.IncreaseGuildMemberCount(dstGuildAddress); + if (bond.Share > 0) { - throw new InvalidOperationException("It may not join any guild."); + repository.Redelegate(guildParticipantAddress, dstGuildAddress); } - return world.RemoveGuildParticipant(target) - .DecreaseGuildMemberCount(guildParticipant.GuildAddress); + return repository; } - private static Model.Guild.GuildParticipant GetGuildParticipant(this IWorldState worldState, AgentAddress agentAddress) + public static GuildRepository LeaveGuild( + this GuildRepository repository, + AgentAddress agentAddress) { - var value = worldState.GetAccountState(Addresses.GuildParticipant) - .GetState(agentAddress); - if (value is List list) + if (repository.GetJoinedGuild(agentAddress) is not { } guildAddress) + { + throw new InvalidOperationException("The signer does not join any guild."); + } + + if (!repository.TryGetGuild(guildAddress, out var guild)) + { + throw new InvalidOperationException( + "There is no such guild."); + } + + if (guild.GuildMasterAddress == agentAddress) + { + throw new InvalidOperationException( + "The signer is a guild master. Guild master cannot quit the guild."); + } + + var validatorRepository = new ValidatorRepository(repository.World, repository.ActionContext); + var validatorDelegatee = validatorRepository.GetValidatorDelegatee(guild.ValidatorAddress); + var bond = validatorRepository.GetBond(validatorDelegatee, agentAddress); + + repository.RemoveGuildParticipant(agentAddress); + repository.DecreaseGuildMemberCount(guild.Address); + if (bond.Share > 0) { - return new Model.Guild.GuildParticipant(list); + repository.Undelegate(agentAddress); } - throw new FailedLoadStateException("It may not join any guild."); + repository.SetGuildRejoinCooldown(agentAddress, repository.ActionContext.BlockIndex); + + return repository; } - private static bool TryGetGuildParticipant(this IWorldState worldState, + public static bool TryGetGuildParticipant( + this GuildRepository repository, AgentAddress agentAddress, [NotNullWhen(true)] out Model.Guild.GuildParticipant? guildParticipant) { try { - guildParticipant = GetGuildParticipant(worldState, agentAddress); + guildParticipant = repository.GetGuildParticipant(agentAddress); return true; } catch @@ -94,10 +136,95 @@ private static bool TryGetGuildParticipant(this IWorldState worldState, } } - private static IWorld RemoveGuildParticipant(this IWorld world, AgentAddress agentAddress) + // TODO: Hide this method as private after migration. + public static GuildRepository Delegate( + this GuildRepository repository, + AgentAddress guildParticipantAddress, + FungibleAssetValue fav) + { + var height = repository.ActionContext.BlockIndex; + + // TODO: Remove below unnecessary height condition after migration. + if (repository.World.GetDelegationMigrationHeight() is long migrationHeight) + { + height = Math.Max(height, migrationHeight); + } + + var guildParticipant = repository.GetGuildParticipant(guildParticipantAddress); + var guild = repository.GetGuild(guildParticipant.GuildAddress); + guildParticipant.Delegate(guild, fav, height); + + return repository; + } + + private static GuildRepository Undelegate( + this GuildRepository repository, + AgentAddress guildParticipantAddress) + { + var height = repository.ActionContext.BlockIndex; + var guildParticipant = repository.GetGuildParticipant(guildParticipantAddress); + var guild = repository.GetGuild(guildParticipant.GuildAddress); + var share = repository.GetBond( + new ValidatorRepository(repository.World, repository.ActionContext) + .GetValidatorDelegatee(guild.ValidatorAddress), + guildParticipantAddress).Share; + guildParticipant.Undelegate(guild, share, height); + + return repository; + } + + private static GuildRepository Undelegate( + this GuildRepository repository, + AgentAddress guildParticipantAddress, + BigInteger share) + { + var height = repository.ActionContext.BlockIndex; + var guildParticipant = repository.GetGuildParticipant(guildParticipantAddress); + var guild = repository.GetGuild(guildParticipant.GuildAddress); + guildParticipant.Undelegate(guild, share, height); + + return repository; + } + + public static GuildRepository Redelegate( + this GuildRepository repository, + AgentAddress guildParticipantAddress, + GuildAddress dstGuildAddress) + { + var height = repository.ActionContext.BlockIndex; + var guildParticipant = repository.GetGuildParticipant(guildParticipantAddress); + var guild = repository.GetGuild(guildParticipant.GuildAddress); + var share = repository.GetBond( + new ValidatorRepository(repository.World, repository.ActionContext) + .GetValidatorDelegatee(guild.ValidatorAddress), + guildParticipantAddress).Share; + var dstGuild = repository.GetGuild(dstGuildAddress); + guildParticipant.Redelegate(guild, dstGuild, share, height); + + return repository; + } + + private static GuildRepository SetGuildRejoinCooldown( + this GuildRepository repository, + AgentAddress guildParticipantAddress, + long height) { - return world.MutateAccount(Addresses.GuildParticipant, - account => account.RemoveState(agentAddress)); + var guildRejoinCooldown = new GuildRejoinCooldown(guildParticipantAddress, height); + repository.UpdateWorld( + repository.World.SetAccount( + Addresses.GuildRejoinCooldown, + repository.World.GetAccount(Addresses.GuildRejoinCooldown) + .SetState(guildParticipantAddress, guildRejoinCooldown.Bencoded))); + return repository; } + + private static GuildRejoinCooldown? GetGuildRejoinCooldown( + this GuildRepository repository, + AgentAddress guildParticipantAddress) + => repository.World + .GetAccount(Addresses.GuildRejoinCooldown) + .GetState(guildParticipantAddress) is Integer bencoded + ? new GuildRejoinCooldown(guildParticipantAddress, bencoded) + : null; } } diff --git a/Lib9c/Module/LegacyModule.cs b/Lib9c/Module/LegacyModule.cs index f9cc7dd75d..f9a9b01769 100644 --- a/Lib9c/Module/LegacyModule.cs +++ b/Lib9c/Module/LegacyModule.cs @@ -877,18 +877,18 @@ public static (Address arenaInfoAddress, ArenaInfo arenaInfo, bool isNewArenaInf return (arenaInfoAddress, arenaInfo, isNew); } - public static bool TryGetStakeState( + public static bool TryGetLegacyStakeState( this IWorldState worldState, Address agentAddress, - out StakeState stakeState) + out LegacyStakeState legacyStakeState) { - if (TryGetLegacyState(worldState, StakeState.DeriveAddress(agentAddress), out Dictionary dictionary)) + if (TryGetLegacyState(worldState, LegacyStakeState.DeriveAddress(agentAddress), out Dictionary dictionary)) { - stakeState = new StakeState(dictionary); + legacyStakeState = new LegacyStakeState(dictionary); return true; } - stakeState = null; + legacyStakeState = null; return false; } @@ -897,19 +897,19 @@ public static FungibleAssetValue GetStakedAmount( Address agentAddr) { var goldCurrency = GetGoldCurrency(worldState); - return worldState.GetBalance(StakeState.DeriveAddress(agentAddr), goldCurrency); + return worldState.GetBalance(LegacyStakeState.DeriveAddress(agentAddr), goldCurrency); } - public static bool TryGetStakeStateV2( + public static bool TryGetStakeState( this IWorldState worldState, Address agentAddr, - out StakeStateV2 stakeStateV2) + out StakeState stakeState) { - var stakeStateAddr = StakeStateV2.DeriveAddress(agentAddr); - return StakeStateUtils.TryMigrate( + var stakeStateAddr = StakeState.DeriveAddress(agentAddr); + return StakeStateUtils.TryMigrateV1ToV2( worldState, stakeStateAddr, - out stakeStateV2); + out stakeState); } public static ArenaParticipants GetArenaParticipants( diff --git a/Lib9c/Module/ValidatorDelegation/AbstainHistoryModule.cs b/Lib9c/Module/ValidatorDelegation/AbstainHistoryModule.cs new file mode 100644 index 0000000000..3c51b46529 --- /dev/null +++ b/Lib9c/Module/ValidatorDelegation/AbstainHistoryModule.cs @@ -0,0 +1,37 @@ +using Nekoyume.Extensions; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Module.ValidatorDelegation +{ + public static class AbstainHistoryModule + { + public static AbstainHistory GetAbstainHistory( + this ValidatorRepository repository) + { + try + { + return new AbstainHistory( + repository.World + .GetAccount(Addresses.AbstainHistory) + .GetState(AbstainHistory.Address)); + } + catch + { + return new AbstainHistory(); + } + } + + public static ValidatorRepository SetAbstainHistory( + this ValidatorRepository repository, + AbstainHistory abstainHistory) + { + repository.UpdateWorld( + repository.World + .MutateAccount( + Addresses.AbstainHistory, + account => account.SetState(AbstainHistory.Address, abstainHistory.Bencoded))); + + return repository; + } + } +} diff --git a/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs b/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs new file mode 100644 index 0000000000..13f792cbef --- /dev/null +++ b/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs @@ -0,0 +1,54 @@ +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Lib9c; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Module.ValidatorDelegation +{ + public static class ValidatorDelegateeModule + { + public static bool TryGetValidatorDelegatee( + this ValidatorRepository repository, + Address address, + [NotNullWhen(true)] out ValidatorDelegatee? validatorDelegatee) + { + try + { + validatorDelegatee = repository.GetValidatorDelegatee(address); + return true; + } + catch + { + validatorDelegatee = null; + return false; + } + } + + public static ValidatorDelegatee CreateValidatorDelegatee( + this ValidatorRepository repository, PublicKey publicKey, BigInteger commissionPercentage) + { + var context = repository.ActionContext; + + if (repository.TryGetValidatorDelegatee(publicKey.Address, out _)) + { + throw new InvalidOperationException("The signer already has a validator delegatee."); + } + + var validatorDelegatee = new ValidatorDelegatee( + publicKey.Address, + publicKey, + commissionPercentage, + context.BlockIndex, + new Currency[] { repository.World.GetGoldCurrency(), Currencies.Mead }, + repository); + + repository.SetValidatorDelegatee(validatorDelegatee); + + return validatorDelegatee; + } + } +} diff --git a/Lib9c/Module/ValidatorDelegation/ValidatorDelegatorModule.cs b/Lib9c/Module/ValidatorDelegation/ValidatorDelegatorModule.cs new file mode 100644 index 0000000000..d352e6cf51 --- /dev/null +++ b/Lib9c/Module/ValidatorDelegation/ValidatorDelegatorModule.cs @@ -0,0 +1,27 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; +using Libplanet.Crypto; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Module.ValidatorDelegation +{ + public static class ValidatorDelegatorModule + { + public static bool TryGetValidatorDelegator( + this ValidatorRepository repository, + Address address, + [NotNullWhen(true)] out ValidatorDelegator? validatorDelegator) + { + try + { + validatorDelegator = repository.GetValidatorDelegator(address); + return true; + } + catch + { + validatorDelegator = null; + return false; + } + } + } +} diff --git a/Lib9c/Module/ValidatorDelegation/ValidatorListModule.cs b/Lib9c/Module/ValidatorDelegation/ValidatorListModule.cs new file mode 100644 index 0000000000..62f951a909 --- /dev/null +++ b/Lib9c/Module/ValidatorDelegation/ValidatorListModule.cs @@ -0,0 +1,80 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; +using Bencodex.Types; +using Libplanet.Crypto; +using Nekoyume.Action; +using Nekoyume.Extensions; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Module.ValidatorDelegation +{ + public static class ValidatorListModule + { + public static ProposerInfo GetProposerInfo(this ValidatorRepository repository) + => repository.TryGetProposerInfo(ProposerInfo.Address, out var proposerInfo) + ? proposerInfo + : throw new FailedLoadStateException("There is no such proposer info"); + + public static bool TryGetProposerInfo( + this ValidatorRepository repository, + Address address, + [NotNullWhen(true)] out ProposerInfo? proposerInfo) + { + try + { + var value = repository.World.GetAccountState(Addresses.ValidatorList).GetState(address); + if (!(value is List list)) + { + proposerInfo = null; + return false; + } + + proposerInfo = new ProposerInfo(list); + return true; + } + catch + { + proposerInfo = null; + return false; + } + } + + public static ValidatorRepository SetProposerInfo( + this ValidatorRepository repository, + ProposerInfo proposerInfo) + { + repository.UpdateWorld(repository.World.MutateAccount( + Addresses.ValidatorList, + state => state.SetState(ProposerInfo.Address, proposerInfo.Bencoded))); + return repository; + } + + public static ValidatorList GetValidatorList(this ValidatorRepository repository) + => repository.TryGetValidatorList(out var validatorList) + ? validatorList + : throw new FailedLoadStateException("There is no such validator list"); + + public static bool TryGetValidatorList( + this ValidatorRepository repository, + [NotNullWhen(true)] out ValidatorList? validatorList) + { + try + { + var value = repository.World.GetAccountState(Addresses.ValidatorList).GetState(ValidatorList.Address); + if (!(value is List list)) + { + validatorList = null; + return false; + } + + validatorList = new ValidatorList(list); + return true; + } + catch + { + validatorList = null; + return false; + } + } + } +} diff --git a/Lib9c/Module/ValidatorDelegation/ValidatorUnbondingModule.cs b/Lib9c/Module/ValidatorDelegation/ValidatorUnbondingModule.cs new file mode 100644 index 0000000000..f1a6030c0f --- /dev/null +++ b/Lib9c/Module/ValidatorDelegation/ValidatorUnbondingModule.cs @@ -0,0 +1,41 @@ +using System; +using Nekoyume.Delegation; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Module.ValidatorDelegation +{ + public static class ValidatorUnbondingModule + { + public static ValidatorRepository ReleaseUnbondings( + this ValidatorRepository repository) + { + var context = repository.ActionContext; + var unbondingSet = repository.GetUnbondingSet(); + var unbondings = unbondingSet.UnbondingsToRelease(context.BlockIndex); + + IUnbonding released; + foreach (var unbonding in unbondings) + { + released = unbonding.Release(context.BlockIndex); + + switch (released) + { + case UnbondLockIn unbondLockIn: + repository.SetUnbondLockIn(unbondLockIn); + break; + case RebondGrace rebondGrace: + repository.SetRebondGrace(rebondGrace); + break; + default: + throw new ArgumentException("Invalid unbonding type."); + } + + unbondingSet = unbondingSet.SetUnbonding(released); + } + + repository.SetUnbondingSet(unbondingSet); + + return repository; + } + } +} diff --git a/Lib9c/PolicyAction/Tx/Begin/AutoJoinGuild.cs b/Lib9c/PolicyAction/Tx/Begin/AutoJoinGuild.cs deleted file mode 100644 index b1d7f4da52..0000000000 --- a/Lib9c/PolicyAction/Tx/Begin/AutoJoinGuild.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using Bencodex.Types; -using Libplanet.Action; -using Libplanet.Action.State; -using Nekoyume.Action; -using Nekoyume.Action.Guild.Migration; -using Nekoyume.Action.Guild.Migration.Controls; -using Nekoyume.Extensions; -using Serilog; - -namespace Nekoyume.PolicyAction.Tx.Begin -{ - /// - /// An action that automatically joins to Planetarium guild if it contracted pledge with Planetarium. - /// - public class AutoJoinGuild : ActionBase - { - public override IValue PlainValue => Null.Value; - - public override void LoadPlainValue(IValue plainValue) - { - throw new InvalidOperationException("Policy action shouldn't be serialized."); - } - - public override IWorld Execute(IActionContext context) - { - if (!context.IsPolicyAction) - { - throw new InvalidOperationException( - "This action must be called when it is a policy action."); - } - - var world = context.PreviousState; - var signer = context.GetAgentAddress(); - - try - { - return GuildMigrationCtrl.MigratePlanetariumPledgeToGuild(world, signer); - } - catch (GuildMigrationFailedException guildMigrationFailedException) - { - Log.ForContext() - .Debug( - "Migration from pledge to guild failed but it just skips. {Message}", - guildMigrationFailedException.Message); - } - catch (Exception e) - { - Log.ForContext() - .Error( - "Unexpected exception but it skips. You should debug this situation. {Message}", - e.Message); - } - - return world; - } - } -} diff --git a/Lib9c/ValidatorDelegation/AbstainHistory.cs b/Lib9c/ValidatorDelegation/AbstainHistory.cs new file mode 100644 index 0000000000..c90a987361 --- /dev/null +++ b/Lib9c/ValidatorDelegation/AbstainHistory.cs @@ -0,0 +1,93 @@ +#nullable enable +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Bencodex.Types; +using Libplanet.Crypto; + +namespace Nekoyume.ValidatorDelegation +{ + public sealed class AbstainHistory + { + private static readonly Comparer Comparer + = Comparer.Create((x, y) => x.Address.CompareTo(y.Address)); + + public AbstainHistory() + { + History = new SortedDictionary>(Comparer); + } + + public AbstainHistory(IValue bencoded) + : this((List)bencoded) + { + } + + public AbstainHistory(List bencoded) + { + History = new SortedDictionary>(Comparer); + foreach (var item in bencoded) + { + var list = (List)item; + var publicKey = new PublicKey(((Binary)list[0]).ToArray()); + var history = new List(); + foreach (var height in (List)list[1]) + { + history.Add((Integer)height); + } + History.Add(publicKey, history); + } + } + + public SortedDictionary> History { get; private set; } + + public static int WindowSize => 10; + + public static int MaxAbstainAllowance => 3; + + public static Address Address => new Address( + ImmutableArray.Create( + 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41)); + + public IValue Bencoded + => new List( + History.Select(pair => + List.Empty + .Add(pair.Key.Format(true)) + .Add(new List(pair.Value)))); + + + public List FindToSlashAndAdd(IEnumerable abstainList, long height) + { + var lowerBound = height - WindowSize; + var toSlashList = new List(); + foreach (var abstain in abstainList) + { + if (History.TryGetValue(abstain, out var history)) + { + history.Add(height); + if (history.Count(abstainHeight => abstainHeight > lowerBound) > MaxAbstainAllowance) + { + toSlashList.Add(abstain); + History.Remove(abstain); + } + } + else + { + History.Add(abstain, new List() { height }); + } + } + + foreach (var history in History) + { + history.Value.RemoveAll(abstainHeight => abstainHeight < lowerBound); + if (history.Value.Count == 0) + { + History.Remove(history.Key); + } + } + + return toSlashList; + } + } +} diff --git a/Lib9c/ValidatorDelegation/ProposerInfo.cs b/Lib9c/ValidatorDelegation/ProposerInfo.cs new file mode 100644 index 0000000000..0f791c6312 --- /dev/null +++ b/Lib9c/ValidatorDelegation/ProposerInfo.cs @@ -0,0 +1,41 @@ +#nullable enable +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Crypto; +using Nekoyume.Model.State; + +namespace Nekoyume.ValidatorDelegation +{ + public class ProposerInfo + { + public ProposerInfo(long blockIndex, Address proposer) + { + BlockIndex = blockIndex; + Proposer = proposer; + } + + public ProposerInfo(IValue bencoded) + : this((List)bencoded) + { + } + + public ProposerInfo(List bencoded) + : this((Integer)bencoded[0], new Address(bencoded[1])) + { + } + + public static Address Address => new Address( + ImmutableArray.Create( + 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50)); + + public long BlockIndex { get; } + + public Address Proposer { get; } + + public IValue Bencoded + => List.Empty + .Add(BlockIndex) + .Add(Proposer.Serialize()); + } +} diff --git a/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs b/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs new file mode 100644 index 0000000000..c3fa0d537b --- /dev/null +++ b/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs @@ -0,0 +1,294 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Numerics; +using Bencodex; +using Bencodex.Types; +using Lib9c; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Consensus; +using Nekoyume.Delegation; +using Nekoyume.Model.State; + +namespace Nekoyume.ValidatorDelegation +{ + public sealed class ValidatorDelegatee + : Delegatee, IEquatable, IBencodable + { + public ValidatorDelegatee( + Address address, + PublicKey publicKey, + BigInteger commissionPercentage, + long creationHeight, + IEnumerable rewardCurrencies, + ValidatorRepository repository) + : base( + address: address, + accountAddress: repository.DelegateeAccountAddress, + delegationCurrency: ValidatorDelegationCurrency, + rewardCurrencies: rewardCurrencies, + delegationPoolAddress: InactiveDelegationPoolAddress, + rewardPoolAddress: DelegationAddress.RewardPoolAddress(address, repository.DelegateeAccountAddress), + rewardRemainderPoolAddress: Addresses.CommunityPool, + slashedPoolAddress: Addresses.CommunityPool, + unbondingPeriod: ValidatorUnbondingPeriod, + maxUnbondLockInEntries: ValidatorMaxUnbondLockInEntries, + maxRebondGraceEntries: ValidatorMaxRebondGraceEntries, + repository: repository) + { + if (!address.Equals(publicKey.Address)) + { + throw new ArgumentException("The address and the public key do not match."); + } + + if (commissionPercentage < MinCommissionPercentage || commissionPercentage > MaxCommissionPercentage) + { + throw new ArgumentOutOfRangeException( + nameof(commissionPercentage), + $"The commission percentage must be between {MinCommissionPercentage} and {MaxCommissionPercentage}."); + } + + PublicKey = publicKey; + IsActive = false; + CommissionPercentage = commissionPercentage; + CommissionPercentageLastUpdateHeight = creationHeight; + DelegationChanged += OnDelegationChanged; + Enjailed += OnEnjailed; + Unjailed += OnUnjailed; + } + + public ValidatorDelegatee( + Address address, + IValue bencoded, + ValidatorRepository repository) + : this( + address: address, + bencoded: (List)bencoded, + repository: repository) + { + } + + public ValidatorDelegatee( + Address address, + List bencoded, + ValidatorRepository repository) + : base( + address: address, + repository: repository) + { + PublicKey = new PublicKey(((Binary)bencoded[0]).ByteArray); + IsActive = (Bencodex.Types.Boolean)bencoded[1]; + CommissionPercentage = (Integer)bencoded[2]; + CommissionPercentageLastUpdateHeight = (Integer)bencoded[3]; + DelegationChanged += OnDelegationChanged; + Enjailed += OnEnjailed; + Unjailed += OnUnjailed; + } + + public static Currency ValidatorDelegationCurrency => Currencies.GuildGold; + + // TODO: [MigrateGuild] Change unbonding period after migration. + public static long ValidatorUnbondingPeriod => LegacyStakeState.LockupInterval; + + public static int ValidatorMaxUnbondLockInEntries => 2; + + public static int ValidatorMaxRebondGraceEntries => 2; + + public static BigInteger BaseProposerRewardPercentage => 1; + + public static BigInteger BonusProposerRewardPercentage => 4; + + public static BigInteger DefaultCommissionPercentage => 10; + + public static BigInteger MinCommissionPercentage => 0; + + public static BigInteger MaxCommissionPercentage => 20; + + public static long CommissionPercentageUpdateCooldown => 100; + + public static BigInteger CommissionPercentageMaxChange => 1; + + public BigInteger CommissionPercentage { get; private set; } + + public long CommissionPercentageLastUpdateHeight { get; private set; } + + public List Bencoded => List.Empty + .Add(PublicKey.Format(true)) + .Add(IsActive) + .Add(CommissionPercentage) + .Add(CommissionPercentageLastUpdateHeight); + + IValue IBencodable.Bencoded => Bencoded; + + public PublicKey PublicKey { get; } + + public bool IsActive { get; private set; } + + public BigInteger Power => TotalDelegated.RawValue; + + public Validator Validator => new(PublicKey, Power); + + public FungibleAssetValue MinSelfDelegation => DelegationCurrency * 10; + + public void AllocateReward( + FungibleAssetValue rewardToAllocate, + BigInteger validatorPower, + BigInteger validatorSetPower, + Address RewardSource, + long height) + { + ValidatorRepository repository = (ValidatorRepository)Repository; + + FungibleAssetValue rewardAllocated + = (rewardToAllocate * validatorPower).DivRem(validatorSetPower).Quotient; + FungibleAssetValue commission + = (rewardAllocated * CommissionPercentage).DivRem(100).Quotient; + FungibleAssetValue delegationRewards = rewardAllocated - commission; + + if (commission.Sign > 0) + { + repository.TransferAsset(RewardSource, Address, commission); + } + + if (delegationRewards.Sign > 0) + { + repository.TransferAsset(RewardSource, RewardPoolAddress, delegationRewards); + } + + CollectRewards(height); + } + + public void SetCommissionPercentage(BigInteger percentage, long height) + { + if (height - CommissionPercentageLastUpdateHeight < CommissionPercentageUpdateCooldown) + { + throw new InvalidOperationException( + $"The commission percentage can be updated only once in {CommissionPercentageUpdateCooldown} blocks."); + } + + if (percentage < MinCommissionPercentage || percentage > MaxCommissionPercentage) + { + throw new ArgumentOutOfRangeException( + nameof(percentage), + $"The commission percentage must be between {MinCommissionPercentage} and {MaxCommissionPercentage}."); + } + + if (BigInteger.Abs(CommissionPercentage - percentage) > CommissionPercentageMaxChange) + { + throw new ArgumentOutOfRangeException( + nameof(percentage), + $"The commission percentage can be changed by at most {CommissionPercentageMaxChange}."); + } + + CommissionPercentage = percentage; + CommissionPercentageLastUpdateHeight = height; + } + public new void Unjail(long height) + { + ValidatorRepository repository = (ValidatorRepository)Repository; + var selfDelegation = FAVFromShare(repository.GetBond(this, Address).Share); + if (MinSelfDelegation > selfDelegation) + { + throw new InvalidOperationException("The self-delegation is still below the minimum."); + } + + base.Unjail(height); + } + + public void Activate() + { + ValidatorRepository repository = (ValidatorRepository)Repository; + IsActive = true; + Metadata.DelegationPoolAddress = ActiveDelegationPoolAddress; + + if (TotalDelegated.Sign > 0) + { + repository.TransferAsset( + InactiveDelegationPoolAddress, + ActiveDelegationPoolAddress, + TotalDelegated); + } + } + + public void Deactivate() + { + ValidatorRepository repository = (ValidatorRepository)Repository; + IsActive = false; + Metadata.DelegationPoolAddress = InactiveDelegationPoolAddress; + + if (TotalDelegated.Sign > 0) + { + repository.TransferAsset( + ActiveDelegationPoolAddress, + InactiveDelegationPoolAddress, + TotalDelegated); + } + } + + public void OnDelegationChanged(object? sender, long height) + { + ValidatorRepository repository = (ValidatorRepository)Repository; + + if (Jailed) + { + return; + } + + if (Validator.Power.IsZero) + { + repository.SetValidatorList(repository.GetValidatorList().RemoveValidator(Validator.PublicKey)); + } + else + { + repository.SetValidatorList(repository.GetValidatorList().SetValidator(Validator)); + } + + var selfDelegation = FAVFromShare(repository.GetBond(this, Address).Share); + if (MinSelfDelegation > selfDelegation && !Jailed) + { + Jail(height); + } + } + + public void OnEnjailed(object? sender, EventArgs e) + { + ValidatorRepository repository = (ValidatorRepository)Repository; + repository.SetValidatorList(repository.GetValidatorList().RemoveValidator(Validator.PublicKey)); + } + + public void OnUnjailed(object? sender, EventArgs e) + { + ValidatorRepository repository = (ValidatorRepository)Repository; + repository.SetValidatorList(repository.GetValidatorList().SetValidator(Validator)); + } + + public bool Equals(ValidatorDelegatee? other) + => other is ValidatorDelegatee validatorDelegatee + && Metadata.Equals(validatorDelegatee.Metadata) + && PublicKey.Equals(validatorDelegatee.PublicKey) + && IsActive == validatorDelegatee.IsActive + && CommissionPercentage == validatorDelegatee.CommissionPercentage + && CommissionPercentageLastUpdateHeight == validatorDelegatee.CommissionPercentageLastUpdateHeight; + + public bool Equals(IDelegatee? other) + => Equals(other as ValidatorDelegatee); + + public override bool Equals(object? obj) + => Equals(obj as ValidatorDelegatee); + + public override int GetHashCode() + => HashCode.Combine(Address, AccountAddress); + + public static Address ActiveDelegationPoolAddress => new Address( + ImmutableArray.Create( + 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41)); + + public static Address InactiveDelegationPoolAddress => new Address( + ImmutableArray.Create( + 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49)); + } +} diff --git a/Lib9c/ValidatorDelegation/ValidatorDelegator.cs b/Lib9c/ValidatorDelegation/ValidatorDelegator.cs new file mode 100644 index 0000000000..206df3bf6d --- /dev/null +++ b/Lib9c/ValidatorDelegation/ValidatorDelegator.cs @@ -0,0 +1,36 @@ +#nullable enable +using System; +using Libplanet.Crypto; +using Nekoyume.Delegation; + +namespace Nekoyume.ValidatorDelegation +{ + public sealed class ValidatorDelegator + : Delegator, IEquatable + { + public ValidatorDelegator( + Address address, + Address delegationPoolAddress, + ValidatorRepository repository) + : base( + address: address, + accountAddress: repository.DelegatorAccountAddress, + delegationPoolAddress: delegationPoolAddress, + rewardAddress: address, + repository: repository) + { + } + + public ValidatorDelegator( + Address address, + ValidatorRepository repository) + : base( + address: address, + repository: repository) + { + } + + public bool Equals(ValidatorDelegator? other) + => Metadata.Equals(other?.Metadata); + } +} diff --git a/Lib9c/ValidatorDelegation/ValidatorList.cs b/Lib9c/ValidatorDelegation/ValidatorList.cs new file mode 100644 index 0000000000..b0ca670d3b --- /dev/null +++ b/Lib9c/ValidatorDelegation/ValidatorList.cs @@ -0,0 +1,99 @@ +#nullable enable +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Bencodex; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Consensus; + +namespace Nekoyume.ValidatorDelegation +{ + public sealed class ValidatorList : IBencodable + { + private static readonly IComparer _reversedComparer + = Comparer.Create((y, x) => new ValidatorComparer().Compare(x, y)); + + public ValidatorList() + : this(ImmutableList.Empty) + { + } + + public ValidatorList(IValue bencoded) + : this((List)bencoded) + { + } + + public ValidatorList(List bencoded) + : this(bencoded.Select(v => new Validator(v)).ToImmutableList()) + { + } + + + private ValidatorList(ImmutableList validators) + { + Validators = validators; + } + + public static Address Address => new Address( + ImmutableArray.Create( + 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56)); + + public ImmutableList Validators { get; } + + public static int MaxActiveSetSize => 100; + + public List Bencoded => new List(Validators.Select(v => v.Bencoded)); + + IValue IBencodable.Bencoded => Bencoded; + + public List ActiveSet() => Validators.Take(MaxActiveSetSize).ToList(); + + public List InActiveSet() => Validators.Skip(MaxActiveSetSize).ToList(); + + public ValidatorList SetValidator(Validator validator) + => RemoveValidator(validator.PublicKey).AddValidator(validator); + + public ValidatorList RemoveValidator(PublicKey publicKey) + => UpdateValidators(Validators.RemoveAll(v => v.PublicKey.Equals(publicKey))); + + private ValidatorList AddValidator(Validator validator) + { + int index = Validators.BinarySearch(validator, _reversedComparer); + return UpdateValidators(Validators.Insert(index < 0 ? ~index : index, validator)); + } + + private ValidatorList UpdateValidators(ImmutableList validators) + => new ValidatorList(validators); + } + + public class ValidatorComparer : IComparer + { + public int Compare(Validator? x, Validator? y) + { + if (ReferenceEquals(x, y)) + { + return 0; + } + + if (x is null) + { + return -1; + } + + if (y is null) + { + return 1; + } + + int comparison = x.Power.CompareTo(y.Power); + if (comparison != 0) + { + return comparison; + } + + return x.OperatorAddress.CompareTo(y.OperatorAddress); + } + } +} diff --git a/Lib9c/ValidatorDelegation/ValidatorRepository.cs b/Lib9c/ValidatorDelegation/ValidatorRepository.cs new file mode 100644 index 0000000000..69e7563c38 --- /dev/null +++ b/Lib9c/ValidatorDelegation/ValidatorRepository.cs @@ -0,0 +1,117 @@ +#nullable enable +using System.Numerics; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Action; +using Nekoyume.Delegation; + +namespace Nekoyume.ValidatorDelegation +{ + public sealed class ValidatorRepository : DelegationRepository + { + private readonly Address validatorListAddress = Addresses.ValidatorList; + + private IAccount _validatorListAccount; + + public ValidatorRepository(IDelegationRepository repository) + : this(repository.World, repository.ActionContext) + { + } + + public ValidatorRepository(IWorld world, IActionContext actionContext) + : base( + world, + actionContext, + Addresses.ValidatorDelegatee, + Addresses.ValidatorDelegator, + Addresses.ValidatorDelegateeMetadata, + Addresses.ValidatorDelegatorMetadata, + Addresses.ValidatorBond, + Addresses.ValidatorUnbondLockIn, + Addresses.ValidatorRebondGrace, + Addresses.ValidatorUnbondingSet, + Addresses.ValidatorLumpSumRewardsRecord) + { + _validatorListAccount = world.GetAccount(validatorListAddress); + } + + public override IWorld World => base.World + .SetAccount(validatorListAddress, _validatorListAccount); + + public ValidatorDelegatee GetValidatorDelegatee(Address address) + => delegateeAccount.GetState(address) is IValue bencoded + ? new ValidatorDelegatee( + address, + bencoded, + this) + : throw new FailedLoadStateException("Delegatee does not exist."); + + public override IDelegatee GetDelegatee(Address address) + => GetValidatorDelegatee(address); + + public ValidatorDelegator GetValidatorDelegator(Address address) + { + try + { + return new ValidatorDelegator(address, this); + } + catch (FailedLoadStateException) + { + return new ValidatorDelegator( + address, + address, + this); + } + } + + public override IDelegator GetDelegator(Address address) + => new ValidatorDelegator(address, this); + + public ValidatorList GetValidatorList() + { + IValue? value = _validatorListAccount.GetState(ValidatorList.Address); + return value is IValue bencoded + ? new ValidatorList(bencoded) + : new ValidatorList(); + } + + public void SetValidatorDelegatee(ValidatorDelegatee validatorDelegatee) + { + delegateeAccount = delegateeAccount.SetState( + validatorDelegatee.Address, validatorDelegatee.Bencoded); + SetDelegateeMetadata(validatorDelegatee.Metadata); + } + + public override void SetDelegatee(IDelegatee delegatee) + => SetValidatorDelegatee((ValidatorDelegatee)delegatee); + + public void SetValidatorDelegator(ValidatorDelegator validatorDelegator) + { + SetDelegatorMetadata(validatorDelegator.Metadata); + } + + public override void SetDelegator(IDelegator delegator) + => SetValidatorDelegator((ValidatorDelegator)delegator); + + public void SetValidatorList(ValidatorList validatorList) + { + _validatorListAccount = _validatorListAccount.SetState( + ValidatorList.Address, validatorList.Bencoded); + } + + public void SetCommissionPercentage(Address address, BigInteger commissionPercentage, long height) + { + ValidatorDelegatee validatorDelegatee = GetValidatorDelegatee(address); + validatorDelegatee.SetCommissionPercentage(commissionPercentage, height); + SetValidatorDelegatee(validatorDelegatee); + } + + public override void UpdateWorld(IWorld world) + { + base.UpdateWorld(world); + _validatorListAccount = world.GetAccount(validatorListAddress); + } + } +}