diff --git a/.Lib9c.Tests/Action/ClaimStakeReward3Test.cs b/.Lib9c.Tests/Action/ClaimStakeReward3Test.cs index 1fb8cc626a..95d9c6a329 100644 --- a/.Lib9c.Tests/Action/ClaimStakeReward3Test.cs +++ b/.Lib9c.Tests/Action/ClaimStakeReward3Test.cs @@ -42,7 +42,7 @@ public ClaimStakeReward3Test(ITestOutputHelper outputHelper) { { nameof(StakeRegularRewardSheet), - ClaimStakeReward.V2.StakeRegularRewardSheetCsv + ClaimStakeReward6.V2.StakeRegularRewardSheetCsv }, }); _ncg = _initialStatesWithAvatarStateV1.GetGoldCurrency(); diff --git a/.Lib9c.Tests/Action/ClaimStakeReward4Test.cs b/.Lib9c.Tests/Action/ClaimStakeReward4Test.cs index a0134072e1..7fe3a598bf 100644 --- a/.Lib9c.Tests/Action/ClaimStakeReward4Test.cs +++ b/.Lib9c.Tests/Action/ClaimStakeReward4Test.cs @@ -42,7 +42,7 @@ public ClaimStakeReward4Test(ITestOutputHelper outputHelper) { { nameof(StakeRegularRewardSheet), - ClaimStakeReward.V2.StakeRegularRewardSheetCsv + ClaimStakeReward6.V2.StakeRegularRewardSheetCsv }, }); _ncg = _initialStatesWithAvatarStateV1.GetGoldCurrency(); diff --git a/.Lib9c.Tests/Action/ClaimStakeReward5Test.cs b/.Lib9c.Tests/Action/ClaimStakeReward5Test.cs index dcab60fdb0..d3020df66b 100644 --- a/.Lib9c.Tests/Action/ClaimStakeReward5Test.cs +++ b/.Lib9c.Tests/Action/ClaimStakeReward5Test.cs @@ -42,7 +42,7 @@ public ClaimStakeReward5Test(ITestOutputHelper outputHelper) { { nameof(StakeRegularRewardSheet), - ClaimStakeReward.V2.StakeRegularRewardSheetCsv + ClaimStakeReward6.V2.StakeRegularRewardSheetCsv }, }); _ncg = _initialStatesWithAvatarStateV1.GetGoldCurrency(); diff --git a/.Lib9c.Tests/Action/ClaimStakeReward6Test.cs b/.Lib9c.Tests/Action/ClaimStakeReward6Test.cs new file mode 100644 index 0000000000..fd1776a638 --- /dev/null +++ b/.Lib9c.Tests/Action/ClaimStakeReward6Test.cs @@ -0,0 +1,319 @@ +#nullable enable + +namespace Lib9c.Tests.Action +{ + using System.Linq; + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model.State; + using Serilog; + using Xunit; + using Xunit.Abstractions; + + public class ClaimStakeReward6Test + { + private const string AgentAddressHex = "0x0000000001000000000100000000010000000001"; + private readonly Address _agentAddr = new Address(AgentAddressHex); + private readonly Address _avatarAddr; + private readonly IAccountStateDelta _initialStatesWithAvatarStateV1; + private readonly IAccountStateDelta _initialStatesWithAvatarStateV2; + private readonly Currency _ncg; + + public ClaimStakeReward6Test(ITestOutputHelper outputHelper) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(outputHelper) + .CreateLogger(); + ( + _, + _, + _avatarAddr, + _initialStatesWithAvatarStateV1, + _initialStatesWithAvatarStateV2) = InitializeUtil.InitializeStates( + agentAddr: _agentAddr); + _ncg = _initialStatesWithAvatarStateV1.GetGoldCurrency(); + } + + [Fact] + public void Serialization() + { + var action = new ClaimStakeReward6(_avatarAddr); + var deserialized = new ClaimStakeReward6(); + deserialized.LoadPlainValue(action.PlainValue); + Assert.Equal(action.AvatarAddress, deserialized.AvatarAddress); + } + + [Theory] + [InlineData( + ClaimStakeReward2.ObsoletedIndex, + 100L, + null, + ClaimStakeReward2.ObsoletedIndex + StakeState.LockupInterval, + 40, + 4, + 0, + null, + null, + 0L + )] + [InlineData( + ClaimStakeReward2.ObsoletedIndex, + 6000L, + null, + ClaimStakeReward2.ObsoletedIndex + StakeState.LockupInterval, + 4800, + 36, + 4, + null, + null, + 0L + )] + // Calculate rune start from hard fork index + [InlineData( + 0L, + 6000L, + 0L, + ClaimStakeReward2.ObsoletedIndex + StakeState.LockupInterval, + 136800, + 1026, + 3, + null, + null, + 0L + )] + // Stake reward v2 + // Stake before v2, prev. receive v1, receive v1 & v2 + [InlineData( + StakeState.StakeRewardSheetV2Index - StakeState.RewardInterval * 2, + 50L, + StakeState.StakeRewardSheetV2Index - StakeState.RewardInterval, + StakeState.StakeRewardSheetV2Index + 1, + 5, + 1, + 0, + null, + null, + 0L + )] + // Stake before v2, prev. receive v2, receive v2 + [InlineData( + StakeState.StakeRewardSheetV2Index - StakeState.RewardInterval, + 50L, + StakeState.StakeRewardSheetV2Index, + StakeState.StakeRewardSheetV2Index + StakeState.RewardInterval, + 5, + 1, + 0, + null, + null, + 0L + )] + // Stake after v2, no prev. receive, receive v2 + [InlineData( + StakeState.StakeRewardSheetV2Index, + 6000L, + null, + StakeState.StakeRewardSheetV2Index + StakeState.RewardInterval, + 1200, + 9, + 1, + null, + null, + 0L + )] + // stake after v2, prev. receive v2, receive v2 + [InlineData( + StakeState.StakeRewardSheetV2Index, + 50L, + StakeState.StakeRewardSheetV2Index + StakeState.RewardInterval, + StakeState.StakeRewardSheetV2Index + StakeState.RewardInterval * 2, + 5, + 1, + 0, + null, + null, + 0L + )] + // stake before currency as reward, non prev. + [InlineData( + StakeState.CurrencyAsRewardStartIndex - StakeState.RewardInterval * 2, + 10_000_000L, + null, + StakeState.CurrencyAsRewardStartIndex + StakeState.RewardInterval, + 3_000_000, + 37_506, + 4_998, + AgentAddressHex, + "GARAGE", + 100_000L + )] + // stake before currency as reward, prev. + [InlineData( + StakeState.CurrencyAsRewardStartIndex - StakeState.RewardInterval * 2, + 10_000_000L, + StakeState.CurrencyAsRewardStartIndex - StakeState.RewardInterval, + StakeState.CurrencyAsRewardStartIndex + StakeState.RewardInterval, + 2_000_000, + 25_004, + 3_332, + AgentAddressHex, + "GARAGE", + 100_000L + )] + // test tx(c46cf83c46bc106372015a5020d6b9f15dc733819a6dc3fde37d9b5625fc3d93) + [InlineData( + 7_009_561L, + 10_000_000L, + 7_110_390L, + 7_160_778L, + 1_000_000, + 12_502, + 1_666, + null, + null, + 0)] + // test tx(3baf5904b8499975a27d3873e58953ef8d0aa740318e99b2fe6a85428c9eb7aa) + [InlineData( + 5_350_456L, + 500_000L, + 7_576_016L, + 7_625_216L, + 100_000, + 627, + 83, + null, + null, + 0)] + public void Execute_Success( + long startedBlockIndex, + long stakeAmount, + long? previousRewardReceiveIndex, + long blockIndex, + int expectedHourglass, + int expectedApStone, + int expectedRune, + string expectedCurrencyAddrHex, + string expectedCurrencyTicker, + long expectedCurrencyAmount) + { + Execute( + _initialStatesWithAvatarStateV1, + _agentAddr, + _avatarAddr, + startedBlockIndex, + stakeAmount, + previousRewardReceiveIndex, + blockIndex, + expectedHourglass, + expectedApStone, + expectedRune, + expectedCurrencyAddrHex, + expectedCurrencyTicker, + expectedCurrencyAmount); + + Execute( + _initialStatesWithAvatarStateV2, + _agentAddr, + _avatarAddr, + startedBlockIndex, + stakeAmount, + previousRewardReceiveIndex, + blockIndex, + expectedHourglass, + expectedApStone, + expectedRune, + expectedCurrencyAddrHex, + expectedCurrencyTicker, + expectedCurrencyAmount); + } + + private void Execute( + IAccountStateDelta prevState, + Address agentAddr, + Address avatarAddr, + long startedBlockIndex, + long stakeAmount, + long? previousRewardReceiveIndex, + long blockIndex, + int expectedHourglass, + int expectedApStone, + int expectedRune, + string expectedCurrencyAddrHex, + string expectedCurrencyTicker, + long expectedCurrencyAmount) + { + var context = new ActionContext(); + var stakeStateAddr = StakeState.DeriveAddress(agentAddr); + var initialStakeState = new StakeState(stakeStateAddr, startedBlockIndex); + if (!(previousRewardReceiveIndex is null)) + { + initialStakeState.Claim((long)previousRewardReceiveIndex); + } + + prevState = prevState + .SetState(stakeStateAddr, initialStakeState.Serialize()) + .MintAsset(context, stakeStateAddr, _ncg * stakeAmount); + + var action = new ClaimStakeReward6(avatarAddr); + var states = action.Execute(new ActionContext + { + PreviousState = prevState, + Signer = agentAddr, + BlockIndex = blockIndex, + }); + + var avatarState = states.GetAvatarStateV2(avatarAddr); + if (expectedHourglass > 0) + { + Assert.Equal( + expectedHourglass, + avatarState.inventory.Items.First(x => x.item.Id == 400000).count); + } + else + { + Assert.DoesNotContain(avatarState.inventory.Items, x => x.item.Id == 400000); + } + + if (expectedApStone > 0) + { + Assert.Equal( + expectedApStone, + avatarState.inventory.Items.First(x => x.item.Id == 500000).count); + } + else + { + Assert.DoesNotContain(avatarState.inventory.Items, x => x.item.Id == 500000); + } + + if (expectedRune > 0) + { + Assert.Equal( + expectedRune * RuneHelper.StakeRune, + states.GetBalance(avatarAddr, RuneHelper.StakeRune)); + } + else + { + Assert.Equal( + 0 * RuneHelper.StakeRune, + states.GetBalance(avatarAddr, RuneHelper.StakeRune)); + } + + if (!string.IsNullOrEmpty(expectedCurrencyAddrHex)) + { + var addr = new Address(expectedCurrencyAddrHex); + var currency = Currencies.GetMinterlessCurrency(expectedCurrencyTicker); + Assert.Equal( + expectedCurrencyAmount * currency, + states.GetBalance(addr, currency)); + } + + Assert.True(states.TryGetStakeState(agentAddr, out StakeState stakeState)); + Assert.Equal(blockIndex, stakeState.ReceivedBlockIndex); + } + } +} diff --git a/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs b/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs index 0caea6b532..acab85150d 100644 --- a/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs +++ b/.Lib9c.Tests/Action/ClaimStakeRewardTest.cs @@ -119,8 +119,8 @@ public void Serialization() 6000L, null, StakeState.StakeRewardSheetV2Index + StakeState.RewardInterval, - 1200, - 9, + 3000, + 17, 1, null, null, @@ -145,8 +145,8 @@ public void Serialization() 10_000_000L, null, StakeState.CurrencyAsRewardStartIndex + StakeState.RewardInterval, - 3_000_000, - 37_506, + 15_000_000, + 75_006, 4_998, AgentAddressHex, "GARAGE", @@ -158,25 +158,13 @@ public void Serialization() 10_000_000L, StakeState.CurrencyAsRewardStartIndex - StakeState.RewardInterval, StakeState.CurrencyAsRewardStartIndex + StakeState.RewardInterval, - 2_000_000, - 25_004, + 10_000_000, + 50_004, 3_332, AgentAddressHex, "GARAGE", 100_000L )] - // test tx(c46cf83c46bc106372015a5020d6b9f15dc733819a6dc3fde37d9b5625fc3d93) - [InlineData( - 7_009_561L, - 10_000_000L, - 7_110_390L, - 7_160_778L, - 1_000_000, - 12_502, - 1_666, - null, - null, - 0)] public void Execute_Success( long startedBlockIndex, long stakeAmount, diff --git a/.Lib9c.Tests/Action/Factory/ClaimStakeRewardFactoryTest.cs b/.Lib9c.Tests/Action/Factory/ClaimStakeRewardFactoryTest.cs index 326d99c636..590c71081f 100644 --- a/.Lib9c.Tests/Action/Factory/ClaimStakeRewardFactoryTest.cs +++ b/.Lib9c.Tests/Action/Factory/ClaimStakeRewardFactoryTest.cs @@ -48,7 +48,9 @@ public static IEnumerable GetAllClaimStakeRewardV1() [InlineData(ClaimStakeReward4.ObsoleteBlockIndex, typeof(ClaimStakeReward4))] [InlineData(ClaimStakeReward4.ObsoleteBlockIndex + 1, typeof(ClaimStakeReward5))] [InlineData(ClaimStakeReward5.ObsoleteBlockIndex, typeof(ClaimStakeReward5))] - [InlineData(ClaimStakeReward5.ObsoleteBlockIndex + 1, typeof(ClaimStakeReward))] + [InlineData(ClaimStakeReward5.ObsoleteBlockIndex + 1, typeof(ClaimStakeReward6))] + [InlineData(ClaimStakeReward6.ObsoleteBlockIndex, typeof(ClaimStakeReward6))] + [InlineData(ClaimStakeReward6.ObsoleteBlockIndex + 1, typeof(ClaimStakeReward))] [InlineData(long.MaxValue, typeof(ClaimStakeReward))] public void Create_ByBlockIndex_Success( long blockIndex, diff --git a/Lib9c/Action/ClaimStakeReward.cs b/Lib9c/Action/ClaimStakeReward.cs index cd642cc575..2842b21e35 100644 --- a/Lib9c/Action/ClaimStakeReward.cs +++ b/Lib9c/Action/ClaimStakeReward.cs @@ -19,12 +19,12 @@ namespace Nekoyume.Action { /// - /// Hard forked at https://github.com/planetarium/lib9c/pull/2071 + /// Hard forked at https://github.com/planetarium/lib9c/pull/2083 /// [ActionType(ActionTypeText)] public class ClaimStakeReward : GameAction, IClaimStakeReward, IClaimStakeRewardV1 { - private const string ActionTypeText = "claim_stake_reward6"; + private const string ActionTypeText = "claim_stake_reward7"; /// /// This is the version 1 of the stake reward sheet. @@ -107,24 +107,24 @@ public static class V2 1,50,400000,10,Item, 1,50,500000,800,Item, 1,50,20001,6000,Rune, -2,500,400000,8,Item, -2,500,500000,800,Item, +2,500,400000,4,Item, +2,500,500000,600,Item, 2,500,20001,6000,Rune, -3,5000,400000,5,Item, -3,5000,500000,800,Item, +3,5000,400000,2,Item, +3,5000,500000,400,Item, 3,5000,20001,6000,Rune, -4,50000,400000,5,Item, -4,50000,500000,800,Item, +4,50000,400000,2,Item, +4,50000,500000,400,Item, 4,50000,20001,6000,Rune, -5,500000,400000,5,Item, -5,500000,500000,800,Item, +5,500000,400000,2,Item, +5,500000,500000,400,Item, 5,500000,20001,6000,Rune, -6,5000000,400000,10,Item, -6,5000000,500000,800,Item, +6,5000000,400000,2,Item, +6,5000000,500000,400,Item, 6,5000000,20001,6000,Rune, -6,5000000,800201,100,Item, -7,10000000,400000,10,Item, -7,10000000,500000,800,Item, +6,5000000,800201,50,Item, +7,10000000,400000,2,Item, +7,10000000,500000,400,Item, 7,10000000,20001,6000,Rune, 7,10000000,600201,50,Item, 7,10000000,800201,50,Item, diff --git a/Lib9c/Action/ClaimStakeReward6.cs b/Lib9c/Action/ClaimStakeReward6.cs new file mode 100644 index 0000000000..f7fca8b7d9 --- /dev/null +++ b/Lib9c/Action/ClaimStakeReward6.cs @@ -0,0 +1,478 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using Bencodex.Types; +using Lib9c; +using Lib9c.Abstractions; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Extensions; +using Nekoyume.Helper; +using Nekoyume.Model.Item; +using Nekoyume.Model.State; +using Nekoyume.TableData; +using static Lib9c.SerializeKeys; + +namespace Nekoyume.Action +{ + /// + /// Hard forked at https://github.com/planetarium/lib9c/pull/2071 + /// + [ActionType(ActionTypeText)] + [ActionObsolete(ObsoleteBlockIndex)] + public class ClaimStakeReward6 : GameAction, IClaimStakeReward, IClaimStakeRewardV1 + { + private const string ActionTypeText = "claim_stake_reward6"; + public const long ObsoleteBlockIndex = ActionObsoleteConfig.V200061ObsoleteIndex; + + /// + /// This is the version 1 of the stake reward sheet. + /// The version 1 is used for calculating the reward for the stake + /// that is accumulated before the table patch. + /// + public static class V1 + { + public const int MaxLevel = 5; + + public const string StakeRegularRewardSheetCsv = + @"level,required_gold,item_id,rate,type,currency_ticker,currency_decimal_places,decimal_rate +1,50,400000,,Item,,,10 +1,50,500000,,Item,,,800 +1,50,20001,,Rune,,,6000 +2,500,400000,,Item,,,8 +2,500,500000,,Item,,,800 +2,500,20001,,Rune,,,6000 +3,5000,400000,,Item,,,5 +3,5000,500000,,Item,,,800 +3,5000,20001,,Rune,,,6000 +4,50000,400000,,Item,,,5 +4,50000,500000,,Item,,,800 +4,50000,20001,,Rune,,,6000 +5,500000,400000,,Item,,,5 +5,500000,500000,,Item,,,800 +5,500000,20001,,Rune,,,6000"; + + public const string StakeRegularFixedRewardSheetCsv = + @"level,required_gold,item_id,count +1,50,500000,1 +2,500,500000,2 +3,5000,500000,2 +4,50000,500000,2 +5,500000,500000,2"; + + private static StakeRegularRewardSheet _stakeRegularRewardSheet; + private static StakeRegularFixedRewardSheet _stakeRegularFixedRewardSheet; + + public static StakeRegularRewardSheet StakeRegularRewardSheet + { + get + { + if (_stakeRegularRewardSheet is null) + { + _stakeRegularRewardSheet = new StakeRegularRewardSheet(); + _stakeRegularRewardSheet.Set(StakeRegularRewardSheetCsv); + } + + return _stakeRegularRewardSheet; + } + } + + public static StakeRegularFixedRewardSheet StakeRegularFixedRewardSheet + { + get + { + if (_stakeRegularFixedRewardSheet is null) + { + _stakeRegularFixedRewardSheet = new StakeRegularFixedRewardSheet(); + _stakeRegularFixedRewardSheet.Set(StakeRegularFixedRewardSheetCsv); + } + + return _stakeRegularFixedRewardSheet; + } + } + } + + /// + /// This is the version 2 of the stake reward sheet. + /// The version 2 is used for calculating the reward for the stake + /// that is accumulated before the table patch. + /// + public static class V2 + { + public const int MaxLevel = 7; + + public const string StakeRegularRewardSheetCsv = + @"level,required_gold,item_id,rate,type,currency_ticker +1,50,400000,10,Item, +1,50,500000,800,Item, +1,50,20001,6000,Rune, +2,500,400000,8,Item, +2,500,500000,800,Item, +2,500,20001,6000,Rune, +3,5000,400000,5,Item, +3,5000,500000,800,Item, +3,5000,20001,6000,Rune, +4,50000,400000,5,Item, +4,50000,500000,800,Item, +4,50000,20001,6000,Rune, +5,500000,400000,5,Item, +5,500000,500000,800,Item, +5,500000,20001,6000,Rune, +6,5000000,400000,10,Item, +6,5000000,500000,800,Item, +6,5000000,20001,6000,Rune, +6,5000000,800201,100,Item, +7,10000000,400000,10,Item, +7,10000000,500000,800,Item, +7,10000000,20001,6000,Rune, +7,10000000,600201,50,Item, +7,10000000,800201,50,Item, +7,10000000,,100,Currency,GARAGE +"; + + public const string StakeRegularFixedRewardSheetCsv = + @"level,required_gold,item_id,count +1,50,500000,1 +2,500,500000,2 +3,5000,500000,2 +4,50000,500000,2 +5,500000,500000,2 +6,5000000,500000,2 +7,10000000,500000,2 +"; + + private static StakeRegularRewardSheet _stakeRegularRewardSheet; + private static StakeRegularFixedRewardSheet _stakeRegularFixedRewardSheet; + + public static StakeRegularRewardSheet StakeRegularRewardSheet + { + get + { + if (_stakeRegularRewardSheet is null) + { + _stakeRegularRewardSheet = new StakeRegularRewardSheet(); + _stakeRegularRewardSheet.Set(StakeRegularRewardSheetCsv); + } + + return _stakeRegularRewardSheet; + } + } + + public static StakeRegularFixedRewardSheet StakeRegularFixedRewardSheet + { + get + { + if (_stakeRegularFixedRewardSheet is null) + { + _stakeRegularFixedRewardSheet = new StakeRegularFixedRewardSheet(); + _stakeRegularFixedRewardSheet.Set(StakeRegularFixedRewardSheetCsv); + } + + return _stakeRegularFixedRewardSheet; + } + } + } + + internal Address AvatarAddress { get; private set; } + + Address IClaimStakeRewardV1.AvatarAddress => AvatarAddress; + + public ClaimStakeReward6(Address avatarAddress) : this() + { + AvatarAddress = avatarAddress; + } + + public ClaimStakeReward6() + { + } + + protected override IImmutableDictionary PlainValueInternal => + ImmutableDictionary.Empty + .Add(AvatarAddressKey, AvatarAddress.Serialize()); + + protected override void LoadPlainValueInternal( + IImmutableDictionary plainValue) + { + AvatarAddress = plainValue[AvatarAddressKey].ToAddress(); + } + + public override IAccountStateDelta Execute(IActionContext context) + { + context.UseGas(1); + if (context.Rehearsal) + { + return context.PreviousState; + } + + var states = context.PreviousState; + var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); + if (!states.TryGetStakeState(context.Signer, out var stakeState)) + { + throw new FailedLoadStateException( + ActionTypeText, + addressesHex, + typeof(StakeState), + StakeState.DeriveAddress(context.Signer)); + } + + if (!stakeState.IsClaimable(context.BlockIndex, out _, out _)) + { + throw new RequiredBlockIndexException( + ActionTypeText, + addressesHex, + context.BlockIndex); + } + + if (!states.TryGetAvatarStateV2( + context.Signer, + AvatarAddress, + out var avatarState, + out var migrationRequired)) + { + throw new FailedLoadStateException( + ActionTypeText, + addressesHex, + typeof(AvatarState), + AvatarAddress); + } + + var sheets = states.GetSheets(sheetTypes: new[] + { + typeof(StakeRegularRewardSheet), + typeof(ConsumableItemSheet), + typeof(CostumeItemSheet), + typeof(EquipmentItemSheet), + typeof(MaterialItemSheet), + }); + + var currency = states.GetGoldCurrency(); + var stakedAmount = states.GetBalance(stakeState.address, currency); + var stakeRegularRewardSheet = sheets.GetSheet(); + var level = + stakeRegularRewardSheet.FindLevelByStakedAmount(context.Signer, stakedAmount); + var itemSheet = sheets.GetItemSheet(); + stakeState.CalculateAccumulatedItemRewards( + context.BlockIndex, + out var itemV1Step, + out var itemV2Step, + out var itemV3Step); + stakeState.CalculateAccumulatedRuneRewards( + context.BlockIndex, + out var runeV1Step, + out var runeV2Step, + out var runeV3Step); + stakeState.CalculateAccumulatedCurrencyRewards( + context.BlockIndex, + out var currencyV1Step, + out var currencyV2Step, + out var currencyV3Step); + stakeState.CalculateAccumulatedCurrencyCrystalRewards( + context.BlockIndex, + out var currencyCrystalV1Step, + out var currencyCrystalV2Step, + out var currencyCrystalV3Step); + if (itemV1Step > 0) + { + var v1Level = Math.Min(level, V1.MaxLevel); + var fixedRewardV1 = V1.StakeRegularFixedRewardSheet[v1Level].Rewards; + var regularRewardV1 = V1.StakeRegularRewardSheet[v1Level].Rewards; + states = ProcessReward( + context, + states, + ref avatarState, + itemSheet, + stakedAmount, + itemV1Step, + runeV1Step, + currencyV1Step, + currencyCrystalV1Step, + fixedRewardV1, + regularRewardV1); + } + + if (itemV2Step > 0) + { + var v2Level = Math.Min(level, V2.MaxLevel); + var fixedRewardV2 = V2.StakeRegularFixedRewardSheet[v2Level].Rewards; + var regularRewardV2 = V2.StakeRegularRewardSheet[v2Level].Rewards; + states = ProcessReward( + context, + states, + ref avatarState, + itemSheet, + stakedAmount, + itemV2Step, + runeV2Step, + currencyV2Step, + currencyCrystalV2Step, + fixedRewardV2, + regularRewardV2); + } + + if (itemV3Step > 0) + { + var regularFixedReward = GetRegularFixedRewardInfos(states, level); + var regularReward = sheets.GetSheet()[level].Rewards; + states = ProcessReward( + context, + states, + ref avatarState, + itemSheet, + stakedAmount, + itemV3Step, + runeV3Step, + currencyV3Step, + currencyCrystalV3Step, + regularFixedReward, + regularReward); + } + + stakeState.Claim(context.BlockIndex); + + if (migrationRequired) + { + states = states + .SetState(avatarState.address, avatarState.SerializeV2()) + .SetState( + avatarState.address.Derive(LegacyWorldInformationKey), + avatarState.worldInformation.Serialize()) + .SetState( + avatarState.address.Derive(LegacyQuestListKey), + avatarState.questList.Serialize()); + } + + return states + .SetState(stakeState.address, stakeState.Serialize()) + .SetState( + avatarState.address.Derive(LegacyInventoryKey), + avatarState.inventory.Serialize()); + } + + private static List GetRegularFixedRewardInfos( + IAccountState states, + int level) + { + return states.TryGetSheet(out var fixedRewardSheet) + ? fixedRewardSheet[level].Rewards + : new List(); + } + + private IAccountStateDelta ProcessReward( + IActionContext context, + IAccountStateDelta states, + ref AvatarState avatarState, + ItemSheet itemSheet, + FungibleAssetValue stakedFav, + int itemRewardStep, + int runeRewardStep, + int currencyRewardStep, + int currencyCrystalRewardStep, + List fixedReward, + List regularReward) + { + // Regular Reward + foreach (var reward in regularReward) + { + var rateFav = FungibleAssetValue.Parse( + stakedFav.Currency, + reward.DecimalRate.ToString(CultureInfo.InvariantCulture)); + var rewardQuantityForSingleStep = stakedFav.DivRem(rateFav, out _); + if (rewardQuantityForSingleStep <= 0) + { + continue; + } + + switch (reward.Type) + { + case StakeRegularRewardSheet.StakeRewardType.Item: + { + if (itemRewardStep == 0) + { + continue; + } + + var itemRow = itemSheet[reward.ItemId]; + var item = itemRow is MaterialItemSheet.Row materialRow + ? ItemFactory.CreateTradableMaterial(materialRow) + : ItemFactory.CreateItem(itemRow, context.Random); + var majorUnit = (int)rewardQuantityForSingleStep * itemRewardStep; + if (majorUnit < 1) + { + continue; + } + + avatarState.inventory.AddItem(item, majorUnit); + break; + } + case StakeRegularRewardSheet.StakeRewardType.Rune: + { + if (runeRewardStep == 0) + { + continue; + } + + var majorUnit = rewardQuantityForSingleStep * runeRewardStep; + if (majorUnit < 1) + { + continue; + } + + var runeReward = RuneHelper.StakeRune * majorUnit; + states = states.MintAsset(context, AvatarAddress, runeReward); + break; + } + case StakeRegularRewardSheet.StakeRewardType.Currency: + { + if (string.IsNullOrEmpty(reward.CurrencyTicker)) + { + throw new NullReferenceException("currency ticker is null or empty"); + } + + var isCrystal = reward.CurrencyTicker == Currencies.Crystal.Ticker; + if (isCrystal + ? currencyCrystalRewardStep == 0 + : currencyRewardStep == 0) + { + continue; + } + + var rewardCurrency = reward.CurrencyDecimalPlaces == null + ? Currencies.GetMinterlessCurrency(reward.CurrencyTicker) + : Currency.Uncapped( + reward.CurrencyTicker, + Convert.ToByte(reward.CurrencyDecimalPlaces.Value), + minters: null); + var majorUnit = isCrystal + ? rewardQuantityForSingleStep * currencyCrystalRewardStep + : rewardQuantityForSingleStep * currencyRewardStep; + var rewardFav = rewardCurrency * majorUnit; + states = states.MintAsset( + context, + context.Signer, + rewardFav); + break; + } + default: + throw new ArgumentException( + $"Can't handle reward type: {reward.Type}", + nameof(regularReward)); + } + } + + // Fixed Reward + foreach (var reward in fixedReward) + { + var itemRow = itemSheet[reward.ItemId]; + var item = itemRow is MaterialItemSheet.Row materialRow + ? ItemFactory.CreateTradableMaterial(materialRow) + : ItemFactory.CreateItem(itemRow, context.Random); + avatarState.inventory.AddItem(item, reward.Count * itemRewardStep); + } + + return states; + } + } +} diff --git a/Lib9c/Action/Factory/ClaimStakeRewardFactory.cs b/Lib9c/Action/Factory/ClaimStakeRewardFactory.cs index ccd06aaffd..658ec27def 100644 --- a/Lib9c/Action/Factory/ClaimStakeRewardFactory.cs +++ b/Lib9c/Action/Factory/ClaimStakeRewardFactory.cs @@ -11,11 +11,16 @@ public static IClaimStakeReward CreateByBlockIndex( long blockIndex, Address avatarAddress) { - if (blockIndex > ClaimStakeReward5.ObsoleteBlockIndex) + if (blockIndex > ClaimStakeReward6.ObsoleteBlockIndex) { return new ClaimStakeReward(avatarAddress); } + if (blockIndex > ClaimStakeReward5.ObsoleteBlockIndex) + { + return new ClaimStakeReward6(avatarAddress); + } + if (blockIndex > ClaimStakeReward4.ObsoleteBlockIndex) { return new ClaimStakeReward5(avatarAddress); @@ -47,7 +52,8 @@ public static IClaimStakeReward CreateByVersion( 3 => new ClaimStakeReward3(avatarAddress), 4 => new ClaimStakeReward4(avatarAddress), 5 => new ClaimStakeReward5(avatarAddress), - 6 => new ClaimStakeReward(avatarAddress), + 6 => new ClaimStakeReward6(avatarAddress), + 7 => new ClaimStakeReward(avatarAddress), _ => throw new ArgumentOutOfRangeException( $"Invalid version: {version}"), }; diff --git a/Lib9c/ActionObsoleteConfig.cs b/Lib9c/ActionObsoleteConfig.cs index 12636e4fcf..1214e18efc 100644 --- a/Lib9c/ActionObsoleteConfig.cs +++ b/Lib9c/ActionObsoleteConfig.cs @@ -70,14 +70,10 @@ public static class ActionObsoleteConfig public const long V200040ObsoleteIndex = 7_330_000L; - // NOTE: - // Target Release Date: 2023-08-17T12:00:00 - // Current Datetime(Block Index): 2023-08-09T20:30:00(7,521,470) - // Seconds Per Block: 9.5 - // Estimated Release Block Index: 7,591,385 - // 7,521,470 + (8 * 24 - 7.5) * 60 * 60 / 9.5 = 7,521,470 + 69,915 = 7,591,385 public const long V200060ObsoleteIndex = 7_649_999L; + public const long V200061ObsoleteIndex = 7_660_000L; + // While v200020, the action obsolete wasn't work well. // So other previous `V*ObsoletedIndex`s lost its meaning and // this block index will replace them.