diff --git a/NineChronicles.Headless.Executable.Tests/Commands/TxCommandTest.cs b/NineChronicles.Headless.Executable.Tests/Commands/TxCommandTest.cs index 1704478f8..dc09855de 100644 --- a/NineChronicles.Headless.Executable.Tests/Commands/TxCommandTest.cs +++ b/NineChronicles.Headless.Executable.Tests/Commands/TxCommandTest.cs @@ -73,18 +73,9 @@ public void Sign_Stake(bool gas) } [Theory] - [InlineData(null, null, false)] - [InlineData(0, null, true)] - [InlineData(ClaimStakeReward2.ObsoletedIndex - 1, null, false)] - [InlineData(ClaimStakeReward2.ObsoletedIndex, null, true)] - [InlineData(ClaimStakeReward2.ObsoletedIndex + 1, null, false)] - [InlineData(long.MaxValue, null, true)] - [InlineData(null, 1, false)] - [InlineData(null, 2, true)] - [InlineData(null, 3, false)] - [InlineData(null, 4, true)] - [InlineData(null, 5, false)] - public void Sign_ClaimStakeReward(long? blockIndex, int? actionVersion, bool gas) + [InlineData(true)] + [InlineData(false)] + public void Sign_ClaimStakeReward(bool gas) { var filePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); var actionCommand = new ActionCommand(_console); diff --git a/NineChronicles.Headless.Executable/Program.cs b/NineChronicles.Headless.Executable/Program.cs index 125cd02aa..c4a0fc92c 100644 --- a/NineChronicles.Headless.Executable/Program.cs +++ b/NineChronicles.Headless.Executable/Program.cs @@ -442,6 +442,7 @@ IActionLoader MakeSingleActionLoader() hostBuilder.ConfigureServices(services => { services.AddSingleton(_ => standaloneContext); + services.AddSingleton(standaloneContext.KeyStore); services.AddOpenTelemetry() .ConfigureResource(resource => resource.AddService( serviceName: Assembly.GetEntryAssembly()?.GetName().Name ?? "NineChronicles.Headless", diff --git a/NineChronicles.Headless.Tests/Action/ActionContext.cs b/NineChronicles.Headless.Tests/Action/ActionContext.cs new file mode 100644 index 000000000..8f13f472c --- /dev/null +++ b/NineChronicles.Headless.Tests/Action/ActionContext.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Extensions.ActionEvaluatorCommonComponents; +using Libplanet.Types.Evidence; +using Libplanet.Types.Tx; + +namespace NineChronicles.Headless.Tests.Action; + +public class ActionContext : IActionContext +{ + private long UsedGas { get; set; } + + public Address Signer { get; init; } + public TxId? TxId { get; init; } + public Address Miner { get; init; } + public long BlockIndex { get; init; } + public int BlockProtocolVersion { get; init; } + public IWorld PreviousState { get; init; } + public int RandomSeed { get; init; } + public bool IsPolicyAction { get; init; } + public IReadOnlyList Txs { get; init; } + public IReadOnlyList Evidence { get; init; } + public void UseGas(long gas) + { + UsedGas += gas; + } + + public IRandom GetRandom() + { + return new Random(RandomSeed); + } + + public long GasUsed() + { + return UsedGas; + } + + public long GasLimit() + { + return 0L; + } +} diff --git a/NineChronicles.Headless.Tests/GraphQLTestUtils.cs b/NineChronicles.Headless.Tests/GraphQLTestUtils.cs index 314f9d802..88707debf 100644 --- a/NineChronicles.Headless.Tests/GraphQLTestUtils.cs +++ b/NineChronicles.Headless.Tests/GraphQLTestUtils.cs @@ -24,6 +24,10 @@ using Nekoyume.Action.Loader; using Nekoyume.Model.State; using Nekoyume.Module; +using NineChronicles.Headless.Repositories.BlockChain; +using NineChronicles.Headless.Repositories.StateTrie; +using NineChronicles.Headless.Repositories.Transaction; +using NineChronicles.Headless.Repositories.WorldState; using NineChronicles.Headless.Utils; namespace NineChronicles.Headless.Tests @@ -61,6 +65,10 @@ public static Task ExecuteQueryAsync( services.AddLibplanetExplorer(); services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); return ExecuteQueryAsync( @@ -78,6 +86,15 @@ public static Task ExecuteQueryAsync( where TObjectGraphType : IObjectGraphType { var graphType = (IObjectGraphType)serviceProvider.GetService(typeof(TObjectGraphType))!; + return ExecuteQueryAsync(graphType, query, userContext, source); + } + + public static Task ExecuteQueryAsync( + IObjectGraphType graphType, + string query, + IDictionary? userContext = null, + object? source = null) + { var documentExecutor = new DocumentExecuter(); return documentExecutor.ExecuteAsync(new ExecutionOptions { diff --git a/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs b/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs index 01e1d1889..eff90d826 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs @@ -34,7 +34,16 @@ using System.Threading; using System.Threading.Tasks; using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Mocks; using Libplanet.Types.Tx; +using Moq; +using NineChronicles.Headless.Executable.Tests.KeyStore; +using NineChronicles.Headless.Repositories; +using NineChronicles.Headless.Repositories.BlockChain; +using NineChronicles.Headless.Repositories.StateTrie; +using NineChronicles.Headless.Repositories.Transaction; +using NineChronicles.Headless.Repositories.WorldState; using Xunit.Abstractions; namespace NineChronicles.Headless.Tests.GraphTypes @@ -86,12 +95,10 @@ public GraphQLTestBase(ITestOutputHelper output) privateKey: AdminPrivateKey); var ncService = ServiceBuilder.CreateNineChroniclesNodeService(genesisBlock, ProposerPrivateKey); - var tempKeyStorePath = Path.Join(Path.GetTempPath(), Path.GetRandomFileName()); - var keyStore = new Web3KeyStore(tempKeyStorePath); StandaloneContextFx = new StandaloneContext { - KeyStore = keyStore, + KeyStore = KeyStore, DifferentAppProtocolVersionEncounterInterval = TimeSpan.FromSeconds(1), NotificationInterval = TimeSpan.FromSeconds(1), NodeExceptionInterval = TimeSpan.FromSeconds(1), @@ -117,6 +124,12 @@ public GraphQLTestBase(ITestOutputHelper output) ); services.AddSingleton(publisher); services.AddSingleton(StandaloneContextFx); + services.AddTransient(provider => provider.GetService().BlockChain); + services.AddSingleton(WorldStateRepository.Object); + services.AddSingleton(BlockChainRepository.Object); + services.AddSingleton(StateTrieRepository.Object); + services.AddSingleton(TransactionRepository.Object); + services.AddSingleton(KeyStore); services.AddSingleton(configuration); services.AddGraphTypes(); services.AddLibplanetExplorer(); @@ -129,6 +142,12 @@ public GraphQLTestBase(ITestOutputHelper output) DocumentExecutor = new DocumentExecuter(); } + protected Mock WorldStateRepository { get; } = new(); + protected Mock StateTrieRepository { get; } = new(); + protected Mock BlockChainRepository { get; } = new(); + protected Mock TransactionRepository { get; } = new(); + protected IKeyStore KeyStore { get; } = new InMemoryKeyStore(); + protected PrivateKey AdminPrivateKey { get; } = new PrivateKey(); protected Address AdminAddress => AdminPrivateKey.Address; @@ -150,9 +169,6 @@ protected List GenesisValidators protected BlockChain BlockChain => StandaloneContextFx.BlockChain!; - protected IKeyStore KeyStore => - StandaloneContextFx.KeyStore!; - protected IDocumentExecuter DocumentExecutor { get; } protected SubscriptionDocumentExecuter SubscriptionDocumentExecuter { get; } = new SubscriptionDocumentExecuter(); @@ -187,6 +203,25 @@ protected async Task StartAsync( return task; } + protected void SetupStatesOnTip(Func func) + { + var worldState = func(new World(MockUtil.MockModernWorldState)); + var stateRootHash = worldState.Trie.Hash; + var tip = new Domain.Model.BlockChain.Block( + BlockHash.FromString("613dfa26e104465790625ae7bc03fc27a64947c02a9377565ec190405ef7154b"), + BlockHash.FromString("36456be15af9a5b9b13a02c7ce1e849ae9cba8781ec309010499cdb93e29237d"), + default(Address), + 0, + Timestamp: DateTimeOffset.UtcNow, + StateRootHash: stateRootHash, + Transactions: ImmutableArray.Empty + ); + BlockChainRepository.Setup(repository => repository.GetTip()) + .Returns(tip); + WorldStateRepository.Setup(repository => repository.GetWorldState(stateRootHash)) + .Returns(worldState); + } + protected LibplanetNodeService CreateLibplanetNodeService( Block genesisBlock, AppProtocolVersion appProtocolVersion, diff --git a/NineChronicles.Headless.Tests/GraphTypes/StandaloneQueryTest.cs b/NineChronicles.Headless.Tests/GraphTypes/StandaloneQueryTest.cs index d668b7cef..43385b869 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/StandaloneQueryTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/StandaloneQueryTest.cs @@ -6,11 +6,14 @@ using System.IO; using System.Linq; using System.Numerics; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Bencodex; using Bencodex.Types; using GraphQL.Execution; +using GraphQL.Types; +using Lib9c; using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Action.Sys; @@ -19,19 +22,28 @@ using Libplanet.Crypto; using Libplanet.Headless.Hosting; using Libplanet.KeyStore; +using Libplanet.Mocks; using Libplanet.Net; +using Libplanet.Store.Trie; using Libplanet.Types.Assets; using Libplanet.Types.Blocks; using Libplanet.Types.Consensus; using Libplanet.Types.Tx; +using Moq; using Nekoyume; using Nekoyume.Action; using Nekoyume.Blockchain.Policy; using Nekoyume.Helper; using Nekoyume.Model; +using Nekoyume.Model.Item; +using Nekoyume.Model.Quest; using Nekoyume.Model.State; using Nekoyume.Module; using Nekoyume.TableData; +using NineChronicles.Headless.Executable; +using NineChronicles.Headless.GraphTypes; +using NineChronicles.Headless.Repositories.WorldState; +using NineChronicles.Headless.Tests.Action; using NineChronicles.Headless.Tests.Common; using Xunit; using Xunit.Abstractions; @@ -42,6 +54,7 @@ namespace NineChronicles.Headless.Tests.GraphTypes public partial class StandaloneQueryTest : GraphQLTestBase { private readonly Dictionary _sheets; + private readonly IObjectGraphType _graph; public StandaloneQueryTest(ITestOutputHelper output) : base(output) { @@ -51,14 +64,18 @@ public StandaloneQueryTest(ITestOutputHelper output) : base(output) [Fact] public async Task GetState() { - AppendEmptyBlock(GenesisValidators); + var adminAddress = new PrivateKey().Address; Address adminStateAddress = AdminState.Address; + + SetupStatesOnTip(world => world + .SetLegacyState(adminStateAddress, new AdminState(adminAddress, 10000).Serialize())); + var result = await ExecuteQueryAsync($"query {{ state(accountAddress: \"{ReservedAddresses.LegacyAccount}\", address: \"{adminStateAddress}\") }}"); var data = (Dictionary)((ExecutionNode)result.Data!).ToValue()!; IValue rawVal = new Codec().Decode(ByteUtil.ParseHex((string)data!["state"])); AdminState adminState = new AdminState((Dictionary)rawVal); - Assert.Equal(AdminAddress, adminState.AdminAddress); + Assert.Equal(adminAddress, adminState.AdminAddress); } [Theory] @@ -230,25 +247,39 @@ public async Task NodeStatusStagedTxIds() [Fact] public async Task NodeStatusGetTopMostBlocks() { - PrivateKey userPrivateKey = new PrivateKey(); - var validators = new List + Domain.Model.BlockChain.Block MakeFakeBlock(long index) { - ProposerPrivateKey, - userPrivateKey, - }.OrderBy(x => x.Address).ToList(); - var service = MakeNineChroniclesNodeService(userPrivateKey); - StandaloneContextFx.NineChroniclesNodeService = service; - StandaloneContextFx.BlockChain = service.Swarm?.BlockChain; - - var blockChain = StandaloneContextFx.BlockChain; - for (int i = 0; i < 10; i++) - { - Block block = blockChain!.ProposeBlock( - userPrivateKey, - lastCommit: GenerateBlockCommit(BlockChain.Tip.Index, BlockChain.Tip.Hash, validators)); - blockChain!.Append(block, GenerateBlockCommit(block.Index, block.Hash, validators)); + return new Domain.Model.BlockChain.Block( + BlockHash.FromString("613dfa26e104465790625ae7bc03fc27a64947c02a9377565ec190405ef7154b"), null, + default, index, DateTimeOffset.UtcNow, MerkleTrie.EmptyRootHash, ImmutableArray.Empty); } + BlockChainRepository.Setup(repo => repo.IterateBlocksDescending(0)) + .Returns(new List + { + MakeFakeBlock(10), + MakeFakeBlock(9), + MakeFakeBlock(8), + MakeFakeBlock(7), + MakeFakeBlock(6), + MakeFakeBlock(5), + MakeFakeBlock(4), + MakeFakeBlock(3), + MakeFakeBlock(2), + MakeFakeBlock(1), + MakeFakeBlock(0), + }); + BlockChainRepository.Setup(repo => repo.IterateBlocksDescending(5)) + .Returns(new List + { + MakeFakeBlock(5), + MakeFakeBlock(4), + MakeFakeBlock(3), + MakeFakeBlock(2), + MakeFakeBlock(1), + MakeFakeBlock(0), + }); + var queryWithoutOffset = @"query { nodeStatus { topmostBlocks(limit: 1) { @@ -549,18 +580,11 @@ public async Task ActivationStatus(bool existsActivatedAccounts) [Fact] public async Task GoldBalance() { - var userPrivateKey = new PrivateKey(); - var userAddress = userPrivateKey.Address; - var validators = new List - { - ProposerPrivateKey, userPrivateKey - }.OrderBy(x => x.Address).ToList(); - var service = MakeNineChroniclesNodeService(userPrivateKey); - StandaloneContextFx.NineChroniclesNodeService = service; - StandaloneContextFx.BlockChain = service.Swarm?.BlockChain; - AppendEmptyBlock(validators); + var userAddress = new PrivateKey().Address; + + SetupStatesOnTip(world => + world.SetLegacyState(Addresses.GoldCurrency, new GoldCurrencyState(Currencies.Crystal).Serialize())); - var blockChain = StandaloneContextFx.BlockChain; var query = $"query {{ goldBalance(address: \"{userAddress}\") }}"; var queryResult = await ExecuteQueryAsync(query); var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; @@ -572,11 +596,9 @@ public async Task GoldBalance() data ); - Block block = blockChain!.ProposeBlock( - userPrivateKey, - lastCommit: GenerateBlockCommit(BlockChain.Tip.Index, BlockChain.Tip.Hash, validators)); - blockChain!.Append(block, GenerateBlockCommit(block.Index, block.Hash, validators)); - AppendEmptyBlock(validators); + SetupStatesOnTip(world => + world.SetLegacyState(Addresses.GoldCurrency, new GoldCurrencyState(Currencies.Crystal).Serialize()) + .MintAsset(new ActionContext(), userAddress, Currencies.Crystal * 10)); queryResult = await ExecuteQueryAsync(query); data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; @@ -594,37 +616,39 @@ public async Task GoldBalance() [InlineData("memo")] public async Task TransferNCGHistories(string? memo) { - PrivateKey senderKey = ProposerPrivateKey, recipientKey = new PrivateKey(); + PrivateKey senderKey = new PrivateKey(), recipientKey = new PrivateKey(); Address sender = senderKey.Address, recipient = recipientKey.Address; - Block block = BlockChain.ProposeBlock( - ProposerPrivateKey, - lastCommit: GenerateBlockCommit(BlockChain.Tip.Index, BlockChain.Tip.Hash, GenesisValidators)); - BlockChain.Append(block, GenerateBlockCommit(block.Index, block.Hash, GenesisValidators)); - block = BlockChain.ProposeBlock( - ProposerPrivateKey, - lastCommit: GenerateBlockCommit(BlockChain.Tip.Index, BlockChain.Tip.Hash, GenesisValidators)); - BlockChain.Append(block, GenerateBlockCommit(block.Index, block.Hash, GenesisValidators)); - - var currency = new GoldCurrencyState((Dictionary)BlockChain.GetWorldState().GetLegacyState(Addresses.GoldCurrency)).Currency; - Transaction MakeTx(ActionBase action) + var blockHash = BlockHash.FromString("613dfa26e104465790625ae7bc03fc27a64947c02a9377565ec190405ef7154b"); + Transaction MakeTx(long nonce, ActionBase action) { - return BlockChain.MakeTransaction(ProposerPrivateKey, new ActionBase[] { action }); + return Transaction.Create(nonce, senderKey, blockHash, new[] { action.PlainValue }); } + + var currency = Currency.Uncapped("NCG", 2, null); var txs = new[] { - MakeTx(new TransferAsset0(sender, recipient, new FungibleAssetValue(currency, 1, 0), memo)), - MakeTx(new TransferAsset(sender, recipient, new FungibleAssetValue(currency, 1, 0), memo)), + MakeTx(0, new TransferAsset0(sender, recipient, new FungibleAssetValue(currency, 1, 0), memo)), + MakeTx(1, new TransferAsset(sender, recipient, new FungibleAssetValue(currency, 1, 0), memo)), }; + var block = new Domain.Model.BlockChain.Block( + blockHash, + BlockHash.FromString("36456be15af9a5b9b13a02c7ce1e849ae9cba8781ec309010499cdb93e29237d"), + default(Address), + 0, + Timestamp: DateTimeOffset.UtcNow, + StateRootHash: MerkleTrie.EmptyRootHash, + Transactions: txs.ToImmutableArray() + ); - block = BlockChain.ProposeBlock( - ProposerPrivateKey, lastCommit: GenerateBlockCommit(BlockChain.Tip.Index, BlockChain.Tip.Hash, GenesisValidators)); - BlockChain.Append(block, GenerateBlockCommit(block.Index, block.Hash, GenesisValidators)); - - foreach (var tx in txs) - { - Assert.NotNull(StandaloneContextFx.Store?.GetTxExecution(block.Hash, tx.Id)); - } + BlockChainRepository.Setup(repo => repo.GetBlock(blockHash)) + .Returns(block); + TransactionRepository.Setup(repo => repo.GetTxExecution(blockHash, txs[0].Id)) + .Returns(new TxExecution( + blockHash, txs[0].Id, false, MerkleTrie.EmptyRootHash, MerkleTrie.EmptyRootHash, new List())); + TransactionRepository.Setup(repo => repo.GetTxExecution(blockHash, txs[1].Id)) + .Returns(new TxExecution( + blockHash, txs[1].Id, false, MerkleTrie.EmptyRootHash, MerkleTrie.EmptyRootHash, new List())); var blockHashHex = ByteUtil.Hex(block.Hash.ToByteArray()); var result = @@ -691,6 +715,12 @@ public async Task MonsterCollectionStatus_AgentState_Null(bool miner) { Assert.Equal(userPrivateKey, StandaloneContextFx.NineChroniclesNodeService.MinerPrivateKey!); } + + // FIXME: Remove the above lines after removing `StandaloneContext` dependency. + SetupStatesOnTip(world => + world.SetLegacyState(Addresses.GoldCurrency, new GoldCurrencyState(Currencies.Crystal).Serialize()) + .MintAsset(new ActionContext(), userAddress, Currencies.Crystal * 10)); + string queryArgs = miner ? "" : $@"(address: ""{userAddress}"")"; string query = $@"query {{ monsterCollectionStatus{queryArgs} {{ @@ -729,23 +759,12 @@ public async Task MonsterCollectionStatus_MonsterCollectionState_Null(bool miner { StandaloneContextFx.NineChroniclesNodeService.MinerPrivateKey = null; } - var action = new CreateAvatar - { - index = 0, - hair = 1, - lens = 2, - ear = 3, - tail = 4, - name = "action", - }; - var blockChain = StandaloneContextFx.BlockChain; - var transaction = blockChain.MakeTransaction(userPrivateKey, new ActionBase[] { action }); - blockChain.StageTransaction(transaction); - Block block = blockChain.ProposeBlock( - userPrivateKey, - lastCommit: GenerateBlockCommit(BlockChain.Tip.Index, BlockChain.Tip.Hash, validators)); - blockChain.Append(block, GenerateBlockCommit(block.Index, block.Hash, validators)); - AppendEmptyBlock(validators); + + // FIXME: Remove the above lines after removing `StandaloneContext` dependency. + SetupStatesOnTip(world => world + .SetLegacyState(Addresses.GoldCurrency, new GoldCurrencyState(Currencies.Crystal).Serialize()) + .SetAgentState(userAddress, new AgentState(userAddress)) + .MintAsset(new ActionContext(), userAddress, Currencies.Crystal * 10)); string queryArgs = miner ? "" : $@"(address: ""{userAddress}"")"; string query = $@"query {{ @@ -772,40 +791,16 @@ public async Task MonsterCollectionStatus_MonsterCollectionState_Null(bool miner [Fact] public async Task Avatar() { - var userPrivateKey = new PrivateKey(); - var userAddress = userPrivateKey.Address; - var validators = new List - { - ProposerPrivateKey, userPrivateKey - }.OrderBy(x => x.Address).ToList(); - var service = MakeNineChroniclesNodeService(userPrivateKey); - StandaloneContextFx.NineChroniclesNodeService = service; - StandaloneContextFx.BlockChain = service.Swarm!.BlockChain; - var action = new CreateAvatar - { - index = 0, - hair = 1, - lens = 2, - ear = 3, - tail = 4, - name = "action", - }; - var blockChain = StandaloneContextFx.BlockChain; - var transaction = blockChain.MakeTransaction(userPrivateKey, new ActionBase[] { action }); - blockChain.StageTransaction(transaction); - Block block = blockChain.ProposeBlock( - userPrivateKey, - lastCommit: GenerateBlockCommit(BlockChain.Tip.Index, BlockChain.Tip.Hash, validators)); - blockChain.Append(block, GenerateBlockCommit(block.Index, block.Hash, validators)); - AppendEmptyBlock(validators); - - var avatarAddress = userAddress.Derive( - string.Format( - CultureInfo.InvariantCulture, - CreateAvatar.DeriveFormat, - 0 - ) - ); + var agentAddress = new Address("f189c04126e2e708cd7d17cd68a7b7f10bbb6f16"); + var avatarAddress = new Address("f1a005c01e683dbcab9a306d5cc70d5e57fccfa9"); + var worldInformation = new WorldInformation(Dictionary.Empty); + var questList = new QuestList(new List((Integer)0, List.Empty, List.Empty)); + var avatarState = new AvatarState( + agentAddress, avatarAddress, 0, questList, worldInformation, default, "name"); + avatarState.inventory = new Inventory(); + + SetupStatesOnTip(world => world + .SetAvatarState(avatarAddress, avatarState)); string query = $@"query {{ stateQuery {{ @@ -823,90 +818,14 @@ public async Task Avatar() [InlineData(false)] public async Task ActivationKeyNonce(bool trim) { - var adminPrivateKey = new PrivateKey(); - var adminAddress = adminPrivateKey.Address; - var activatedAccounts = ImmutableHashSet
.Empty; - var nonce = new byte[] { 0x00, 0x01, 0x02, 0x03 }; var privateKey = new PrivateKey(); - (ActivationKey activationKey, PendingActivationState pendingActivation) = - ActivationKey.Create(privateKey, nonce); - var pendingActivationStates = new List - { - pendingActivation, - }; - Block genesis = - BlockChain.ProposeGenesisBlock( - transactions: ImmutableList.Empty - .Add( - Transaction.Create( - 0, ProposerPrivateKey, - null, - new IAction[] - { - new Initialize( - new ValidatorSet( - GenesisValidators.Select( - v => new Validator(v.PublicKey, BigInteger.One)).ToList()), - ImmutableDictionary.Create()) - }.Select(a => a.PlainValue))) - .Add( - Transaction.Create( - 1, - ProposerPrivateKey, - null, - new ActionBase[] - { - new InitializeStates( - rankingState: new RankingState0(), - shopState: new ShopState(), - gameConfigState: new GameConfigState(), - redeemCodeState: new RedeemCodeState(Bencodex.Types.Dictionary.Empty - .Add("address", RedeemCodeState.Address.Serialize()) - .Add("map", Bencodex.Types.Dictionary.Empty) - ), - adminAddressState: new AdminState(adminAddress, 1500000), - activatedAccountsState: new ActivatedAccountsState(activatedAccounts), -#pragma warning disable CS0618 - // Use of obsolete method Currency.Legacy(): - // https://github.com/planetarium/lib9c/discussions/1319 - goldCurrencyState: new GoldCurrencyState(Currency.Legacy("NCG", 2, null)), -#pragma warning restore CS0618 - goldDistributions: new GoldDistribution[0], - tableSheets: _sheets, - pendingActivationStates: pendingActivationStates.ToArray() - ), - }.ToPlainValues())) - ); - - var apvPrivateKey = new PrivateKey(); - var apv = AppProtocolVersion.Sign(apvPrivateKey, 0); - var userPrivateKey = new PrivateKey(); - var consensusPrivateKey = new PrivateKey(); - var properties = new LibplanetNodeServiceProperties - { - Host = System.Net.IPAddress.Loopback.ToString(), - AppProtocolVersion = apv, - GenesisBlock = genesis, - StorePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()), - StoreStatesCacheSize = 2, - SwarmPrivateKey = new PrivateKey(), - ConsensusPrivateKey = consensusPrivateKey, - ConsensusPort = null, - Port = null, - NoMiner = true, - Render = false, - Peers = ImmutableHashSet.Empty, - TrustedAppProtocolVersionSigners = null, - IceServers = ImmutableList.Empty, - ConsensusSeeds = ImmutableList.Empty, - ConsensusPeers = ImmutableList.Empty - }; + var random = new Random(); + var nonce = new byte[10]; + random.NextBytes(nonce); + var (activationKey, pendingActivationState) = ActivationKey.Create(privateKey, nonce); - var blockPolicy = NineChroniclesNodeService.GetBlockPolicy(Planet.Odin, StaticActionLoaderSingleton.Instance, null); - var service = new NineChroniclesNodeService(userPrivateKey, properties, blockPolicy, Planet.Odin, StaticActionLoaderSingleton.Instance); - StandaloneContextFx.NineChroniclesNodeService = service; - StandaloneContextFx.BlockChain = service.Swarm?.BlockChain; - AppendEmptyBlock(GenesisValidators); + SetupStatesOnTip(world => world + .SetLegacyState(activationKey.PendingAddress, pendingActivationState.Serialize())); var code = activationKey.Encode(); if (trim) @@ -923,150 +842,41 @@ public async Task ActivationKeyNonce(bool trim) [Theory] [InlineData("1", "invitationCode format is invalid.")] [InlineData("9330b3287bd2bbc38770c69ae7cd380350c60a1dff9ec41254f3048d5b3eb01c", "invitationCode format is invalid.")] - [InlineData("9330b3287bd2bbc38770c69ae7cd380350c60a1dff9ec41254f3048d5b3eb01c/44C889Af1e1e90213Cff5d69C9086c34ecCb60B0", "invitationCode is invalid.")] - public async Task ActivationKeyNonce_Throw_ExecutionError(string code, string msg) + public async Task ActivationKeyNonce_ThrowError_WithInvalidFormatCode(string code, string msg) { - var adminPrivateKey = new PrivateKey(); - var adminAddress = adminPrivateKey.Address; - var activatedAccounts = ImmutableHashSet
.Empty; - var pendingActivationStates = new List(); + var query = $"query {{ activationKeyNonce(invitationCode: \"{code}\") }}"; + var queryResult = await ExecuteQueryAsync(query); + Assert.NotNull(queryResult.Errors); + Assert.Single(queryResult.Errors!); + Assert.Equal(msg, queryResult.Errors!.First().Message); + } - Block genesis = - BlockChain.ProposeGenesisBlock( - transactions: ImmutableList.Empty - .Add(Transaction.Create( - 0, - new PrivateKey(), - null, - new ActionBase[] - { - new InitializeStates( - rankingState: new RankingState0(), - shopState: new ShopState(), - gameConfigState: new GameConfigState(), - redeemCodeState: new RedeemCodeState(Bencodex.Types.Dictionary.Empty - .Add("address", RedeemCodeState.Address.Serialize()) - .Add("map", Bencodex.Types.Dictionary.Empty) - ), - adminAddressState: new AdminState(adminAddress, 1500000), - activatedAccountsState: new ActivatedAccountsState(activatedAccounts), -#pragma warning disable CS0618 - // Use of obsolete method Currency.Legacy(): - // https://github.com/planetarium/lib9c/discussions/1319 - goldCurrencyState: new GoldCurrencyState(Currency.Legacy("NCG", 2, null)), -#pragma warning restore CS0618 - goldDistributions: new GoldDistribution[0], - tableSheets: _sheets, - pendingActivationStates: pendingActivationStates.ToArray() - ), - }.ToPlainValues())) - ); + [Theory] + [InlineData("9330b3287bd2bbc38770c69ae7cd380350c60a1dff9ec41254f3048d5b3eb01c/44C889Af1e1e90213Cff5d69C9086c34ecCb60B0", "invitationCode is invalid.")] + public async Task ActivationKeyNonce_ThrowError_WithOutdatedCode(string code, string msg) + { + var activationKey = ActivationKey.Decode(code); - var apvPrivateKey = new PrivateKey(); - var apv = AppProtocolVersion.Sign(apvPrivateKey, 0); - var userPrivateKey = new PrivateKey(); - var consensusPrivateKey = new PrivateKey(); - var properties = new LibplanetNodeServiceProperties - { - Host = System.Net.IPAddress.Loopback.ToString(), - AppProtocolVersion = apv, - GenesisBlock = genesis, - StorePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()), - StoreStatesCacheSize = 2, - SwarmPrivateKey = new PrivateKey(), - ConsensusPrivateKey = consensusPrivateKey, - ConsensusPort = null, - Port = null, - NoMiner = true, - Render = false, - Peers = ImmutableHashSet.Empty, - TrustedAppProtocolVersionSigners = null, - IceServers = ImmutableList.Empty, - ConsensusSeeds = ImmutableList.Empty, - ConsensusPeers = ImmutableList.Empty - }; - var blockPolicy = new BlockPolicySource().GetPolicy(); - - var service = new NineChroniclesNodeService(userPrivateKey, properties, blockPolicy, Planet.Odin, StaticActionLoaderSingleton.Instance); - StandaloneContextFx.NineChroniclesNodeService = service; - StandaloneContextFx.BlockChain = service.Swarm?.BlockChain; + SetupStatesOnTip(world => world + .SetLegacyState(activationKey.PendingAddress, Bencodex.Types.Null.Value)); var query = $"query {{ activationKeyNonce(invitationCode: \"{code}\") }}"; var queryResult = await ExecuteQueryAsync(query); Assert.NotNull(queryResult.Errors); Assert.Single(queryResult.Errors!); Assert.Equal(msg, queryResult.Errors!.First().Message); - } [Fact] public async Task Balance() { - var adminPrivateKey = new PrivateKey(); - var adminAddress = adminPrivateKey.Address; - var activatedAccounts = ImmutableHashSet
.Empty; - var pendingActivationStates = new List(); - - Block genesis = - BlockChain.ProposeGenesisBlock( - transactions: ImmutableList.Empty.Add( - Transaction.Create(0, new PrivateKey(), null, - new ActionBase[] - { - new InitializeStates( - rankingState: new RankingState0(), - shopState: new ShopState(), - gameConfigState: new GameConfigState(), - redeemCodeState: new RedeemCodeState(Bencodex.Types.Dictionary.Empty - .Add("address", RedeemCodeState.Address.Serialize()) - .Add("map", Bencodex.Types.Dictionary.Empty) - ), - adminAddressState: new AdminState(adminAddress, 1500000), - activatedAccountsState: new ActivatedAccountsState(activatedAccounts), -#pragma warning disable CS0618 - // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 - goldCurrencyState: new GoldCurrencyState(Currency.Legacy("NCG", 2, null)), -#pragma warning restore CS0618 - goldDistributions: new GoldDistribution[0], - tableSheets: _sheets, - pendingActivationStates: pendingActivationStates.ToArray() - ), - }.ToPlainValues())) - ); + var address = new PrivateKey().Address; - var apvPrivateKey = new PrivateKey(); - var apv = AppProtocolVersion.Sign(apvPrivateKey, 0); - - var userPrivateKey = new PrivateKey(); - var consensusPrivateKey = new PrivateKey(); - var properties = new LibplanetNodeServiceProperties - { - Host = System.Net.IPAddress.Loopback.ToString(), - AppProtocolVersion = apv, - GenesisBlock = genesis, - StorePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()), - StoreStatesCacheSize = 2, - SwarmPrivateKey = new PrivateKey(), - ConsensusPrivateKey = consensusPrivateKey, - ConsensusPort = null, - Port = null, - NoMiner = true, - Render = false, - Peers = ImmutableHashSet.Empty, - TrustedAppProtocolVersionSigners = null, - IceServers = ImmutableList.Empty, - ConsensusSeeds = ImmutableList.Empty, - ConsensusPeers = ImmutableList.Empty - }; - var blockPolicy = new BlockPolicySource().GetPolicy(); - - var service = new NineChroniclesNodeService(userPrivateKey, properties, blockPolicy, Planet.Odin, StaticActionLoaderSingleton.Instance); - StandaloneContextFx.NineChroniclesNodeService = service; - StandaloneContextFx.BlockChain = service.Swarm?.BlockChain; + SetupStatesOnTip(world => world); var query = $@"query {{ stateQuery {{ - balance(address: ""{adminAddress}"", currency: {{ decimalPlaces: 18, ticker: ""CRYSTAL"" }}) {{ + balance(address: ""{address}"", currency: {{ decimalPlaces: 18, ticker: ""CRYSTAL"" }}) {{ quantity currency {{ ticker diff --git a/NineChronicles.Headless.Tests/NineChronicles.Headless.Tests.csproj b/NineChronicles.Headless.Tests/NineChronicles.Headless.Tests.csproj index 85ffad613..a5a69b9d2 100644 --- a/NineChronicles.Headless.Tests/NineChronicles.Headless.Tests.csproj +++ b/NineChronicles.Headless.Tests/NineChronicles.Headless.Tests.csproj @@ -21,10 +21,12 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -44,4 +46,8 @@ + + + + diff --git a/NineChronicles.Headless/BlockChainContext.cs b/NineChronicles.Headless/BlockChainContext.cs index 17a53f10a..34d04d631 100644 --- a/NineChronicles.Headless/BlockChainContext.cs +++ b/NineChronicles.Headless/BlockChainContext.cs @@ -15,7 +15,7 @@ public BlockChainContext(StandaloneContext standaloneContext) _standaloneContext = standaloneContext; } - public bool Preloaded => _standaloneContext.NodeStatus.PreloadEnded; + public bool Preloaded => _standaloneContext.PreloadEnded; public BlockChain BlockChain => _standaloneContext.BlockChain; public IStore Store => _standaloneContext.Store; public Swarm Swarm => _standaloneContext.Swarm; diff --git a/NineChronicles.Headless/Domain/Model/BlockChain/Block.cs b/NineChronicles.Headless/Domain/Model/BlockChain/Block.cs new file mode 100644 index 000000000..439144261 --- /dev/null +++ b/NineChronicles.Headless/Domain/Model/BlockChain/Block.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Blocks; +using Libplanet.Types.Tx; + +namespace NineChronicles.Headless.Domain.Model.BlockChain; + +using StateRootHash = HashDigest; + +public record Block( +#pragma warning disable SA1313 + BlockHash Hash, + BlockHash? PreviousHash, + Address Miner, + long Index, + DateTimeOffset Timestamp, + StateRootHash StateRootHash, + IEnumerable Transactions); +#pragma warning restore SA1313 diff --git a/NineChronicles.Headless/GraphQLService.cs b/NineChronicles.Headless/GraphQLService.cs index 3beadf376..c68027eb6 100644 --- a/NineChronicles.Headless/GraphQLService.cs +++ b/NineChronicles.Headless/GraphQLService.cs @@ -18,6 +18,10 @@ using NineChronicles.Headless.GraphTypes; using NineChronicles.Headless.Middleware; using NineChronicles.Headless.Properties; +using NineChronicles.Headless.Repositories.BlockChain; +using NineChronicles.Headless.Repositories.StateTrie; +using NineChronicles.Headless.Repositories.Transaction; +using NineChronicles.Headless.Repositories.WorldState; using Serilog; namespace NineChronicles.Headless @@ -158,6 +162,12 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); + // Repositories + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHealthChecks(); services.AddControllers(); diff --git a/NineChronicles.Headless/GraphTypes/BlockHeaderType.cs b/NineChronicles.Headless/GraphTypes/BlockHeaderType.cs index 575114bc4..3a06b6313 100644 --- a/NineChronicles.Headless/GraphTypes/BlockHeaderType.cs +++ b/NineChronicles.Headless/GraphTypes/BlockHeaderType.cs @@ -2,6 +2,7 @@ using Libplanet.Crypto; using Libplanet.Types.Blocks; using Libplanet.Explorer.GraphTypes; +using Block = NineChronicles.Headless.Domain.Model.BlockChain.Block; namespace NineChronicles.Headless.GraphTypes { diff --git a/NineChronicles.Headless/GraphTypes/NodeStatus.cs b/NineChronicles.Headless/GraphTypes/NodeStatus.cs index e03ef3bb6..3a21cbb15 100644 --- a/NineChronicles.Headless/GraphTypes/NodeStatus.cs +++ b/NineChronicles.Headless/GraphTypes/NodeStatus.cs @@ -1,6 +1,5 @@ using GraphQL; using GraphQL.Types; -using Libplanet.Blockchain; using Libplanet.Crypto; using Libplanet.Types.Blocks; using Libplanet.Explorer.GraphTypes; @@ -10,11 +9,17 @@ using System.Collections.Immutable; using System.Linq; using System.Reflection; +using NineChronicles.Headless.Repositories.BlockChain; +using Block = NineChronicles.Headless.Domain.Model.BlockChain.Block; namespace NineChronicles.Headless.GraphTypes { - public class NodeStatusType : ObjectGraphType + public class NodeStatusType : ObjectGraphType { +#pragma warning disable SA1313 + public record NodeStatus(bool BootstrapEnded, bool PreloadEnded, bool IsMining); +#pragma warning restore SA1313 + private static readonly string _productVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "Unknown"; @@ -22,13 +27,7 @@ public class NodeStatusType : ObjectGraphType Assembly.GetExecutingAssembly().GetCustomAttribute() ?.InformationalVersion ?? "Unknown"; - public bool BootstrapEnded { get; set; } - - public bool PreloadEnded { get; set; } - - public bool IsMining { get; set; } - - public NodeStatusType(StandaloneContext context) + public NodeStatusType(StandaloneContext context, IBlockChainRepository blockChainRepository) { Field>( name: "bootstrapEnded", @@ -43,9 +42,7 @@ public NodeStatusType(StandaloneContext context) Field>( name: "tip", description: "Block header of the tip block from the current canonical chain.", - resolve: _ => context.BlockChain is { } blockChain - ? BlockHeaderType.FromBlock(blockChain.Tip) - : null + resolve: _ => BlockHeaderType.FromBlock(blockChainRepository.GetTip()) ); Field>>( name: "topmostBlocks", @@ -72,13 +69,8 @@ public NodeStatusType(StandaloneContext context) description: "The topmost blocks from the current node.", resolve: fieldContext => { - if (context.BlockChain is null) - { - throw new InvalidOperationException($"{nameof(context.BlockChain)} is null."); - } - IEnumerable blocks = - GetTopmostBlocks(context.BlockChain, fieldContext.GetArgument("offset")); + blockChainRepository.IterateBlocksDescending(fieldContext.GetArgument("offset")); if (fieldContext.GetArgument("miner") is { } miner) { blocks = blocks.Where(b => b.Miner.Equals(miner)); @@ -136,9 +128,7 @@ public NodeStatusType(StandaloneContext context) name: "genesis", description: "Block header of the genesis block from the current chain.", resolve: fieldContext => - context.BlockChain is { } blockChain - ? BlockHeaderType.FromBlock(blockChain.Genesis) - : null + BlockHeaderType.FromBlock(blockChainRepository.GetBlock(0)) ); Field>( name: "isMining", @@ -173,32 +163,5 @@ context.BlockChain is { } blockChain resolve: _ => _informationalVersion ); } - - private IEnumerable GetTopmostBlocks(BlockChain blockChain, int offset) - { - Block block = blockChain.Tip; - - while (offset > 0) - { - offset--; - if (block.PreviousHash is { } prev) - { - block = blockChain[prev]; - } - } - - while (true) - { - yield return block; - if (block.PreviousHash is { } prev) - { - block = blockChain[prev]; - } - else - { - break; - } - } - } } } diff --git a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs index 8b3ab9dde..cc9e97804 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs @@ -26,10 +26,13 @@ using NineChronicles.Headless.GraphTypes.States; using NineChronicles.Headless.GraphTypes.Diff; using System.Security.Cryptography; -using System.Text; -using Libplanet.Store.Trie; +using Libplanet.KeyStore; +using NineChronicles.Headless.Repositories.BlockChain; +using NineChronicles.Headless.Repositories.StateTrie; +using NineChronicles.Headless.Repositories.Transaction; +using NineChronicles.Headless.Repositories.WorldState; using static NineChronicles.Headless.NCActionUtils; -using Transaction = Libplanet.Types.Tx.Transaction; +using Block = NineChronicles.Headless.Domain.Model.BlockChain.Block; namespace NineChronicles.Headless.GraphTypes { @@ -37,7 +40,7 @@ public class StandaloneQuery : ObjectGraphType { private static readonly ActivitySource ActivitySource = new ActivitySource("NineChronicles.Headless.GraphTypes.StandaloneQuery"); - public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration configuration, ActionEvaluationPublisher publisher, StateMemoryCache stateMemoryCache) + public StandaloneQuery(StandaloneContext standaloneContext, IKeyStore keyStore, IConfiguration configuration, StateMemoryCache stateMemoryCache, IWorldStateRepository worldStateRepository, IBlockChainRepository blockChainRepository, ITransactionRepository transactionRepository, IStateTrieRepository stateTrieRepository) { bool useSecretToken = configuration[GraphQLService.SecretTokenKey] is { }; if (Convert.ToBoolean(configuration.GetSection("Jwt")["EnableJwtAuthentication"])) @@ -59,28 +62,18 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi resolve: context => { using var activity = ActivitySource.StartActivity("stateQuery"); - BlockHash blockHash = (context.GetArgument("hash"), context.GetArgument("index")) switch + Block block = (context.GetArgument("hash"), context.GetArgument("index")) switch { - ({ } bytes, null) => new BlockHash(bytes), - (null, { } index) => standaloneContext.BlockChain[index].Hash, + ({ } bytes, null) => blockChainRepository.GetBlock(new BlockHash(bytes)), + (null, { } index) => blockChainRepository.GetBlock(index), (not null, not null) => throw new ArgumentException("Only one of 'hash' and 'index' must be given."), - (null, null) => standaloneContext.BlockChain.Tip.Hash, + (null, null) => blockChainRepository.GetTip(), }; - activity?.AddTag("BlockHash", blockHash.ToString()); - - if (!(standaloneContext.BlockChain is { } chain)) - { - return null; - } - - if (!(blockHash is { } hash)) - { - return null; - } + activity?.AddTag("BlockHash", block.Hash.ToString()); return new StateContext( - chain.GetWorldState(blockHash), - chain[blockHash].Index, + worldStateRepository.GetWorldState(block.StateRootHash), + block.Index, stateMemoryCache ); } @@ -108,13 +101,6 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi resolve: context => { using var activity = ActivitySource.StartActivity("diffs"); - if (!(standaloneContext.BlockChain is BlockChain blockChain)) - { - throw new ExecutionError( - $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!" - ); - } - var baseIndex = context.GetArgument("baseIndex"); var changedIndex = context.GetArgument("changedIndex"); @@ -126,51 +112,15 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi ); } - var baseBlockStateRootHash = blockChain[baseIndex].StateRootHash.ToString(); - var changedBlockStateRootHash = blockChain[changedIndex].StateRootHash.ToString(); + var baseBlockStateRootHash = blockChainRepository.GetBlock(baseIndex).StateRootHash.ToString(); + var changedBlockStateRootHash = blockChainRepository.GetBlock(changedIndex).StateRootHash.ToString(); var baseStateRootHash = HashDigest.FromString(baseBlockStateRootHash); var targetStateRootHash = HashDigest.FromString( changedBlockStateRootHash ); - var stateStore = standaloneContext.StateStore; - var baseTrieModel = stateStore.GetStateRoot(baseStateRootHash); - var targetTrieModel = stateStore.GetStateRoot(targetStateRootHash); - - IDiffType[] diffs = baseTrieModel - .Diff(targetTrieModel) - .Select(x => - { - if (x.TargetValue is not null) - { - var baseSubTrieModel = stateStore.GetStateRoot(new HashDigest((Binary)x.SourceValue)); - var targetSubTrieModel = stateStore.GetStateRoot(new HashDigest((Binary)x.TargetValue)); - var subDiff = baseSubTrieModel - .Diff(targetSubTrieModel) - .Select(diff => - { - return new StateDiffType.Value( - Encoding.Default.GetString(diff.Path.ByteArray.ToArray()), - diff.SourceValue, - diff.TargetValue); - }).ToArray(); - return (IDiffType)new RootStateDiffType.Value( - Encoding.Default.GetString(x.Path.ByteArray.ToArray()), - subDiff - ); - } - else - { - return new StateDiffType.Value( - Encoding.Default.GetString(x.Path.ByteArray.ToArray()), - x.SourceValue, - x.TargetValue - ); - } - }).ToArray(); - - return diffs; + return stateTrieRepository.CompareStateTrie(baseStateRootHash, targetStateRootHash); } ); @@ -201,13 +151,6 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi resolve: context => { using var activity = ActivitySource.StartActivity("accountDiffs"); - if (!(standaloneContext.BlockChain is BlockChain blockChain)) - { - throw new ExecutionError( - $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!" - ); - } - var baseIndex = context.GetArgument("baseIndex"); var changedIndex = context.GetArgument("changedIndex"); var accountAddress = context.GetArgument
("accountAddress"); @@ -220,39 +163,15 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi ); } - var baseBlockStateRootHash = blockChain[baseIndex].StateRootHash.ToString(); - var changedBlockStateRootHash = blockChain[changedIndex].StateRootHash.ToString(); + var baseBlockStateRootHash = blockChainRepository.GetBlock(baseIndex).StateRootHash.ToString(); + var changedBlockStateRootHash = blockChainRepository.GetBlock(changedIndex).StateRootHash.ToString(); var baseStateRootHash = HashDigest.FromString(baseBlockStateRootHash); var targetStateRootHash = HashDigest.FromString( changedBlockStateRootHash ); - var stateStore = standaloneContext.StateStore; - var baseTrieModel = stateStore.GetStateRoot(baseStateRootHash); - var targetTrieModel = stateStore.GetStateRoot(targetStateRootHash); - - var accountKey = new KeyBytes(ByteUtil.Hex(accountAddress.ByteArray)); - - Binary GetAccountState(ITrie model, KeyBytes key) - { - return model.Get(key) is Binary state ? state : throw new Exception($"Account state not found."); - } - - var baseAccountState = GetAccountState(baseTrieModel, accountKey); - var targetAccountState = GetAccountState(targetTrieModel, accountKey); - - var baseSubTrieModel = stateStore.GetStateRoot(new HashDigest(baseAccountState)); - var targetSubTrieModel = stateStore.GetStateRoot(new HashDigest(targetAccountState)); - - var subDiff = baseSubTrieModel - .Diff(targetSubTrieModel) - .Select(diff => new StateDiffType.Value( - Encoding.Default.GetString(diff.Path.ByteArray.ToArray()), - diff.SourceValue, - diff.TargetValue)) - .ToArray(); - return subDiff; + return stateTrieRepository.CompareStateAccountTrie(baseStateRootHash, targetStateRootHash, accountAddress); } ); @@ -267,29 +186,22 @@ Binary GetAccountState(ITrie model, KeyBytes key) resolve: context => { using var activity = ActivitySource.StartActivity("state"); - if (!(standaloneContext.BlockChain is BlockChain blockChain)) - { - throw new ExecutionError( - $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!"); - } - - var blockHash = (context.GetArgument("hash"), context.GetArgument("index")) switch + var block = (context.GetArgument("hash"), context.GetArgument("index")) switch { (not null, not null) => throw new ArgumentException( "Only one of 'hash' and 'index' must be given."), - (null, { } index) => blockChain[index].Hash, - ({ } bytes, null) => new BlockHash(bytes), - (null, null) => blockChain.Tip.Hash, + (null, { } index) => blockChainRepository.GetBlock(index), + ({ } bytes, null) => blockChainRepository.GetBlock(new BlockHash(bytes)), + (null, null) => blockChainRepository.GetTip(), }; var accountAddress = context.GetArgument
("accountAddress"); var address = context.GetArgument
("address"); activity? - .AddTag("BlockHash", blockHash.ToString()) + .AddTag("BlockHash", block.Hash.ToString()) .AddTag("Address", address.ToString()); - - var state = blockChain - .GetWorldState(blockHash) + var state = worldStateRepository + .GetWorldState(block.StateRootHash) .GetAccountState(accountAddress) .GetState(address); @@ -320,31 +232,15 @@ Binary GetAccountState(ITrie model, KeyBytes key) activity?.AddTag("BlockHash", blockHash.ToString()); - if (!(standaloneContext.Store is { } store)) - { - throw new InvalidOperationException(); - } - - if (!(store.GetBlockDigest(blockHash) is { } digest)) - { - throw new ArgumentException("blockHash"); - } + var block = blockChainRepository.GetBlock(blockHash); var recipient = context.GetArgument("recipient"); - IEnumerable blockTxs = digest.TxIds - .Select(bytes => new TxId(bytes)) - .Select(txid => - { - return store.GetTransaction(txid) ?? - throw new InvalidOperationException($"Transaction {txid} not found."); - }); - - var filtered = blockTxs + var filtered = block.Transactions .Where(tx => tx.Actions.Count == 1) .Select(tx => ( - store.GetTxExecution(blockHash, tx.Id) ?? + transactionRepository.GetTxExecution(blockHash, tx.Id) ?? throw new InvalidOperationException($"TxExecution {tx.Id} not found."), ToAction(tx.Actions[0]) )) @@ -369,7 +265,7 @@ Binary GetAccountState(ITrie model, KeyBytes key) Field( name: "keyStore", deprecationReason: "Use `planet key` command instead. https://www.npmjs.com/package/@planetarium/cli", - resolve: context => standaloneContext.KeyStore + resolve: context => keyStore ).AuthorizeWithLocalPolicyIf(useSecretToken); Field>( @@ -377,31 +273,32 @@ Binary GetAccountState(ITrie model, KeyBytes key) resolve: _ => { using var activity = ActivitySource.StartActivity("nodeStatus"); - return new NodeStatusType(standaloneContext); + return standaloneContext.NodeStatus; }); Field>( name: "chainQuery", deprecationReason: "Use /graphql/explorer", - resolve: context => new { } + resolve: _ => new object() ); Field>( name: "validation", description: "The validation method provider for Libplanet types.", - resolve: context => new ValidationQuery(standaloneContext)); + resolve: _ => new object() + ); Field>( name: "activationStatus", description: "Check if the provided address is activated.", deprecationReason: "Since NCIP-15, it doesn't care account activation.", - resolve: context => new ActivationStatusQuery(standaloneContext)) + resolve: _ => new object()) .AuthorizeWithLocalPolicyIf(useSecretToken); Field>( name: "peerChainState", description: "Get the peer's block chain state", - resolve: context => new PeerChainStateQuery(standaloneContext)); + resolve: _ => new object()); Field>( name: "goldBalance", @@ -412,26 +309,21 @@ Binary GetAccountState(ITrie model, KeyBytes key) resolve: context => { using var activity = ActivitySource.StartActivity("goldBalance"); - if (!(standaloneContext.BlockChain is BlockChain blockChain)) - { - throw new ExecutionError( - $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!"); - } - Address address = context.GetArgument
("address"); byte[] blockHashByteArray = context.GetArgument("hash"); - var blockHash = blockHashByteArray is null - ? blockChain.Tip.Hash - : new BlockHash(blockHashByteArray); + var block = blockHashByteArray is null + ? blockChainRepository.GetTip() + : blockChainRepository.GetBlock(new BlockHash(blockHashByteArray)); + var worldState = worldStateRepository.GetWorldState(block.StateRootHash); Currency currency = new GoldCurrencyState( - (Dictionary)blockChain.GetWorldState(blockHash).GetLegacyState(GoldCurrencyState.Address) + (Dictionary)worldState + .GetLegacyState(GoldCurrencyState.Address) ).Currency; activity? - .AddTag("BlockHash", blockHash.ToString()) + .AddTag("BlockHash", block.Hash.ToString()) .AddTag("Address", address.ToString()); - - return blockChain.GetWorldState(blockHash).GetBalance( + return worldState.GetBalance( address, currency ).GetQuantityString(); @@ -448,15 +340,10 @@ Binary GetAccountState(ITrie model, KeyBytes key) resolve: context => { using var activity = ActivitySource.StartActivity("nextTxNonce"); - if (!(standaloneContext.BlockChain is BlockChain blockChain)) - { - throw new ExecutionError( - $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!"); - } Address address = context.GetArgument
("address"); activity?.AddTag("Address", address.ToString()); - return blockChain.GetNextTxNonce(address); + return transactionRepository.GetNextTxNonce(address); } ); @@ -470,14 +357,8 @@ Binary GetAccountState(ITrie model, KeyBytes key) ), resolve: context => { - if (!(standaloneContext.BlockChain is BlockChain blockChain)) - { - throw new ExecutionError( - $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!"); - } - var txId = context.GetArgument("txId"); - return blockChain.GetTransaction(txId); + return transactionRepository.GetTransaction(txId); } ); @@ -532,13 +413,11 @@ Binary GetAccountState(ITrie model, KeyBytes key) agentAddress = (Address)address; } - - BlockHash offset = blockChain.Tip.Hash; - + HashDigest offset = blockChainRepository.GetTip().StateRootHash; activity? .AddTag("BlockHash", offset.ToString()) .AddTag("Address", address.ToString()); - IWorldState worldState = blockChain.GetWorldState(offset); + IWorldState worldState = worldStateRepository.GetWorldState(offset); #pragma warning disable S3247 if (worldState.GetAgentState(agentAddress) is { } agentState) #pragma warning restore S3247 @@ -580,7 +459,7 @@ Binary GetAccountState(ITrie model, KeyBytes key) resolve: context => { using var activity = ActivitySource.StartActivity("transaction"); - return new TransactionHeadlessQuery(standaloneContext); + return new object(); }); Field>( @@ -600,9 +479,10 @@ Binary GetAccountState(ITrie model, KeyBytes key) $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!"); } + var worldState = worldStateRepository.GetWorldState(blockChainRepository.GetTip().StateRootHash); string invitationCode = context.GetArgument("invitationCode"); ActivationKey activationKey = ActivationKey.Decode(invitationCode); - if (blockChain.GetWorldState().GetLegacyState(activationKey.PendingAddress) is Dictionary dictionary) + if (worldState.GetLegacyState(activationKey.PendingAddress) is Dictionary dictionary) { var pending = new PendingActivationState(dictionary); ActivateAccount action = activationKey.CreateActivateAccount(pending.Nonce); @@ -629,12 +509,6 @@ Binary GetAccountState(ITrie model, KeyBytes key) ), resolve: context => { - if (!(standaloneContext.BlockChain is { } blockChain)) - { - throw new ExecutionError( - $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!"); - } - ActivationKey activationKey; try { @@ -646,7 +520,9 @@ Binary GetAccountState(ITrie model, KeyBytes key) { throw new ExecutionError("invitationCode format is invalid."); } - if (blockChain.GetWorldState().GetLegacyState(activationKey.PendingAddress) is Dictionary dictionary) + + var worldState = worldStateRepository.GetWorldState(blockChainRepository.GetTip().StateRootHash); + if (worldState.GetLegacyState(activationKey.PendingAddress) is Dictionary dictionary) { var pending = new PendingActivationState(dictionary); return ByteUtil.Hex(pending.Nonce); @@ -659,7 +535,7 @@ Binary GetAccountState(ITrie model, KeyBytes key) Field>( name: "rpcInformation", description: "Query for rpc mode information.", - resolve: context => new RpcInformationQuery(publisher) + resolve: _ => new object() ); Field>( @@ -668,7 +544,7 @@ Binary GetAccountState(ITrie model, KeyBytes key) resolve: context => { using var activity = ActivitySource.StartActivity("actionQuery"); - return new ActionQuery(standaloneContext); + return new object(); }); Field>( @@ -698,7 +574,7 @@ Binary GetAccountState(ITrie model, KeyBytes key) resolve: context => { using var activity = ActivitySource.StartActivity("actionTxQuery"); - return new ActionTxQuery(standaloneContext); + return new object(); }); Field>( @@ -707,7 +583,7 @@ Binary GetAccountState(ITrie model, KeyBytes key) resolve: context => { using var activity = ActivitySource.StartActivity("addressQuery"); - return new AddressQuery(standaloneContext); + return new object(); }); } } diff --git a/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs b/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs index af0600912..502ce41c7 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs @@ -152,8 +152,8 @@ public StandaloneSubscription(StandaloneContext standaloneContext, IConfiguratio { Name = "nodeStatus", Type = typeof(NodeStatusType), - Resolver = new FuncFieldResolver(context => (context.Source as NodeStatusType)!), - Subscriber = new EventStreamResolver(context => StandaloneContext.NodeStatusSubject.AsObservable()), + Resolver = new FuncFieldResolver(context => (context.Source as NodeStatusType.NodeStatus)!), + Subscriber = new EventStreamResolver(context => StandaloneContext.NodeStatusSubject.AsObservable()), }); AddField(new EventStreamFieldType { diff --git a/NineChronicles.Headless/HostBuilderExtensions.cs b/NineChronicles.Headless/HostBuilderExtensions.cs index 0767f4bbc..9224d976f 100644 --- a/NineChronicles.Headless/HostBuilderExtensions.cs +++ b/NineChronicles.Headless/HostBuilderExtensions.cs @@ -35,6 +35,7 @@ NineChroniclesNodeService service services.AddSingleton(provider => service.Swarm); services.AddSingleton(provider => service.BlockChain); services.AddSingleton(provider => service.Store); + services.AddSingleton(provider => service.StateStore); services.AddSingleton(provider => Serilog.Log.Logger); services.AddSingleton(provider => service.StateKeyValueStore); diff --git a/NineChronicles.Headless/NineChroniclesNodeService.cs b/NineChronicles.Headless/NineChroniclesNodeService.cs index ceef8bcb4..1133b47ca 100644 --- a/NineChronicles.Headless/NineChroniclesNodeService.cs +++ b/NineChronicles.Headless/NineChroniclesNodeService.cs @@ -54,6 +54,8 @@ public class NineChroniclesNodeService : IHostedService, IDisposable public IStore Store => NodeService.Store; + public IStateStore StateStore => NodeService.StateStore; + public IKeyValueStore StateKeyValueStore => NodeService.StateKeyValueStore; public PrivateKey? MinerPrivateKey { get; set; } diff --git a/NineChronicles.Headless/Repositories/BlockChain/BlockChainRepository.cs b/NineChronicles.Headless/Repositories/BlockChain/BlockChainRepository.cs new file mode 100644 index 000000000..dcf0b5486 --- /dev/null +++ b/NineChronicles.Headless/Repositories/BlockChain/BlockChainRepository.cs @@ -0,0 +1,87 @@ +namespace NineChronicles.Headless.Repositories.BlockChain; + +using System.Collections.Generic; +using Libplanet.Blockchain; +using Libplanet.Types.Blocks; +using Block = NineChronicles.Headless.Domain.Model.BlockChain.Block; +using LibplanetBlock = Libplanet.Types.Blocks.Block; + +public class BlockChainRepository : IBlockChainRepository +{ + private readonly BlockChain _blockChain; + + private static Block Convert(LibplanetBlock block) + { + return new Block( + block.Hash, + block.PreviousHash, + block.Miner, + block.Index, + block.Timestamp, + block.StateRootHash, + block.Transactions + ); + } + + public BlockChainRepository(BlockChain blockChain) + { + _blockChain = blockChain; + } + + public Block GetTip() + { + return FetchTip(); + } + + public Block GetBlock(long index) + { + return FetchBlock(index); + } + + public Block GetBlock(BlockHash blockHash) + { + return FetchBlock(blockHash); + } + + public IEnumerable IterateBlocksDescending(long offset) + { + Block block = FetchTip(); + + while (offset > 0) + { + offset--; + if (block.PreviousHash is { } prev) + { + block = FetchBlock(prev); + } + } + + while (true) + { + yield return block; + if (block.PreviousHash is { } prev) + { + block = FetchBlock(prev); + } + else + { + break; + } + } + } + + private Block FetchTip() + { + return Convert(_blockChain.Tip); + } + + private Block FetchBlock(BlockHash blockHash) + { + return Convert(_blockChain[blockHash]); + } + + private Block FetchBlock(long index) + { + return Convert(_blockChain[index]); + } +} diff --git a/NineChronicles.Headless/Repositories/BlockChain/IBlockChainRepository.cs b/NineChronicles.Headless/Repositories/BlockChain/IBlockChainRepository.cs new file mode 100644 index 000000000..57fa71abf --- /dev/null +++ b/NineChronicles.Headless/Repositories/BlockChain/IBlockChainRepository.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Libplanet.Types.Blocks; + +namespace NineChronicles.Headless.Repositories.BlockChain; + +using Block = NineChronicles.Headless.Domain.Model.BlockChain.Block; + +public interface IBlockChainRepository +{ + Block GetTip(); + Block GetBlock(long index); + Block GetBlock(BlockHash blockHash); + IEnumerable IterateBlocksDescending(long offset); +} diff --git a/NineChronicles.Headless/Repositories/StateTrie/IStateTrieRepository.cs b/NineChronicles.Headless/Repositories/StateTrie/IStateTrieRepository.cs new file mode 100644 index 000000000..da5bdbce9 --- /dev/null +++ b/NineChronicles.Headless/Repositories/StateTrie/IStateTrieRepository.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Security.Cryptography; +using Libplanet.Common; +using Libplanet.Crypto; +using NineChronicles.Headless.GraphTypes.Diff; + +namespace NineChronicles.Headless.Repositories.StateTrie; + +public interface IStateTrieRepository +{ + IEnumerable CompareStateTrie(HashDigest baseStateRootHash, HashDigest targetStateRootHash); + IEnumerable CompareStateAccountTrie(HashDigest baseStateRootHash, HashDigest targetStateRootHash, Address accountAddress); +} diff --git a/NineChronicles.Headless/Repositories/StateTrie/StateTrieRepository.cs b/NineChronicles.Headless/Repositories/StateTrie/StateTrieRepository.cs new file mode 100644 index 000000000..193468362 --- /dev/null +++ b/NineChronicles.Headless/Repositories/StateTrie/StateTrieRepository.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Bencodex.Types; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Store; +using Libplanet.Store.Trie; +using NineChronicles.Headless.GraphTypes.Diff; + +namespace NineChronicles.Headless.Repositories.StateTrie; + +public class StateTrieRepository : IStateTrieRepository +{ + private readonly IStateStore _stateStore; + + public StateTrieRepository(IStateStore stateStore) + { + _stateStore = stateStore; + } + + public IEnumerable CompareStateTrie(HashDigest baseStateRootHash, HashDigest targetStateRootHash) + { + var baseTrieModel = _stateStore.GetStateRoot(baseStateRootHash); + var targetTrieModel = _stateStore.GetStateRoot(targetStateRootHash); + + return baseTrieModel + .Diff(targetTrieModel) + .Select(x => + { + if (x.TargetValue is not null) + { + var baseSubTrieModel = _stateStore.GetStateRoot(new HashDigest((Binary)x.SourceValue)); + var targetSubTrieModel = _stateStore.GetStateRoot(new HashDigest((Binary)x.TargetValue)); + var subDiff = baseSubTrieModel + .Diff(targetSubTrieModel) + .Select(diff => + { + return new StateDiffType.Value( + Encoding.Default.GetString(diff.Path.ByteArray.ToArray()), + diff.SourceValue, + diff.TargetValue); + }).ToArray(); + return (IDiffType)new RootStateDiffType.Value( + Encoding.Default.GetString(x.Path.ByteArray.ToArray()), + subDiff + ); + } + else + { + return new StateDiffType.Value( + Encoding.Default.GetString(x.Path.ByteArray.ToArray()), + x.SourceValue, + x.TargetValue + ); + } + }); + } + + public IEnumerable CompareStateAccountTrie(HashDigest baseStateRootHash, HashDigest targetStateRootHash, Address accountAddress) + { + var baseTrieModel = _stateStore.GetStateRoot(baseStateRootHash); + var targetTrieModel = _stateStore.GetStateRoot(targetStateRootHash); + + var accountKey = new KeyBytes(ByteUtil.Hex(accountAddress.ByteArray)); + + Binary GetAccountState(ITrie model, KeyBytes key) + { + return model.Get(key) is Binary state ? state : throw new Exception($"Account state not found."); + } + + var baseAccountState = GetAccountState(baseTrieModel, accountKey); + var targetAccountState = GetAccountState(targetTrieModel, accountKey); + + var baseSubTrieModel = _stateStore.GetStateRoot(new HashDigest(baseAccountState)); + var targetSubTrieModel = _stateStore.GetStateRoot(new HashDigest(targetAccountState)); + + return baseSubTrieModel + .Diff(targetSubTrieModel) + .Select(diff => new StateDiffType.Value( + Encoding.Default.GetString(diff.Path.ByteArray.ToArray()), + diff.SourceValue, + diff.TargetValue)); + } +} diff --git a/NineChronicles.Headless/Repositories/Transaction/ITransactionRepository.cs b/NineChronicles.Headless/Repositories/Transaction/ITransactionRepository.cs new file mode 100644 index 000000000..59e8cb8ee --- /dev/null +++ b/NineChronicles.Headless/Repositories/Transaction/ITransactionRepository.cs @@ -0,0 +1,12 @@ +namespace NineChronicles.Headless.Repositories.Transaction; + +using Libplanet.Crypto; +using Libplanet.Types.Blocks; +using Libplanet.Types.Tx; + +public interface ITransactionRepository +{ + Transaction? GetTransaction(TxId txId); + TxExecution? GetTxExecution(BlockHash blockHash, TxId txId); + long GetNextTxNonce(Address address); +} diff --git a/NineChronicles.Headless/Repositories/Transaction/TransactionRepository.cs b/NineChronicles.Headless/Repositories/Transaction/TransactionRepository.cs new file mode 100644 index 000000000..8fe828f2e --- /dev/null +++ b/NineChronicles.Headless/Repositories/Transaction/TransactionRepository.cs @@ -0,0 +1,31 @@ +namespace NineChronicles.Headless.Repositories.Transaction; + +using Libplanet.Blockchain; +using Libplanet.Crypto; +using Libplanet.Types.Blocks; +using Libplanet.Types.Tx; + +public class TransactionRepository : ITransactionRepository +{ + private readonly BlockChain _blockChain; + + public TransactionRepository(BlockChain blockChain) + { + _blockChain = blockChain; + } + + public Transaction? GetTransaction(TxId txId) + { + return _blockChain.GetTransaction(txId); + } + + public TxExecution? GetTxExecution(BlockHash blockHash, TxId txId) + { + return _blockChain.GetTxExecution(blockHash, txId); + } + + public long GetNextTxNonce(Address address) + { + return _blockChain.GetNextTxNonce(address); + } +} diff --git a/NineChronicles.Headless/Repositories/WorldState/IWorldStateRepository.cs b/NineChronicles.Headless/Repositories/WorldState/IWorldStateRepository.cs new file mode 100644 index 000000000..e4fe41f86 --- /dev/null +++ b/NineChronicles.Headless/Repositories/WorldState/IWorldStateRepository.cs @@ -0,0 +1,13 @@ +using System.Security.Cryptography; +using Libplanet.Action.State; +using Libplanet.Common; +using Libplanet.Types.Blocks; + +namespace NineChronicles.Headless.Repositories.WorldState; + +public interface IWorldStateRepository +{ + IWorldState GetWorldState(long index); + IWorldState GetWorldState(BlockHash blockHash); + IWorldState GetWorldState(HashDigest stateRootHash); +} diff --git a/NineChronicles.Headless/Repositories/WorldState/WorldStateRepository.cs b/NineChronicles.Headless/Repositories/WorldState/WorldStateRepository.cs new file mode 100644 index 000000000..6cc389a4f --- /dev/null +++ b/NineChronicles.Headless/Repositories/WorldState/WorldStateRepository.cs @@ -0,0 +1,32 @@ +namespace NineChronicles.Headless.Repositories.WorldState; + +using System.Security.Cryptography; +using Libplanet.Action.State; +using Libplanet.Common; +using Libplanet.Types.Blocks; +using Libplanet.Blockchain; + +public class WorldStateRepository : IWorldStateRepository +{ + private readonly BlockChain _blockChain; + + public WorldStateRepository(BlockChain blockChain) + { + _blockChain = blockChain; + } + + public IWorldState GetWorldState(long index) + { + return _blockChain.GetWorldState(_blockChain[index].StateRootHash); + } + + public IWorldState GetWorldState(BlockHash blockHash) + { + return _blockChain.GetWorldState(blockHash); + } + + public IWorldState GetWorldState(HashDigest stateRootHash) + { + return _blockChain.GetWorldState(stateRootHash); + } +} diff --git a/NineChronicles.Headless/StandaloneContext.cs b/NineChronicles.Headless/StandaloneContext.cs index 6ad387e9a..8aa9bca85 100644 --- a/NineChronicles.Headless/StandaloneContext.cs +++ b/NineChronicles.Headless/StandaloneContext.cs @@ -36,7 +36,7 @@ public IKeyStore KeyStore public bool BootstrapEnded { get; set; } public bool PreloadEnded { get; set; } public bool IsMining { get; set; } - public ReplaySubject NodeStatusSubject { get; } = new(1); + public ReplaySubject NodeStatusSubject { get; } = new(1); public ReplaySubject PreloadStateSubject { get; } = new(5); public Subject DifferentAppProtocolVersionEncounterSubject { get; } = @@ -51,12 +51,8 @@ public IKeyStore KeyStore AgentAddresses { get; } = new ConcurrentDictionary>(); - public NodeStatusType NodeStatus => new(this) - { - BootstrapEnded = BootstrapEnded, - PreloadEnded = PreloadEnded, - IsMining = IsMining, - }; + public NodeStatusType.NodeStatus NodeStatus => new( + BootstrapEnded: BootstrapEnded, PreloadEnded: PreloadEnded, IsMining: IsMining); public IStore Store {