diff --git a/Lib9c b/Lib9c index e1742b5c9..103898317 160000 --- a/Lib9c +++ b/Lib9c @@ -1 +1 @@ -Subproject commit e1742b5c9a96d9b86a329e796c9c6e13e874ba0c +Subproject commit 1038983171a47f4c51a5951f10fccda27051046c diff --git a/NineChronicles.Headless.Tests/GraphTypes/Abstractions/CalculatedStakeRewardsTypeTest.cs b/NineChronicles.Headless.Tests/GraphTypes/Abstractions/CalculatedStakeRewardsTypeTest.cs new file mode 100644 index 000000000..bb7680b0d --- /dev/null +++ b/NineChronicles.Headless.Tests/GraphTypes/Abstractions/CalculatedStakeRewardsTypeTest.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GraphQL.Execution; +using Lib9c; +using Libplanet.Types.Assets; +using Nekoyume.Model.Item; +using NineChronicles.Headless.GraphTypes.Abstractions; +using Xunit; +using static NineChronicles.Headless.Tests.GraphQLTestUtils; + +namespace NineChronicles.Headless.Tests.GraphTypes.Abstractions +{ + public class CalculatedStakeRewardsTypeTest + { + [Fact] + public async Task Query() + { + const string query = @" + { + favs { + quantity + currency + } + items { + count + fungibleItemId + } + }"; + var materialItemSheet = Fixtures.TableSheetsFX.MaterialItemSheet; + var row = materialItemSheet.First; + var item = ItemFactory.CreateMaterial(row); + var fav = 1 * Currencies.Crystal; + var itemResult = new Dictionary + { + [item] = 2 + }; + var favs = new List + { + fav, + fav, + }; + var queryResult = await ExecuteQueryAsync( + query, + source: (itemResult, favs)); + var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; + Assert.NotEmpty(data); + Assert.Null(queryResult.Errors); + } + } +} diff --git a/NineChronicles.Headless.Tests/GraphTypes/States/Models/StakeStateTypeTest.cs b/NineChronicles.Headless.Tests/GraphTypes/States/Models/StakeStateTypeTest.cs index dd8ae477d..58634f816 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/States/Models/StakeStateTypeTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/States/Models/StakeStateTypeTest.cs @@ -43,6 +43,7 @@ public async Task Query(StakeStateV2 stakeState, Address stakeStateAddress, long source: new StakeStateType.StakeStateContext( stakeState, stakeStateAddress, + stakeStateAddress, mockState, blockIndex, new StateMemoryCache())); var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; diff --git a/NineChronicles.Headless/GraphTypes/Abstractions/CalculatedStakeRewardsType.cs b/NineChronicles.Headless/GraphTypes/Abstractions/CalculatedStakeRewardsType.cs new file mode 100644 index 000000000..de9e4194d --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/Abstractions/CalculatedStakeRewardsType.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using GraphQL.Types; +using Libplanet.Types.Assets; +using Nekoyume.Model.Item; +using NineChronicles.Headless.GraphTypes.States.Models.Item; + +namespace NineChronicles.Headless.GraphTypes.Abstractions +{ + public class CalculatedStakeRewardsType : ObjectGraphType<(Dictionary itemResult, List favs)> + { + public CalculatedStakeRewardsType() + { + Field>( + "favs", + resolve: context => context.Source.favs); + Field>( + "items", + resolve: context => + { + var result = new List(); + foreach (var pair in context.Source.itemResult) + { + var item = new Inventory.Item(pair.Key, pair.Value); + result.Add(item); + } + + return result; + }); + } + } +} diff --git a/NineChronicles.Headless/GraphTypes/StateQuery.cs b/NineChronicles.Headless/GraphTypes/StateQuery.cs index d088fb5af..528eea92e 100644 --- a/NineChronicles.Headless/GraphTypes/StateQuery.cs +++ b/NineChronicles.Headless/GraphTypes/StateQuery.cs @@ -253,6 +253,7 @@ public StateQuery() return new StakeStateType.StakeStateContext( stakeStateV2, stakeStateAddress, + agentAddress, ctx.AccountState, ctx.BlockIndex, ctx.StateMemoryCache diff --git a/NineChronicles.Headless/GraphTypes/States/StakeStateType.cs b/NineChronicles.Headless/GraphTypes/States/StakeStateType.cs index 9d82d3f4b..3c2cf824c 100644 --- a/NineChronicles.Headless/GraphTypes/States/StakeStateType.cs +++ b/NineChronicles.Headless/GraphTypes/States/StakeStateType.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Bencodex.Types; using GraphQL; using GraphQL.Types; @@ -8,13 +9,9 @@ using Libplanet.Action.State; using Libplanet.Crypto; using Nekoyume.Model.State; -using NineChronicles.Headless.GraphTypes.States.Models; -using NineChronicles.Headless.GraphTypes.States.Models.World; -using NineChronicles.Headless.GraphTypes.States.Models.Item; -using NineChronicles.Headless.GraphTypes.States.Models.Mail; -using NineChronicles.Headless.GraphTypes.States.Models.Quest; -using Nekoyume.Blockchain.Policy; using Nekoyume; +using Nekoyume.Action; +using Nekoyume.Extensions; using Nekoyume.Model.Stake; using Nekoyume.TableData; using NineChronicles.Headless.GraphTypes.Abstractions; @@ -25,15 +22,17 @@ public class StakeStateType : ObjectGraphType { public class StakeStateContext : StateContext { - public StakeStateContext(StakeStateV2 stakeState, Address address, IAccountState accountState, long blockIndex, StateMemoryCache stateMemoryCache) + public StakeStateContext(StakeStateV2 stakeState, Address address, Address agentAddress, IAccountState accountState, long blockIndex, StateMemoryCache stateMemoryCache) : base(accountState, blockIndex, stateMemoryCache) { StakeState = stakeState; Address = address; + AgentAddress = agentAddress; } public StakeStateV2 StakeState { get; } public Address Address { get; } + public Address AgentAddress { get; } } public StakeStateType() @@ -98,6 +97,100 @@ public StakeStateType() return (stakeRegularRewardSheet, stakeRegularFixedRewardSheet); } ); + Field>( + "calculatedStakeRewards", + resolve: context => + { + if (context.Source.StakeState.Contract is not { } contract) + { + return null; + } + + IReadOnlyList values = context.Source.GetStates(new[] + { + Addresses.GetSheetAddress(contract.StakeRegularFixedRewardSheetTableName), + Addresses.GetSheetAddress(contract.StakeRegularRewardSheetTableName), + }); + + if (!(values[0] is Text fsv && values[1] is Text sv)) + { + throw new ExecutionError("Could not found stake rewards sheets"); + } + + StakeRegularFixedRewardSheet stakeRegularFixedRewardSheet = new StakeRegularFixedRewardSheet(); + StakeRegularRewardSheet stakeRegularRewardSheet = new StakeRegularRewardSheet(); + stakeRegularFixedRewardSheet.Set(fsv); + stakeRegularRewardSheet.Set(sv); + + var accountState = context.Source.AccountState; + var ncg = accountState.GetGoldCurrency(); + var stakedNcg = accountState.GetBalance(context.Source.Address, ncg); + var stakingLevel = Math.Min( + stakeRegularRewardSheet.FindLevelByStakedAmount( + context.Source.AgentAddress, + stakedNcg), + stakeRegularRewardSheet.Keys.Max()); + var itemSheet = accountState.GetItemSheet(); + accountState.TryGetStakeStateV2(context.Source.AgentAddress, out var stakeStateV2); + // The first reward is given at the claimable block index. + var rewardSteps = stakeStateV2.ClaimableBlockIndex == context.Source.BlockIndex + ? 1 + : 1 + (int)Math.DivRem( + context.Source.BlockIndex - stakeStateV2.ClaimableBlockIndex, + stakeStateV2.Contract.RewardInterval, + out _); + + var random = new Random(); + var result = StakeRewardCalculator.CalculateFixedRewards(stakingLevel, random, stakeRegularFixedRewardSheet, + itemSheet, rewardSteps); + var (itemResult, favResult) = StakeRewardCalculator.CalculateRewards(ncg, stakedNcg, stakingLevel, rewardSteps, + stakeRegularRewardSheet, itemSheet, random); + foreach (var pair in itemResult) + { + var item = pair.Key; + result.TryAdd(item, 0); + result[item] += pair.Value; + } + return (result, favResult); + } + ); + } + + public class Random : IRandom + { + private readonly System.Random _random; + + public Random(int seed = default) + { + _random = new System.Random(seed); + } + + public int Seed => 0; + + public int Next() + { + return _random.Next(); + } + + public int Next(int maxValue) + { + return _random.Next(maxValue); + } + + public int Next(int minValue, int maxValue) + { + return _random.Next(minValue, maxValue); + } + + public void NextBytes(byte[] buffer) + { + _random.NextBytes(buffer); + } + + public double NextDouble() + { + return _random.NextDouble(); + } } } }