diff --git a/.github/workflows/deploy_gh_pages.yml b/.github/workflows/deploy_gh_pages.yml deleted file mode 100644 index 716c6201f..000000000 --- a/.github/workflows/deploy_gh_pages.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Deploy gh pages -on: - push: - branches: - - development -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - name: Set up Node.js - uses: actions/setup-node@v2 - with: - node-version: '14' - - uses: actions/setup-dotnet@v3 - name: Set up .NET Core SDK - with: - dotnet-version: 6.0.x - - name: Build GraphQL Schema - run: | - dotnet restore - dotnet build - dotnet run --project NineChronicles.Headless.Executable -- \ - --graphql-server \ - --graphql-port 30000 \ - --graphql-host localhost \ - --store-path /tmp/store \ - --no-miner \ - --no-cors \ - --skip-preload \ - -H localhost \ - -V "0/CbfC996ad185c61a031f40CeeE80a055e6D83005/MEUCIQCtoZmiFgg5NXW7+5jYMae80lTlj7xO4tQfX9CnvomAtwIgWViM8s.4mYQ89wlGkohmynZ43olDzZLBk.bHShKCVrc=" \ - -G "https://download.nine-chronicles.com/genesis-block-9c-main" & - sleep 180s - wget http://localhost:30000/schema.graphql -O schema.graphql - - name: Cache node modules - uses: actions/cache@v2 - env: - cache-name: cache-node-modules - with: - # npm cache files are stored in `~/.npm` on Linux/macOS - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - name: Install dependencies - run: npm install -g @2fd/graphdoc - - name: Build - run: graphdoc -s schema.graphql -o doc - - name: Copy GraphQL Schema to deploy - run: cp schema.graphql doc - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./doc diff --git a/.github/workflows/update-submodule.yaml b/.github/workflows/update-submodule.yaml new file mode 100644 index 000000000..4f2e7d60f --- /dev/null +++ b/.github/workflows/update-submodule.yaml @@ -0,0 +1,21 @@ +name: update-submodule + +on: + push: + branches: + - rc-v* + - release/* + +jobs: + update-submodule: + if: github.ref_type == 'branch' + runs-on: ubuntu-latest + steps: + - name: Update other repos referring NineChronicles.Headless as submodules + uses: planetarium/submodule-updater@main + with: + token: ${{ secrets.SUBMODULE_UPDATER_GH_TOKEN }} + committer: > + Submodule Updater + targets: | + ${{ github.repository_owner }}/NineChronicles.DataProvider:${{ github.ref_name }}? diff --git a/Lib9c b/Lib9c index 627e12ba3..f76932c20 160000 --- a/Lib9c +++ b/Lib9c @@ -1 +1 @@ -Subproject commit 627e12ba3dd6ec8463398b6e83f609712bd692b2 +Subproject commit f76932c200a7766cedb7e15d4c40e4bdded0c504 diff --git a/Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs b/Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs index 7729b8f58..072ffbd1a 100644 --- a/Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs +++ b/Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs @@ -27,6 +27,7 @@ public void Serialization() null, addresses[1], 0, + 0, false, previousStates, new Random(123), @@ -41,11 +42,12 @@ public void Serialization() Assert.Equal(Null.Value, deserialized.Action); Assert.Equal(123, deserialized.InputContext.Random.Seed); Assert.Equal(0, deserialized.InputContext.BlockIndex); + Assert.Equal(0, deserialized.InputContext.BlockProtocolVersion); Assert.Equal(new[] { "one", "two" }, deserialized.Logs); Assert.Equal(addresses[0], deserialized.InputContext.Signer); Assert.Equal(addresses[1], deserialized.InputContext.Miner); - Assert.Equal(Null.Value, deserialized.OutputStates.GetState(addresses[0])); - Assert.Equal((Text)"foo", deserialized.OutputStates.GetState(addresses[1])); - Assert.Equal(new List((Text)"bar"), deserialized.OutputStates.GetState(addresses[2])); + Assert.Equal(Null.Value, deserialized.OutputState.GetState(addresses[0])); + Assert.Equal((Text)"foo", deserialized.OutputState.GetState(addresses[1])); + Assert.Equal(new List((Text)"bar"), deserialized.OutputState.GetState(addresses[2])); } } diff --git a/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountDelta.cs b/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountDelta.cs new file mode 100644 index 000000000..88f141304 --- /dev/null +++ b/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountDelta.cs @@ -0,0 +1,64 @@ +using System.Collections.Immutable; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Assets; +using Libplanet.Consensus; +using Libplanet.State; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents +{ + public class AccountDelta : IAccountDelta + { + public AccountDelta() + { + States = ImmutableDictionary.Empty; + Fungibles = ImmutableDictionary<(Address, Currency), BigInteger>.Empty; + TotalSupplies = ImmutableDictionary.Empty; + ValidatorSet = null; + } + + public AccountDelta( + IImmutableDictionary statesDelta, + IImmutableDictionary<(Address, Currency), BigInteger> fungiblesDelta, + IImmutableDictionary totalSuppliesDelta, + ValidatorSet? validatorSetDelta) + { + States = statesDelta; + Fungibles = fungiblesDelta; + TotalSupplies = totalSuppliesDelta; + ValidatorSet = validatorSetDelta; + } + + /// + public IImmutableSet
UpdatedAddresses => + StateUpdatedAddresses.Union(FungibleUpdatedAddresses); + + /// + public IImmutableSet
StateUpdatedAddresses => + States.Keys.ToImmutableHashSet(); + + /// + public IImmutableDictionary States { get; } + + /// + public IImmutableSet
FungibleUpdatedAddresses => + Fungibles.Keys.Select(pair => pair.Item1).ToImmutableHashSet(); + + /// + public IImmutableSet<(Address, Currency)> UpdatedFungibleAssets => + Fungibles.Keys.ToImmutableHashSet(); + + /// + public IImmutableDictionary<(Address, Currency), BigInteger> Fungibles { get; } + + /// + public IImmutableSet UpdatedTotalSupplyCurrencies => + TotalSupplies.Keys.ToImmutableHashSet(); + + /// + public IImmutableDictionary TotalSupplies { get; } + + /// + public ValidatorSet? ValidatorSet { get; } + } +} diff --git a/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDelta.cs b/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDelta.cs index 3eca4cc35..ea143bff3 100644 --- a/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDelta.cs +++ b/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDelta.cs @@ -9,29 +9,25 @@ namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; -public class AccountStateDelta : IAccountStateDelta, IValidatorSupportStateDelta +public class AccountStateDelta : IAccountStateDelta { private IImmutableDictionary _states; - private IImmutableDictionary<(Address, Currency), BigInteger> _balances; + private IImmutableDictionary<(Address, Currency), BigInteger> _fungibles; private IImmutableDictionary _totalSupplies; private ValidatorSet? _validatorSet; + private IAccountDelta _delta; - public IImmutableSet
UpdatedAddresses => _states.Keys.ToImmutableHashSet(); + public IImmutableSet
UpdatedAddresses => _delta.UpdatedAddresses; - public IImmutableSet
StateUpdatedAddresses => _states.Keys.ToImmutableHashSet(); + public IImmutableSet
StateUpdatedAddresses => _delta.StateUpdatedAddresses; -#pragma warning disable LAA1002 - public IImmutableDictionary> UpdatedFungibleAssets => - _balances.GroupBy(kv => kv.Key.Item1).ToImmutableDictionary( - g => g.Key, - g => (IImmutableSet)g.Select(kv => kv.Key.Item2).ToImmutableHashSet() - ); + public IImmutableSet<(Address, Currency)> UpdatedFungibleAssets => _delta.UpdatedFungibleAssets; - public IImmutableDictionary> TotalUpdatedFungibleAssets { get; } +#pragma warning disable LAA1002 + public IImmutableSet<(Address, Currency)> TotalUpdatedFungibleAssets { get; } #pragma warning restore LAA1002 - public IImmutableSet TotalSupplyUpdatedCurrencies => - _totalSupplies.Keys.ToImmutableHashSet(); + public IImmutableSet UpdatedTotalSupplyCurrencies => _delta.UpdatedTotalSupplyCurrencies; public AccountStateGetter StateGetter { get; set; } @@ -53,47 +49,73 @@ public AccountStateDelta() public AccountStateDelta( IImmutableDictionary states, - IImmutableDictionary<(Address, Currency), BigInteger> balances, + IImmutableDictionary<(Address, Currency), BigInteger> fungibles, IImmutableDictionary totalSupplies, ValidatorSet? validatorSet ) { + _delta = new AccountDelta( + states, + fungibles, + totalSupplies, + validatorSet); _states = states; - _balances = balances; + _fungibles = fungibles; _totalSupplies = totalSupplies; _validatorSet = validatorSet; } - public AccountStateDelta(Dictionary states, List balances, Dictionary totalSupplies, IValue validatorSet) + public AccountStateDelta(Dictionary states, List fungibles, List totalSupplies, IValue validatorSet) { // This assumes `states` consists of only Binary keys: - _states = states.ToImmutableDictionary( - kv => new Address(kv.Key), - kv => kv.Value - ); - - _balances = balances.Cast().ToImmutableDictionary( - record => (new Address(((Binary)record["address"]).ByteArray), - new Currency((Dictionary)record["currency"])), - record => (BigInteger)(Integer)record["amount"] - ); + _states = states + .ToImmutableDictionary( + kv => new Address(((Binary)kv.Key).ByteArray), + kv => kv.Value); + + _fungibles = fungibles + .Cast() + .Select(dict => + new KeyValuePair<(Address, Currency), BigInteger>( + ( + new Address(((Binary)dict["address"]).ByteArray), + new Currency(dict["currency"]) + ), + new BigInteger((Integer)dict["amount"]) + )) + .ToImmutableDictionary(); // This assumes `totalSupplies` consists of only Binary keys: - _totalSupplies = totalSupplies.ToImmutableDictionary( - kv => new Currency(new Codec().Decode((Binary)kv.Key)), - kv => (BigInteger)(Integer)kv.Value - ); - - _validatorSet = new ValidatorSet(validatorSet); + _totalSupplies = totalSupplies + .Cast() + .Select(dict => + new KeyValuePair( + new Currency(dict["currency"]), + new BigInteger((Integer)dict["amount"]))) + .ToImmutableDictionary(); + + _validatorSet = validatorSet is Null + ? null + : new ValidatorSet(validatorSet); + + _delta = new AccountDelta( + _states, + _fungibles, + _totalSupplies, + _validatorSet); } public AccountStateDelta(IValue serialized) + : this((Dictionary)serialized) + { + } + + public AccountStateDelta(Dictionary dict) : this( - (Dictionary)((Dictionary)serialized)["states"], - (List)((Dictionary)serialized)["balances"], - (Dictionary)((Dictionary)serialized)["totalSupplies"], - ((Dictionary)serialized)["validatorSet"] - ) + (Dictionary)dict["states"], + (List)dict["balances"], + (List)dict["totalSupplies"], + dict["validatorSet"]) { } @@ -102,6 +124,8 @@ public AccountStateDelta(byte[] bytes) { } + public IAccountDelta Delta => _delta; + public IValue? GetState(Address address) => _states.ContainsKey(address) ? _states[address] @@ -111,7 +135,7 @@ public AccountStateDelta(byte[] bytes) addresses.Select(GetState).ToArray(); public IAccountStateDelta SetState(Address address, IValue state) => - new AccountStateDelta(_states.SetItem(address, state), _balances, _totalSupplies, _validatorSet) + new AccountStateDelta(_states.SetItem(address, state), _fungibles, _totalSupplies, _validatorSet) { StateGetter = StateGetter, BalanceGetter = BalanceGetter, @@ -121,7 +145,7 @@ public IAccountStateDelta SetState(Address address, IValue state) => public FungibleAssetValue GetBalance(Address address, Currency currency) { - if (!_balances.TryGetValue((address, currency), out BigInteger rawValue)) + if (!_fungibles.TryGetValue((address, currency), out BigInteger rawValue)) { return BalanceGetter(address, currency); } @@ -149,7 +173,8 @@ public FungibleAssetValue GetTotalSupply(Currency currency) return TotalSupplyGetter(currency); } - public IAccountStateDelta MintAsset(Address recipient, FungibleAssetValue value) + public IAccountStateDelta MintAsset( + IActionContext context, Address recipient, FungibleAssetValue value) { // FIXME: 트랜잭션 서명자를 알아내 currency.AllowsToMint() 확인해서 CurrencyPermissionException // 던지는 처리를 해야하는데 여기서 트랜잭션 서명자를 무슨 수로 가져올지 잘 모르겠음. @@ -176,7 +201,7 @@ public IAccountStateDelta MintAsset(Address recipient, FungibleAssetValue value) return new AccountStateDelta( _states, - _balances.SetItem( + _fungibles.SetItem( (recipient, value.Currency), nextAmount.RawValue ), @@ -193,7 +218,7 @@ public IAccountStateDelta MintAsset(Address recipient, FungibleAssetValue value) return new AccountStateDelta( _states, - _balances.SetItem( + _fungibles.SetItem( (recipient, value.Currency), nextAmount.RawValue ), @@ -209,6 +234,7 @@ public IAccountStateDelta MintAsset(Address recipient, FungibleAssetValue value) } public IAccountStateDelta TransferAsset( + IActionContext context, Address sender, Address recipient, FungibleAssetValue value, @@ -232,7 +258,7 @@ public IAccountStateDelta TransferAsset( Currency currency = value.Currency; FungibleAssetValue senderRemains = senderBalance - value; FungibleAssetValue recipientRemains = GetBalance(recipient, currency) + value; - var balances = _balances + var balances = _fungibles .SetItem((sender, currency), senderRemains.RawValue) .SetItem((recipient, currency), recipientRemains.RawValue); return new AccountStateDelta(_states, balances, _totalSupplies, _validatorSet) @@ -244,7 +270,8 @@ public IAccountStateDelta TransferAsset( }; } - public IAccountStateDelta BurnAsset(Address owner, FungibleAssetValue value) + public IAccountStateDelta BurnAsset( + IActionContext context, Address owner, FungibleAssetValue value) { // FIXME: 트랜잭션 서명자를 알아내 currency.AllowsToMint() 확인해서 CurrencyPermissionException // 던지는 처리를 해야하는데 여기서 트랜잭션 서명자를 무슨 수로 가져올지 잘 모르겠음. @@ -269,7 +296,7 @@ public IAccountStateDelta BurnAsset(Address owner, FungibleAssetValue value) FungibleAssetValue nextValue = balance - value; return new AccountStateDelta( _states, - _balances.SetItem( + _fungibles.SetItem( (owner, currency), nextValue.RawValue ), @@ -297,7 +324,7 @@ public IAccountStateDelta SetValidator(Validator validator) { return new AccountStateDelta( _states, - _balances, + _fungibles, _totalSupplies, GetValidatorSet().Update(validator) ) diff --git a/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDeltaMarshaller.cs b/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDeltaMarshaller.cs index 4605b897b..2e5d8de5f 100644 --- a/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDeltaMarshaller.cs +++ b/Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDeltaMarshaller.cs @@ -16,94 +16,37 @@ public static byte[] Serialize(this IAccountStateDelta value) return Codec.Encode(Marshal(value)); } - public static IEnumerable Marshal(IEnumerable deltas) + public static IEnumerable Marshal(IEnumerable stateDeltas) { - IImmutableDictionary updatedStates = ImmutableDictionary.Empty; - IImmutableDictionary> updatedFungibleAssets = ImmutableDictionary>.Empty; - IImmutableSet totalSupplyUpdatedCurrencies = ImmutableHashSet.Empty; - foreach (var value in deltas) + foreach (var stateDelta in stateDeltas) { - updatedStates = updatedStates.SetItems(value.StateUpdatedAddresses.Select(addr => - new KeyValuePair(addr, value.GetState(addr)))); - updatedFungibleAssets = updatedFungibleAssets.SetItems(value.UpdatedFungibleAssets); - totalSupplyUpdatedCurrencies = totalSupplyUpdatedCurrencies.Union(value.TotalSupplyUpdatedCurrencies); - var state = new Dictionary( - updatedStates.Select(pair => new KeyValuePair((Binary)pair.Key.ByteArray, pair.Value)) - ); - var balance = new Bencodex.Types.List( -#pragma warning disable LAA1002 - updatedFungibleAssets.SelectMany(ua => -#pragma warning restore LAA1002 - ua.Value.Select(c => - { - FungibleAssetValue b = value.GetBalance(ua.Key, c); - return new Bencodex.Types.Dictionary(new[] - { - new KeyValuePair((Text) "address", (Binary) ua.Key.ByteArray), - new KeyValuePair((Text) "currency", c.Serialize()), - new KeyValuePair((Text) "amount", (Integer) b.RawValue), - }); - } - ) - ).Cast() - ); - var totalSupply = new Dictionary( - totalSupplyUpdatedCurrencies.Select(currency => - new KeyValuePair( - (Binary)Codec.Encode(currency.Serialize()), - (Integer)value.GetTotalSupply(currency).RawValue))); - - var bdict = new Dictionary(new[] - { - new KeyValuePair((Text) "states", state), - new KeyValuePair((Text) "balances", balance), - new KeyValuePair((Text) "totalSupplies", totalSupply), - new KeyValuePair((Text) "validatorSet", value.GetValidatorSet().Bencoded), - }); - + var bdict = Marshal(stateDelta); yield return bdict; } } - public static Dictionary Marshal(IAccountStateDelta value) + public static Dictionary Marshal(IAccountStateDelta stateDelta) { - var state = new Dictionary( - value.UpdatedAddresses.Where(addr => value.GetState(addr) is not null).Select(addr => new KeyValuePair( - (Binary)addr.ToByteArray(), - value.GetState(addr) - )) - ); - var balance = new Bencodex.Types.List( -#pragma warning disable LAA1002 - value.UpdatedFungibleAssets.SelectMany(ua => -#pragma warning restore LAA1002 - ua.Value.Select(c => - { - FungibleAssetValue b = value.GetBalance(ua.Key, c); - return new Bencodex.Types.Dictionary(new[] - { - new KeyValuePair((Text) "address", (Binary) ua.Key.ByteArray), - new KeyValuePair((Text) "currency", c.Serialize()), - new KeyValuePair((Text) "amount", (Integer) b.RawValue), - }); - } - ) - ).Cast() - ); - var totalSupply = new Dictionary( - value.TotalSupplyUpdatedCurrencies.Select(currency => - new KeyValuePair( - (Binary)new Codec().Encode(currency.Serialize()), - (Integer)value.GetTotalSupply(currency).RawValue))); - - var bdict = new Dictionary(new[] - { - new KeyValuePair((Text) "states", state), - new KeyValuePair((Text) "balances", balance), - new KeyValuePair((Text) "totalSupplies", totalSupply), - new KeyValuePair((Text) "validatorSet", value.GetValidatorSet().Bencoded), - }); - + var state = new Dictionary(stateDelta.Delta.States.Select( + kv => new KeyValuePair( + new Binary(kv.Key.ByteArray), + kv.Value))); + var balance = new List(stateDelta.Delta.Fungibles.Select( + kv => Dictionary.Empty + .Add("address", new Binary(kv.Key.Item1.ByteArray)) + .Add("currency", kv.Key.Item2.Serialize()) + .Add("amount", new Integer(kv.Value)))); + var totalSupply = new List(stateDelta.Delta.TotalSupplies.Select( + kv => Dictionary.Empty + .Add("currency", kv.Key.Serialize()) + .Add("amount", new Integer(kv.Value)))); + var bdict = Dictionary.Empty + .Add("states", state) + .Add("balances", balance) + .Add("totalSupplies", totalSupply) + .Add("validatorSet", stateDelta.Delta.ValidatorSet is { } validatorSet + ? validatorSet.Bencoded + : Null.Value); return bdict; } diff --git a/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContext.cs b/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContext.cs index 177915120..8624fcc83 100644 --- a/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContext.cs +++ b/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContext.cs @@ -9,8 +9,17 @@ namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; public class ActionContext : IActionContext { - public ActionContext(BlockHash? genesisHash, Address signer, TxId? txId, Address miner, long blockIndex, - bool rehearsal, AccountStateDelta previousStates, IRandom random, HashDigest? previousStateRootHash, + public ActionContext( + BlockHash? genesisHash, + Address signer, + TxId? txId, + Address miner, + long blockIndex, + int blockProtocolVersion, + bool rehearsal, + AccountStateDelta previousState, + IRandom random, + HashDigest? previousStateRootHash, bool blockAction) { GenesisHash = genesisHash; @@ -18,8 +27,9 @@ public ActionContext(BlockHash? genesisHash, Address signer, TxId? txId, Address TxId = txId; Miner = miner; BlockIndex = blockIndex; + BlockProtocolVersion = blockProtocolVersion; Rehearsal = rehearsal; - PreviousStates = previousStates; + PreviousState = previousState; Random = random; PreviousStateRootHash = previousStateRootHash; BlockAction = blockAction; @@ -30,9 +40,10 @@ public ActionContext(BlockHash? genesisHash, Address signer, TxId? txId, Address public TxId? TxId { get; } public Address Miner { get; init; } public long BlockIndex { get; init; } + public int BlockProtocolVersion { get; init; } public bool Rehearsal { get; init; } - public AccountStateDelta PreviousStates { get; init; } - IAccountStateDelta IActionContext.PreviousStates => PreviousStates; + public AccountStateDelta PreviousState { get; init; } + IAccountStateDelta IActionContext.PreviousState => PreviousState; public IRandom Random { get; init; } public HashDigest? PreviousStateRootHash { get; init; } public bool BlockAction { get; init; } @@ -49,8 +60,18 @@ public void UseGas(long gas) public IActionContext GetUnconsumedContext() { - return new ActionContext(GenesisHash, Signer, TxId, Miner, BlockIndex, Rehearsal, PreviousStates, - new Random(Random.Seed), PreviousStateRootHash, BlockAction); + return new ActionContext( + GenesisHash, + Signer, + TxId, + Miner, + BlockIndex, + BlockProtocolVersion, + Rehearsal, + PreviousState, + new Random(Random.Seed), + PreviousStateRootHash, + BlockAction); } public long GasUsed() => 0; diff --git a/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs b/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs index 39b14ba3c..48a030db8 100644 --- a/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs +++ b/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs @@ -24,9 +24,10 @@ public static Dictionary Marshal(this IActionContext actionContext) .Add("miner", actionContext.Miner.ToHex()) .Add("rehearsal", actionContext.Rehearsal) .Add("block_index", actionContext.BlockIndex) + .Add("block_protocol_version", actionContext.BlockProtocolVersion) .Add("random_seed", actionContext.Random.Seed) .Add("signer", actionContext.Signer.ToHex()) - .Add("previous_states", AccountStateDeltaMarshaller.Marshal(actionContext.PreviousStates)); + .Add("previous_states", AccountStateDeltaMarshaller.Marshal(actionContext.PreviousState)); if (actionContext.TxId is { } txId) { @@ -44,6 +45,7 @@ genesisHashValue is Binary genesisHashBinaryValue ? new BlockHash(genesisHashBinaryValue.ByteArray) : null, blockIndex: (Integer)dictionary["block_index"], + blockProtocolVersion: (Integer)dictionary["block_protocol_version"], signer: new Address(((Text)dictionary["signer"]).Value), txId: dictionary.TryGetValue((Text)"tx_id", out IValue txIdValue) && txIdValue is Binary txIdBinaryValue @@ -55,7 +57,7 @@ txIdValue is Binary txIdBinaryValue previousStateRootHash: dictionary.ContainsKey("previous_state_root_hash") ? new HashDigest(((Binary)dictionary["previous_state_root_hash"]).ByteArray) : null, - previousStates: AccountStateDeltaMarshaller.Unmarshal(dictionary["previous_states"]), + previousState: AccountStateDeltaMarshaller.Unmarshal(dictionary["previous_states"]), random: new Random((Integer)dictionary["random_seed"]) ); } diff --git a/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluation.cs b/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluation.cs index 50ef06f57..a2e655376 100644 --- a/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluation.cs +++ b/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluation.cs @@ -9,13 +9,13 @@ public class ActionEvaluation : IActionEvaluation public ActionEvaluation( IValue action, ActionContext inputContext, - AccountStateDelta outputStates, + AccountStateDelta outputState, Exception? exception, List logs) { Action = action; InputContext = inputContext; - OutputStates = outputStates; + OutputState = outputState; Exception = exception; Logs = logs; } @@ -23,8 +23,8 @@ public ActionEvaluation( public IValue Action { get; } public ActionContext InputContext { get; } IActionContext IActionEvaluation.InputContext => InputContext; - public AccountStateDelta OutputStates { get; } - IAccountStateDelta IActionEvaluation.OutputStates => OutputStates; + public AccountStateDelta OutputState { get; } + IAccountStateDelta IActionEvaluation.OutputState => OutputState; public Exception? Exception { get; } public List Logs { get; } } diff --git a/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluationMarshaller.cs b/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluationMarshaller.cs index 8eac9771e..2addd6de8 100644 --- a/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluationMarshaller.cs +++ b/Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluationMarshaller.cs @@ -16,14 +16,14 @@ public static byte[] Serialize(this IActionEvaluation actionEvaluation) public static IEnumerable Marshal(this IEnumerable actionEvaluations) { var actionEvaluationsArray = actionEvaluations.ToArray(); - var outputStates = AccountStateDeltaMarshaller.Marshal(actionEvaluationsArray.Select(aev => aev.OutputStates)); - var previousStates = AccountStateDeltaMarshaller.Marshal(actionEvaluationsArray.Select(aev => aev.InputContext.PreviousStates)); + var outputStates = AccountStateDeltaMarshaller.Marshal(actionEvaluationsArray.Select(aev => aev.OutputState)); + var previousStates = AccountStateDeltaMarshaller.Marshal(actionEvaluationsArray.Select(aev => aev.InputContext.PreviousState)); foreach (var actionEvaluation in actionEvaluationsArray) { yield return Dictionary.Empty .Add("action", actionEvaluation.Action) .Add("logs", new List(actionEvaluation.Logs)) - .Add("output_states", AccountStateDeltaMarshaller.Marshal(actionEvaluation.OutputStates)) + .Add("output_states", AccountStateDeltaMarshaller.Marshal(actionEvaluation.OutputState)) .Add("input_context", ActionContextMarshaller.Marshal(actionEvaluation.InputContext)) .Add("exception", actionEvaluation.Exception?.GetType().FullName is { } typeName ? (Text)typeName : Null.Value); } @@ -34,7 +34,7 @@ public static Dictionary Marshal(this IActionEvaluation actionEvaluation) return Dictionary.Empty .Add("action", actionEvaluation.Action) .Add("logs", new List(actionEvaluation.Logs)) - .Add("output_states", AccountStateDeltaMarshaller.Marshal(actionEvaluation.OutputStates)) + .Add("output_states", AccountStateDeltaMarshaller.Marshal(actionEvaluation.OutputState)) .Add("input_context", ActionContextMarshaller.Marshal(actionEvaluation.InputContext)) .Add("exception", actionEvaluation.Exception?.GetType().FullName is { } typeName ? (Text)typeName : Null.Value); } diff --git a/Libplanet.Extensions.ForkableActionEvaluator.Tests/ForkableActionEvaluatorTest.cs b/Libplanet.Extensions.ForkableActionEvaluator.Tests/ForkableActionEvaluatorTest.cs index 80cce28ac..8cd5f6d62 100644 --- a/Libplanet.Extensions.ForkableActionEvaluator.Tests/ForkableActionEvaluatorTest.cs +++ b/Libplanet.Extensions.ForkableActionEvaluator.Tests/ForkableActionEvaluatorTest.cs @@ -75,6 +75,7 @@ public IReadOnlyList Evaluate(IPreEvaluationBlock block) null, default, 0, + 0, false, new AccountStateDelta(), new Random(0), @@ -102,6 +103,7 @@ public IReadOnlyList Evaluate(IPreEvaluationBlock block) null, default, 0, + 0, false, new AccountStateDelta(), new Random(0), diff --git a/Libplanet.Extensions.RemoteActionEvaluator/RemoteActionEvaluator.cs b/Libplanet.Extensions.RemoteActionEvaluator/RemoteActionEvaluator.cs index e11364aae..803670334 100644 --- a/Libplanet.Extensions.RemoteActionEvaluator/RemoteActionEvaluator.cs +++ b/Libplanet.Extensions.RemoteActionEvaluator/RemoteActionEvaluator.cs @@ -44,33 +44,33 @@ public IReadOnlyList Evaluate(IPreEvaluationBlock block) { if (i > 0) { - actionEvaluations[i].InputContext.PreviousStates.StateGetter = - actionEvaluations[i - 1].OutputStates.GetStates; - actionEvaluations[i].InputContext.PreviousStates.BalanceGetter = - actionEvaluations[i - 1].OutputStates.GetBalance; - actionEvaluations[i].InputContext.PreviousStates.TotalSupplyGetter = - actionEvaluations[i - 1].OutputStates.GetTotalSupply; - actionEvaluations[i].InputContext.PreviousStates.ValidatorSetGetter = - actionEvaluations[i - 1].OutputStates.GetValidatorSet; + actionEvaluations[i].InputContext.PreviousState.StateGetter = + actionEvaluations[i - 1].OutputState.GetStates; + actionEvaluations[i].InputContext.PreviousState.BalanceGetter = + actionEvaluations[i - 1].OutputState.GetBalance; + actionEvaluations[i].InputContext.PreviousState.TotalSupplyGetter = + actionEvaluations[i - 1].OutputState.GetTotalSupply; + actionEvaluations[i].InputContext.PreviousState.ValidatorSetGetter = + actionEvaluations[i - 1].OutputState.GetValidatorSet; } else { ( - actionEvaluations[i].InputContext.PreviousStates.StateGetter, - actionEvaluations[i].InputContext.PreviousStates.BalanceGetter, - actionEvaluations[i].InputContext.PreviousStates.TotalSupplyGetter, - actionEvaluations[i].InputContext.PreviousStates.ValidatorSetGetter + actionEvaluations[i].InputContext.PreviousState.StateGetter, + actionEvaluations[i].InputContext.PreviousState.BalanceGetter, + actionEvaluations[i].InputContext.PreviousState.TotalSupplyGetter, + actionEvaluations[i].InputContext.PreviousState.ValidatorSetGetter ) = InitializeAccountGettersPair(block); } - actionEvaluations[i].OutputStates.StateGetter = - actionEvaluations[i].InputContext.PreviousStates.GetStates; - actionEvaluations[i].OutputStates.BalanceGetter = - actionEvaluations[i].InputContext.PreviousStates.GetBalance; - actionEvaluations[i].OutputStates.TotalSupplyGetter = - actionEvaluations[i].InputContext.PreviousStates.GetTotalSupply; - actionEvaluations[i].OutputStates.ValidatorSetGetter = - actionEvaluations[i].InputContext.PreviousStates.GetValidatorSet; + actionEvaluations[i].OutputState.StateGetter = + actionEvaluations[i].InputContext.PreviousState.GetStates; + actionEvaluations[i].OutputState.BalanceGetter = + actionEvaluations[i].InputContext.PreviousState.GetBalance; + actionEvaluations[i].OutputState.TotalSupplyGetter = + actionEvaluations[i].InputContext.PreviousState.GetTotalSupply; + actionEvaluations[i].OutputState.ValidatorSetGetter = + actionEvaluations[i].InputContext.PreviousState.GetValidatorSet; } return actionEvaluations; diff --git a/Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockChainStates.cs b/Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockChainStates.cs index 618c535cc..4afd7be39 100644 --- a/Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockChainStates.cs +++ b/Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockChainStates.cs @@ -24,6 +24,9 @@ public RemoteBlockChainStates(Uri explorerEndpoint) new GraphQLHttpClient(_explorerEndpoint, new SystemTextJsonSerializer()); } + public IValue? GetState(Address address, BlockHash? offset) => + GetStates(new[] { address }, offset).First(); + public IReadOnlyList GetStates(IReadOnlyList
addresses, BlockHash? offset) { var response = _graphQlHttpClient.SendQueryAsync( @@ -160,7 +163,7 @@ public ValidatorSet GetValidatorSet(BlockHash? offset) .ToList()); } - public IBlockStates GetBlockStates(BlockHash? offset) + public IBlockState GetBlockState(BlockHash? offset) { throw new NotSupportedException(); } diff --git a/Libplanet.Headless.Tests/Hosting/LibplanetNodeServiceTest.cs b/Libplanet.Headless.Tests/Hosting/LibplanetNodeServiceTest.cs index 22c117fee..a8c644e39 100644 --- a/Libplanet.Headless.Tests/Hosting/LibplanetNodeServiceTest.cs +++ b/Libplanet.Headless.Tests/Hosting/LibplanetNodeServiceTest.cs @@ -91,7 +91,7 @@ private class DummyAction : IAction IAccountStateDelta IAction.Execute(IActionContext context) { - return context.PreviousStates; + return context.PreviousState; } void IAction.LoadPlainValue(IValue plainValue) diff --git a/NineChronicles.Headless.Executable.Tests/Commands/ChainCommandTest.cs b/NineChronicles.Headless.Executable.Tests/Commands/ChainCommandTest.cs index e9ae26180..f3ecc3591 100644 --- a/NineChronicles.Headless.Executable.Tests/Commands/ChainCommandTest.cs +++ b/NineChronicles.Headless.Executable.Tests/Commands/ChainCommandTest.cs @@ -9,12 +9,10 @@ using Bencodex.Types; using Libplanet; using Libplanet.Action; -using Libplanet.Action.Loader; using Libplanet.Action.Sys; using Libplanet.Blockchain; using Libplanet.Blockchain.Policies; using Libplanet.Blocks; -using Libplanet.Extensions.Cocona; using Libplanet.Consensus; using Libplanet.Crypto; using Libplanet.RocksDBStore; @@ -33,6 +31,7 @@ using Serilog.Core; using Xunit; using Lib9cUtils = Lib9c.DevExtensions.Utils; +using CoconaUtils = Libplanet.Extensions.Cocona.Utils; namespace NineChronicles.Headless.Executable.Tests.Commands { @@ -78,7 +77,7 @@ public void Tip(StoreType storeType) _command.Tip(storeType, _storePath); Assert.Equal( - Utils.SerializeHumanReadable(genesisBlock.Header), + CoconaUtils.SerializeHumanReadable(genesisBlock.Header), _console.Out.ToString().Trim() ); } diff --git a/NineChronicles.Headless.Executable.sln.DotSettings b/NineChronicles.Headless.Executable.sln.DotSettings index 52e48e018..61ddddd89 100644 --- a/NineChronicles.Headless.Executable.sln.DotSettings +++ b/NineChronicles.Headless.Executable.sln.DotSettings @@ -1,2 +1,3 @@  + NCG True \ No newline at end of file diff --git a/NineChronicles.Headless.Executable/Commands/AccountCommand.cs b/NineChronicles.Headless.Executable/Commands/AccountCommand.cs index 99e912059..3c6ca5f2f 100644 --- a/NineChronicles.Headless.Executable/Commands/AccountCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/AccountCommand.cs @@ -3,7 +3,6 @@ using System.Linq; using Bencodex; using Cocona; -using Lib9c.DevExtensions; using Libplanet; using Libplanet.Assets; using Libplanet.Blockchain; @@ -15,6 +14,7 @@ using NineChronicles.Headless.Executable.IO; using Serilog.Core; using static NineChronicles.Headless.NCActionUtils; +using DevExUtils = Lib9c.DevExtensions.Utils; namespace NineChronicles.Headless.Executable.Commands { @@ -45,11 +45,11 @@ public void Balance( string? address = null ) { - using Logger logger = Utils.ConfigureLogger(verbose); + using Logger logger = DevExUtils.ConfigureLogger(verbose); (BlockChain chain, IStore store, _, _) = - Utils.GetBlockChain(logger, storePath, chainId); + DevExUtils.GetBlockChain(logger, storePath, chainId); - Block offset = Utils.ParseBlockOffset(chain, block); + Block offset = DevExUtils.ParseBlockOffset(chain, block); _console.Error.WriteLine("The offset block: #{0} {1}.", offset.Index, offset.Hash); Bencodex.Types.Dictionary goldCurrencyStateDict = (Bencodex.Types.Dictionary) @@ -59,7 +59,7 @@ public void Balance( if (address is { } addrStr) { - Address addr = Utils.ParseAddress(addrStr); + Address addr = DevExUtils.ParseAddress(addrStr); FungibleAssetValue balance = chain.GetBalance(addr, gold, offset.Hash); _console.Out.WriteLine("{0}\t{1}", addr, balance); return; diff --git a/NineChronicles.Headless.Executable/Commands/ChainCommand.cs b/NineChronicles.Headless.Executable/Commands/ChainCommand.cs index 80fa864ea..87ea951e8 100644 --- a/NineChronicles.Headless.Executable/Commands/ChainCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/ChainCommand.cs @@ -10,11 +10,9 @@ using Cocona.Help; using Libplanet; using Libplanet.Action; -using Libplanet.Action.Loader; using Libplanet.Blockchain; using Libplanet.Blockchain.Policies; using Libplanet.Blocks; -using Libplanet.Extensions.Cocona; using Libplanet.RocksDBStore; using Libplanet.Store; using Libplanet.Store.Trie; @@ -26,6 +24,7 @@ using NineChronicles.Headless.Executable.Store; using Serilog.Core; using static NineChronicles.Headless.NCActionUtils; +using CoconaUtils = Libplanet.Extensions.Cocona.Utils; namespace NineChronicles.Headless.Executable.Commands { @@ -81,7 +80,7 @@ public void Tip( BlockHash tipHash = store.IndexBlockHash(chainId, -1) ?? throw new CommandExitedException("The given chain seems empty.", -1); Block tip = store.GetBlock(tipHash); - _console.Out.WriteLine(Utils.SerializeHumanReadable(tip.Header)); + _console.Out.WriteLine(CoconaUtils.SerializeHumanReadable(tip.Header)); store.Dispose(); } diff --git a/NineChronicles.Headless.Executable/Commands/GenesisCommand.cs b/NineChronicles.Headless.Executable/Commands/GenesisCommand.cs index 7eac9bc1d..2294f7638 100644 --- a/NineChronicles.Headless.Executable/Commands/GenesisCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/GenesisCommand.cs @@ -7,16 +7,16 @@ using Bencodex; using Bencodex.Types; using Cocona; +using Lib9c; using Libplanet; -using Libplanet.Action; +using Libplanet.Assets; using Libplanet.Blocks; using Libplanet.Crypto; -using Libplanet.Extensions.Cocona; using Nekoyume; using Nekoyume.Action; using Nekoyume.Model.State; using NineChronicles.Headless.Executable.IO; -using Serilog; +using CoconaUtils = Libplanet.Extensions.Cocona.Utils; using Lib9cUtils = Lib9c.DevExtensions.Utils; namespace NineChronicles.Headless.Executable.Commands @@ -37,7 +37,7 @@ private void ProcessData(DataConfig config, out Dictionary table _console.Out.WriteLine("\nProcessing data for genesis..."); if (string.IsNullOrEmpty(config.TablePath)) { - throw Utils.Error("TablePath is not set."); + throw CoconaUtils.Error("TablePath is not set."); } tableSheets = Lib9cUtils.ImportSheets(config.TablePath); @@ -94,12 +94,15 @@ out List initialDepositList } } - private void ProcessAdmin(AdminConfig? config, PrivateKey initialMinter, out AdminState adminState) + private void ProcessAdmin(AdminConfig? config, PrivateKey initialMinter, + out AdminState adminState, out List meadActions) { // FIXME: If the `adminState` is not required inside `MineGenesisBlock`, // this logic will be much lighter. _console.Out.WriteLine("\nProcessing admin for genesis..."); adminState = new AdminState(new Address(), 0); + meadActions = new List(); + if (config is null) { _console.Out.WriteLine("AdminConfig not provided. Skip admin setting..."); @@ -112,10 +115,26 @@ private void ProcessAdmin(AdminConfig? config, PrivateKey initialMinter, out Adm { _console.Out.WriteLine("Admin address not provided. Give admin privilege to initialMinter"); adminState = new AdminState(initialMinter.ToAddress(), config.Value.ValidUntil); + meadActions.Add(new PrepareRewardAssets + { + RewardPoolAddress = initialMinter.ToAddress(), + Assets = new List + { + 10000 * Currencies.Mead, + }, + }); } else { adminState = new AdminState(new Address(config.Value.Address), config.Value.ValidUntil); + meadActions.Add(new PrepareRewardAssets + { + RewardPoolAddress = new Address(config.Value.Address), + Assets = new List + { + 10000 * Currencies.Mead, + }, + }); } } else @@ -197,12 +216,11 @@ public void Mine( ProcessCurrency(genesisConfig.Currency, out var initialMinter, out var initialDepositList); - ProcessAdmin(genesisConfig.Admin, initialMinter, out var adminState); + ProcessAdmin(genesisConfig.Admin, initialMinter, out var adminState, out var meadActions); ProcessValidator(genesisConfig.InitialValidatorSet, initialMinter, out var initialValidatorSet); - ProcessExtra(genesisConfig.Extra, - out var pendingActivationStates); + ProcessExtra(genesisConfig.Extra, out var pendingActivationStates); // Mine genesis block _console.Out.WriteLine("\nMining genesis block...\n"); @@ -214,7 +232,8 @@ public void Mine( privateKey: initialMinter, initialValidators: initialValidatorSet.ToDictionary( item => new PublicKey(ByteUtil.ParseHex(item.PublicKey)), - item => new BigInteger(item.Power)) + item => new BigInteger(item.Power)), + actionBases: meadActions ); Lib9cUtils.ExportBlock(block, "genesis-block"); @@ -254,7 +273,7 @@ public void Mine( } catch (Exception e) { - throw Utils.Error(e.Message); + throw CoconaUtils.Error(e.Message); } } diff --git a/NineChronicles.Headless.Executable/Commands/MarketCommand.cs b/NineChronicles.Headless.Executable/Commands/MarketCommand.cs index 13d348a99..2cfb88f51 100644 --- a/NineChronicles.Headless.Executable/Commands/MarketCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/MarketCommand.cs @@ -5,7 +5,6 @@ using Bencodex; using Bencodex.Types; using Cocona; -using Lib9c.DevExtensions; using Lib9c.Model.Order; using Libplanet; using Libplanet.Assets; @@ -18,6 +17,7 @@ using NineChronicles.Headless.Executable.IO; using Serilog.Core; using static NineChronicles.Headless.NCActionUtils; +using DevExUtils = Lib9c.DevExtensions.Utils; namespace NineChronicles.Headless.Executable.Commands { @@ -59,10 +59,10 @@ public void Query( Guid? chainId = null ) { - using Logger logger = Utils.ConfigureLogger(verbose); + using Logger logger = DevExUtils.ConfigureLogger(verbose); TextWriter stderr = _console.Error; (BlockChain chain, IStore store, _, _) = - Utils.GetBlockChain(logger, storePath, chainId); + DevExUtils.GetBlockChain(logger, storePath, chainId); HashSet? itemTypes = null; if (itemType is { } t) @@ -79,9 +79,9 @@ public void Query( } } - Block start = Utils.ParseBlockOffset(chain, from, defaultIndex: 0); + Block start = DevExUtils.ParseBlockOffset(chain, from, defaultIndex: 0); stderr.WriteLine("The bottom block to search: #{0} {1}.", start.Index, start.Hash); - Block end = Utils.ParseBlockOffset(chain, to); + Block end = DevExUtils.ParseBlockOffset(chain, to); stderr.WriteLine("The topmost block to search: #{0} {1}.", end.Index, end.Hash); Block block = end; diff --git a/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs b/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs index 77062903f..3c55116df 100644 --- a/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs +++ b/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.Contracts; using System.Linq; using System.Numerics; using System.Security.Cryptography; @@ -22,123 +24,134 @@ namespace NineChronicles.Headless.Executable.Commands public partial class ReplayCommand : CoconaLiteConsoleAppBase { /// - /// Almost duplicate https://github.com/planetarium/libplanet/blob/main/Libplanet/Action/AccountStateDeltaImpl.cs. + /// Almost duplicate https://github.com/planetarium/libplanet/blob/main/Libplanet/State/AccountStateDelta.cs. /// - private class AccountStateDeltaImpl : IAccountStateDelta + [Pure] + private sealed class AccountStateDelta : IAccountStateDelta { - public IImmutableSet
UpdatedAddresses => - UpdatedStates.Keys.ToImmutableHashSet() - .Union(UpdatedFungibles - .Select(kv => kv.Key.Item1)); - - public IImmutableSet
StateUpdatedAddresses => - UpdatedStates.Keys.ToImmutableHashSet(); - - public IImmutableDictionary> - UpdatedFungibleAssets => - UpdatedFungibles - .GroupBy(kv => kv.Key.Item1) - .ToImmutableDictionary( - g => g.Key, - g => - (IImmutableSet)g - .Select(kv => kv.Key.Item2) - .ToImmutableHashSet()); + /// + /// Creates a null state delta from the given . + /// + /// A view to the “epoch” states. + /// A view to the “epoch” asset balances. + /// + /// A view to the “epoch” total supplies of + /// currencies. + /// A view to the “epoch” validator + /// set. + private AccountStateDelta( + AccountStateGetter accountStateGetter, + AccountBalanceGetter accountBalanceGetter, + TotalSupplyGetter totalSupplyGetter, + ValidatorSetGetter validatorSetGetter) + { + Delta = new AccountDelta(); + StateGetter = accountStateGetter; + BalanceGetter = accountBalanceGetter; + TotalSupplyGetter = totalSupplyGetter; + ValidatorSetGetter = validatorSetGetter; + TotalUpdatedFungibles = ImmutableDictionary<(Address, Currency), BigInteger>.Empty; + } - public IImmutableDictionary> TotalUpdatedFungibleAssets { get; } = new Dictionary>().ToImmutableDictionary(); + /// + public IAccountDelta Delta { get; private set; } - public IImmutableSet TotalSupplyUpdatedCurrencies => - UpdatedTotalSupply.Keys.ToImmutableHashSet(); + /// + [Pure] + public IImmutableSet
UpdatedAddresses => + Delta.UpdatedAddresses; - protected AccountStateGetter StateGetter { get; set; } + /// + public IImmutableSet
StateUpdatedAddresses => + Delta.StateUpdatedAddresses; - protected AccountBalanceGetter BalanceGetter { get; set; } + /// + public IImmutableSet<(Address, Currency)> UpdatedFungibleAssets => + Delta.UpdatedFungibleAssets; - protected TotalSupplyGetter TotalSupplyGetter { get; set; } + /// + public IImmutableSet<(Address, Currency)> TotalUpdatedFungibleAssets => + TotalUpdatedFungibles.Keys.ToImmutableHashSet(); - protected ValidatorSetGetter ValidatorSetGetter { get; set; } + [Pure] + public IImmutableSet UpdatedTotalSupplyCurrencies => + Delta.UpdatedTotalSupplyCurrencies; - protected IImmutableDictionary UpdatedStates { get; set; } + public IImmutableDictionary<(Address, Currency), BigInteger> TotalUpdatedFungibles + { get; private set; } - protected IImmutableDictionary<(Address, Currency), BigInteger> UpdatedFungibles { get; set; } + private AccountStateGetter StateGetter { get; set; } - protected IImmutableDictionary UpdatedTotalSupply { get; set; } + private AccountBalanceGetter BalanceGetter { get; set; } - protected ValidatorSet? UpdatedValidatorSet { get; set; } = null; + private TotalSupplyGetter TotalSupplyGetter { get; set; } - protected Address Signer { get; set; } + private ValidatorSetGetter ValidatorSetGetter { get; set; } - public AccountStateDeltaImpl( - AccountStateGetter accountStateGetter, - AccountBalanceGetter accountBalanceGetter, - TotalSupplyGetter totalSupplyGetter, - ValidatorSetGetter validatorSetGetter, - Address signer) + /// + [Pure] + public IValue? GetState(Address address) { - StateGetter = accountStateGetter; - BalanceGetter = accountBalanceGetter; - TotalSupplyGetter = totalSupplyGetter; - ValidatorSetGetter = validatorSetGetter; - UpdatedStates = ImmutableDictionary.Empty; - UpdatedFungibles = ImmutableDictionary<(Address, Currency), BigInteger>.Empty; - UpdatedTotalSupply = ImmutableDictionary.Empty; - Signer = signer; + IValue? state = GetStates(new[] { address })[0]; + return state; } - public IValue? GetState(Address address) => - UpdatedStates.TryGetValue(address, out IValue? value) - ? value - : StateGetter(new[] { address })[0]; - + /// + [Pure] public IReadOnlyList GetStates(IReadOnlyList
addresses) { int length = addresses.Count; IValue?[] values = new IValue?[length]; - var notFound = new List
(length); + var notFoundIndices = new List(length); for (int i = 0; i < length; i++) { Address address = addresses[i]; - if (UpdatedStates.TryGetValue(address, out IValue? v)) + if (Delta.States.TryGetValue(address, out IValue? updatedValue)) { - values[i] = v; - continue; + values[i] = updatedValue; + } + else + { + notFoundIndices.Add(i); } - - notFound.Add(address); } - IReadOnlyList restValues = StateGetter(notFound); - for (int i = 0, j = 0; i < length && j < notFound.Count; i++) + if (notFoundIndices.Count > 0) { - if (addresses[i].Equals(notFound[j])) + IReadOnlyList restValues = StateGetter( + notFoundIndices.Select(index => addresses[index]).ToArray()); + foreach ((var v, var i) in notFoundIndices.Select((v, i) => (v, i))) { - values[i] = restValues[j]; - j++; + values[v] = restValues[i]; } } return values; } - public FungibleAssetValue GetBalance( - Address address, - Currency currency) => - GetBalance(address, currency, UpdatedFungibles); + /// + [Pure] + public IAccountStateDelta SetState(Address address, IValue state) => + UpdateStates(Delta.States.SetItem(address, state)); + /// + [Pure] + public FungibleAssetValue GetBalance(Address address, Currency currency) => + GetBalance(address, currency, Delta.Fungibles); + + /// + [Pure] public FungibleAssetValue GetTotalSupply(Currency currency) { if (!currency.TotalSupplyTrackable) { - // throw TotalSupplyNotTrackableException.WithDefaultMessage(currency); - var msg = - $"The total supply value of the currency {currency} is not trackable because it" - + " is a legacy untracked currency which might have been established before" - + " the introduction of total supply tracking support."; - throw new TotalSupplyNotTrackableException(msg, currency); + throw new TotalSupplyNotTrackableException( + $"Given currency {currency} is not trackable for its total supply", + currency); } // Return dirty state if it exists. - if (UpdatedTotalSupply.TryGetValue(currency, out BigInteger totalSupplyValue)) + if (Delta.TotalSupplies.TryGetValue(currency, out BigInteger totalSupplyValue)) { return FungibleAssetValue.FromRawValue(currency, totalSupplyValue); } @@ -146,10 +159,15 @@ public FungibleAssetValue GetTotalSupply(Currency currency) return TotalSupplyGetter(currency); } - public IAccountStateDelta SetState(Address address, IValue state) => - UpdateStates(UpdatedStates.SetItem(address, state)); + /// + [Pure] + public ValidatorSet GetValidatorSet() => + Delta.ValidatorSet ?? ValidatorSetGetter(); - public IAccountStateDelta MintAsset(Address recipient, FungibleAssetValue value) + /// + [Pure] + public IAccountStateDelta MintAsset( + IActionContext context, Address recipient, FungibleAssetValue value) { if (value.Sign <= 0) { @@ -160,16 +178,18 @@ public IAccountStateDelta MintAsset(Address recipient, FungibleAssetValue value) } Currency currency = value.Currency; - if (!currency.AllowsToMint(Signer)) + if (!currency.AllowsToMint(context.Signer)) { throw new CurrencyPermissionException( - $"The account {Signer} has no permission to mint the currency {currency}.", - Signer, + $"The account {context.Signer} has no permission to mint currency {currency}.", + context.Signer, currency ); } FungibleAssetValue balance = GetBalance(recipient, currency); + (Address, Currency) assetKey = (recipient, currency); + BigInteger rawBalance = (balance + value).RawValue; if (currency.TotalSupplyTrackable) { @@ -177,59 +197,39 @@ public IAccountStateDelta MintAsset(Address recipient, FungibleAssetValue value) if (currency.MaximumSupply < currentTotalSupply + value) { var msg = $"The amount {value} attempted to be minted added to the current" - + $" total supply of {currentTotalSupply} exceeds the" - + $" maximum allowed supply of {currency.MaximumSupply}."; + + $" total supply of {currentTotalSupply} exceeds the" + + $" maximum allowed supply of {currency.MaximumSupply}."; throw new SupplyOverflowException(msg, value); } return UpdateFungibleAssets( - UpdatedFungibles.SetItem((recipient, currency), (balance + value).RawValue), - UpdatedTotalSupply.SetItem(currency, (currentTotalSupply + value).RawValue) - ); - } - - return UpdateFungibleAssets( - UpdatedFungibles.SetItem((recipient, currency), (balance + value).RawValue) - ); - } - - public IAccountStateDelta TransferAsset(Address sender, Address recipient, FungibleAssetValue value, - bool allowNegativeBalance = false) - { - if (value.Sign <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(value), - "The value to transfer has to be greater than zero." + Delta.Fungibles.SetItem(assetKey, rawBalance), + TotalUpdatedFungibles.SetItem(assetKey, rawBalance), + Delta.TotalSupplies.SetItem(currency, (currentTotalSupply + value).RawValue) ); } - Currency currency = value.Currency; - FungibleAssetValue senderBalance = GetBalance(sender, currency); - - if (!allowNegativeBalance && senderBalance < value) - { - var msg = $"The account {sender}'s balance of {currency} is insufficient to " + - $"transfer: {senderBalance} < {value}."; - throw new InsufficientBalanceException(msg, sender, senderBalance); - } - - IImmutableDictionary<(Address, Currency), BigInteger> updatedFungibleAssets = - UpdatedFungibles - .SetItem((sender, currency), (senderBalance - value).RawValue); - - FungibleAssetValue recipientBalance = GetBalance( - recipient, - currency, - updatedFungibleAssets); - return UpdateFungibleAssets( - updatedFungibleAssets - .SetItem((recipient, currency), (recipientBalance + value).RawValue) + Delta.Fungibles.SetItem(assetKey, rawBalance), + TotalUpdatedFungibles.SetItem(assetKey, rawBalance) ); } - public IAccountStateDelta BurnAsset(Address owner, FungibleAssetValue value) + /// + [Pure] + public IAccountStateDelta TransferAsset( + IActionContext context, + Address sender, + Address recipient, + FungibleAssetValue value, + bool allowNegativeBalance = false) => context.BlockProtocolVersion > 0 + ? TransferAssetV1(sender, recipient, value, allowNegativeBalance) + : TransferAssetV0(sender, recipient, value, allowNegativeBalance); + + /// + [Pure] + public IAccountStateDelta BurnAsset( + IActionContext context, Address owner, FungibleAssetValue value) { string msg; @@ -242,11 +242,11 @@ public IAccountStateDelta BurnAsset(Address owner, FungibleAssetValue value) } Currency currency = value.Currency; - if (!currency.AllowsToMint(Signer)) + if (!currency.AllowsToMint(context.Signer)) { - msg = $"The account {Signer} has no permission to burn assets of " + - $"the currency {currency}."; - throw new CurrencyPermissionException(msg, Signer, currency); + msg = $"The account {context.Signer} has no permission to burn assets of " + + $"the currency {currency}."; + throw new CurrencyPermissionException(msg, context.Signer, currency); } FungibleAssetValue balance = GetBalance(owner, currency); @@ -254,70 +254,89 @@ public IAccountStateDelta BurnAsset(Address owner, FungibleAssetValue value) if (balance < value) { msg = $"The account {owner}'s balance of {currency} is insufficient to burn: " + - $"{balance} < {value}."; + $"{balance} < {value}."; throw new InsufficientBalanceException(msg, owner, balance); } + (Address, Currency) assetKey = (owner, currency); + BigInteger rawBalance = (balance - value).RawValue; if (currency.TotalSupplyTrackable) { return UpdateFungibleAssets( - UpdatedFungibles.SetItem((owner, currency), (balance - value).RawValue), - UpdatedTotalSupply.SetItem( + Delta.Fungibles.SetItem(assetKey, rawBalance), + TotalUpdatedFungibles.SetItem(assetKey, rawBalance), + Delta.TotalSupplies.SetItem( currency, (GetTotalSupply(currency) - value).RawValue) ); } return UpdateFungibleAssets( - UpdatedFungibles.SetItem((owner, currency), (balance - value).RawValue) + Delta.Fungibles.SetItem(assetKey, rawBalance), + TotalUpdatedFungibles.SetItem(assetKey, rawBalance) ); } - public IImmutableDictionary GetUpdatedStates() => - StateUpdatedAddresses.Select(address => - new KeyValuePair( - address, - GetState(address) - ) - ).ToImmutableDictionary(); - - public IImmutableDictionary<(Address, Currency), FungibleAssetValue> - GetUpdatedBalances() => - UpdatedFungibleAssets.SelectMany(kv => - kv.Value.Select(currency => - new KeyValuePair<(Address, Currency), FungibleAssetValue>( - (kv.Key, currency), - GetBalance(kv.Key, currency) - ) - ) - ).ToImmutableDictionary(); - - public IImmutableDictionary - GetUpdatedTotalSupplies() => - TotalSupplyUpdatedCurrencies.Select(currency => - new KeyValuePair( - currency, - GetTotalSupply(currency))) - .ToImmutableDictionary(); - - public IImmutableDictionary GetUpdatedRawStates() => - GetUpdatedStates() - .Select(pair => - new KeyValuePair( - ToStateKey(pair.Key), - pair.Value)) - .Union( - GetUpdatedBalances().Select(pair => - new KeyValuePair( - ToFungibleAssetKey(pair.Key), - (Integer)pair.Value.RawValue))) - .Union( - GetUpdatedTotalSupplies().Select(pair => - new KeyValuePair( - ToTotalSupplyKey(pair.Key), - (Integer)pair.Value.RawValue))).ToImmutableDictionary(); - - protected virtual FungibleAssetValue GetBalance( + /// + [Pure] + public IAccountStateDelta SetValidator(Validator validator) + { + return UpdateValidatorSet(GetValidatorSet().Update(validator)); + } + + /// + /// Creates a null state delta from given . + /// + /// The previous to use as + /// a basis. + /// A null state delta created from . + /// + internal static IAccountStateDelta Create(IAccountState previousState) + { + return new AccountStateDelta( + previousState.GetStates, + previousState.GetBalance, + previousState.GetTotalSupply, + previousState.GetValidatorSet); + } + + /// + /// Creates a null state delta while inheriting s + /// total updated fungibles. + /// + /// The previous to use. + /// A null state delta that is of the same type as . + /// + /// Thrown if given + /// is not . + /// + /// + /// This inherits 's + /// . + /// + internal static IAccountStateDelta Flush( + IAccountStateDelta stateDelta) + { + if (stateDelta is AccountStateDelta impl) + { + return new AccountStateDelta( + stateDelta.GetStates, + stateDelta.GetBalance, + stateDelta.GetTotalSupply, + stateDelta.GetValidatorSet) + { + TotalUpdatedFungibles = impl.TotalUpdatedFungibles, + }; + } + else + { + throw new ArgumentException( + $"Unknown type for {nameof(stateDelta)}: {stateDelta.GetType()}"); + } + } + + [Pure] + private FungibleAssetValue GetBalance( Address address, Currency currency, IImmutableDictionary<(Address, Currency), BigInteger> balances) => @@ -325,122 +344,241 @@ protected virtual FungibleAssetValue GetBalance( ? FungibleAssetValue.FromRawValue(currency, balance) : BalanceGetter(address, currency); - protected virtual AccountStateDeltaImpl UpdateStates( + [Pure] + private AccountStateDelta UpdateStates( IImmutableDictionary updatedStates ) => - new AccountStateDeltaImpl( + new AccountStateDelta( StateGetter, BalanceGetter, TotalSupplyGetter, - ValidatorSetGetter, - Signer) + ValidatorSetGetter) { - UpdatedStates = updatedStates, - UpdatedFungibles = UpdatedFungibles, - UpdatedTotalSupply = UpdatedTotalSupply, - UpdatedValidatorSet = UpdatedValidatorSet, + Delta = new AccountDelta( + updatedStates, + Delta.Fungibles, + Delta.TotalSupplies, + Delta.ValidatorSet), + TotalUpdatedFungibles = TotalUpdatedFungibles, }; - protected virtual AccountStateDeltaImpl UpdateFungibleAssets( + [Pure] + private AccountStateDelta UpdateFungibleAssets( IImmutableDictionary<(Address, Currency), BigInteger> updatedFungibleAssets, + IImmutableDictionary<(Address, Currency), BigInteger> totalUpdatedFungibles + ) => + UpdateFungibleAssets( + updatedFungibleAssets, + totalUpdatedFungibles, + Delta.TotalSupplies); + + [Pure] + private AccountStateDelta UpdateFungibleAssets( + IImmutableDictionary<(Address, Currency), BigInteger> updatedFungibleAssets, + IImmutableDictionary<(Address, Currency), BigInteger> totalUpdatedFungibles, IImmutableDictionary updatedTotalSupply ) => - new AccountStateDeltaImpl( + new AccountStateDelta( StateGetter, BalanceGetter, TotalSupplyGetter, - ValidatorSetGetter, - Signer) + ValidatorSetGetter) { - UpdatedStates = UpdatedStates, - UpdatedFungibles = updatedFungibleAssets, - UpdatedTotalSupply = updatedTotalSupply, - UpdatedValidatorSet = UpdatedValidatorSet, + Delta = new AccountDelta( + Delta.States, + updatedFungibleAssets, + updatedTotalSupply, + Delta.ValidatorSet), + TotalUpdatedFungibles = totalUpdatedFungibles, }; - protected virtual AccountStateDeltaImpl UpdateFungibleAssets( - IImmutableDictionary<(Address, Currency), BigInteger> updatedFungibleAssets - ) => - UpdateFungibleAssets(updatedFungibleAssets, UpdatedTotalSupply); - - public static string ToStateKey(Address address) => - address.ToHex().ToLowerInvariant(); - - public static string ToFungibleAssetKey(Address address, Currency currency) => - "_" + address.ToHex().ToLowerInvariant() + - "_" + ByteUtil.Hex(currency.Hash.ByteArray).ToLowerInvariant(); - - public static string ToFungibleAssetKey((Address, Currency) pair) => - ToFungibleAssetKey(pair.Item1, pair.Item2); - - public static string ToTotalSupplyKey(Currency currency) => - "__" + ByteUtil.Hex(currency.Hash.ByteArray).ToLowerInvariant(); - - protected virtual AccountStateDeltaImpl UpdateValidatorSet( + [Pure] + private AccountStateDelta UpdateValidatorSet( ValidatorSet updatedValidatorSet ) => - new AccountStateDeltaImpl( + new AccountStateDelta( StateGetter, BalanceGetter, TotalSupplyGetter, - ValidatorSetGetter, - Signer) + ValidatorSetGetter) { - UpdatedStates = UpdatedStates, - UpdatedFungibles = UpdatedFungibles, - UpdatedTotalSupply = UpdatedTotalSupply, - UpdatedValidatorSet = updatedValidatorSet, + Delta = new AccountDelta( + Delta.States, + Delta.Fungibles, + Delta.TotalSupplies, + updatedValidatorSet), + TotalUpdatedFungibles = TotalUpdatedFungibles, }; - public virtual ValidatorSet GetValidatorSet() => - UpdatedValidatorSet ?? ValidatorSetGetter(); + [Pure] + private IAccountStateDelta TransferAssetV0( + Address sender, + Address recipient, + FungibleAssetValue value, + bool allowNegativeBalance = false) + { + if (value.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(value), + "The value to transfer has to be greater than zero." + ); + } + + Currency currency = value.Currency; + FungibleAssetValue senderBalance = GetBalance(sender, currency); + FungibleAssetValue recipientBalance = GetBalance(recipient, currency); - public IAccountStateDelta SetValidator(Validator validator) + if (!allowNegativeBalance && senderBalance < value) + { + var msg = $"The account {sender}'s balance of {currency} is insufficient to " + + $"transfer: {senderBalance} < {value}."; + throw new InsufficientBalanceException(msg, sender, senderBalance); + } + + return UpdateFungibleAssets( + Delta.Fungibles + .SetItem((sender, currency), (senderBalance - value).RawValue) + .SetItem((recipient, currency), (recipientBalance + value).RawValue), + TotalUpdatedFungibles + .SetItem((sender, currency), (senderBalance - value).RawValue) + .SetItem((recipient, currency), (recipientBalance + value).RawValue) + ); + } + + [Pure] + private IAccountStateDelta TransferAssetV1( + Address sender, + Address recipient, + FungibleAssetValue value, + bool allowNegativeBalance = false) { - Log.Debug( - "Update validator {PublicKey} {Power} to validator set", - validator.PublicKey, - validator.Power); - return UpdateValidatorSet(GetValidatorSet().Update(validator)); + if (value.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(value), + "The value to transfer has to be greater than zero." + ); + } + + Currency currency = value.Currency; + FungibleAssetValue senderBalance = GetBalance(sender, currency); + + if (!allowNegativeBalance && senderBalance < value) + { + var msg = $"The account {sender}'s balance of {currency} is insufficient to " + + $"transfer: {senderBalance} < {value}."; + throw new InsufficientBalanceException(msg, sender, senderBalance); + } + + (Address, Currency) senderAssetKey = (sender, currency); + BigInteger senderRawBalance = (senderBalance - value).RawValue; + + IImmutableDictionary<(Address, Currency), BigInteger> updatedFungibleAssets = + Delta.Fungibles.SetItem(senderAssetKey, senderRawBalance); + IImmutableDictionary<(Address, Currency), BigInteger> totalUpdatedFungibles = + TotalUpdatedFungibles.SetItem(senderAssetKey, senderRawBalance); + + FungibleAssetValue recipientBalance = GetBalance( + recipient, + currency, + updatedFungibleAssets); + (Address, Currency) recipientAssetKey = (recipient, currency); + BigInteger recipientRawBalance = (recipientBalance + value).RawValue; + + return UpdateFungibleAssets( + updatedFungibleAssets.SetItem(recipientAssetKey, recipientRawBalance), + totalUpdatedFungibles.SetItem(recipientAssetKey, recipientRawBalance) + ); } } + /// + /// Almost duplicate https://github.com/planetarium/libplanet/blob/main/Libplanet/State/AccountDelta.cs. + /// + private sealed class AccountDelta : IAccountDelta + { + internal AccountDelta() + { + States = ImmutableDictionary.Empty; + Fungibles = ImmutableDictionary<(Address, Currency), BigInteger>.Empty; + TotalSupplies = ImmutableDictionary.Empty; + ValidatorSet = null; + } + + internal AccountDelta( + IImmutableDictionary statesDelta, + IImmutableDictionary<(Address, Currency), BigInteger> fungiblesDelta, + IImmutableDictionary totalSuppliesDelta, + ValidatorSet? validatorSetDelta) + { + States = statesDelta; + Fungibles = fungiblesDelta; + TotalSupplies = totalSuppliesDelta; + ValidatorSet = validatorSetDelta; + } + + /// + public IImmutableSet
UpdatedAddresses => + StateUpdatedAddresses.Union(FungibleUpdatedAddresses); + + /// + public IImmutableSet
StateUpdatedAddresses => + States.Keys.ToImmutableHashSet(); + + /// + public IImmutableDictionary States { get; } + + /// + public IImmutableSet
FungibleUpdatedAddresses => + Fungibles.Keys.Select(pair => pair.Item1).ToImmutableHashSet(); + + /// + public IImmutableSet<(Address, Currency)> UpdatedFungibleAssets => + Fungibles.Keys.ToImmutableHashSet(); + + /// + public IImmutableDictionary<(Address, Currency), BigInteger> Fungibles { get; } + + /// + public IImmutableSet UpdatedTotalSupplyCurrencies => + TotalSupplies.Keys.ToImmutableHashSet(); + + /// + public IImmutableDictionary TotalSupplies { get; } + + /// + public ValidatorSet? ValidatorSet { get; } + } + /// /// Almost duplicate https://github.com/planetarium/libplanet/blob/main/Libplanet/Action/ActionContext.cs. /// private sealed class ActionContext : IActionContext { private readonly int _randomSeed; - private readonly ITrie? _previousBlockStatesTrie; - private HashDigest? _previousStateRootHash; public ActionContext( - BlockHash? genesisHash, Address signer, TxId? txid, Address miner, long blockIndex, - IAccountStateDelta previousStates, + int blockProtocolVersion, + IAccountStateDelta previousState, int randomSeed, - bool rehearsal = false, - ITrie? previousBlockStatesTrie = null, - bool blockAction = false) + bool rehearsal = false) { - GenesisHash = genesisHash; Signer = signer; TxId = txid; Miner = miner; BlockIndex = blockIndex; + BlockProtocolVersion = blockProtocolVersion; Rehearsal = rehearsal; - PreviousStates = previousStates; + PreviousState = previousState; Random = new Random(randomSeed); _randomSeed = randomSeed; - _previousBlockStatesTrie = previousBlockStatesTrie; - BlockAction = blockAction; } - public BlockHash? GenesisHash { get; } - public Address Signer { get; } public TxId? TxId { get; } @@ -449,22 +587,15 @@ public ActionContext( public long BlockIndex { get; } + public int BlockProtocolVersion { get; } + public bool Rehearsal { get; } - public IAccountStateDelta PreviousStates { get; } + public IAccountStateDelta PreviousState { get; } public IRandom Random { get; } - public HashDigest? PreviousStateRootHash => - _previousStateRootHash ??= _previousBlockStatesTrie is null - ? null - : Set( - _previousBlockStatesTrie!, - GetUpdatedRawStates(PreviousStates)) - .Commit() - .Hash; - - public bool BlockAction { get; } + public bool BlockAction => TxId is null; public void PutLog(string log) { @@ -477,76 +608,18 @@ public void UseGas(long gas) public IActionContext GetUnconsumedContext() => new ActionContext( - GenesisHash, Signer, TxId, Miner, BlockIndex, - PreviousStates, + BlockProtocolVersion, + PreviousState, _randomSeed, - Rehearsal, - _previousBlockStatesTrie, - BlockAction); + Rehearsal); public long GasUsed() => 0; public long GasLimit() => 0; - - private IImmutableDictionary GetUpdatedRawStates( - IAccountStateDelta delta) - { - if (delta is not AccountStateDeltaImpl impl) - { - return ImmutableDictionary.Empty; - } - - return impl.GetUpdatedStates() - .Select(pair => - new KeyValuePair( - AccountStateDeltaImpl.ToStateKey(pair.Key), - pair.Value)) - .Union( - impl.GetUpdatedBalances().Select(pair => - new KeyValuePair( - AccountStateDeltaImpl.ToFungibleAssetKey(pair.Key), - (Integer)pair.Value.RawValue))) - .Union( - impl.GetUpdatedTotalSupplies().Select(pair => - new KeyValuePair( - AccountStateDeltaImpl.ToTotalSupplyKey(pair.Key), - (Integer)pair.Value.RawValue))).ToImmutableDictionary(); - } - - private ITrie Set(ITrie trie, IEnumerable> pairs) - => Set( - trie, - pairs.Select(pair => - new KeyValuePair( - StateStoreExtensions.EncodeKey(pair.Key), - pair.Value - ) - ) - ); - - private ITrie Set(ITrie trie, IEnumerable> pairs) - { - foreach (var pair in pairs) - { - if (pair.Value is { } v) - { - trie = trie.Set(pair.Key, v); - } - else - { - throw new NotSupportedException( - "Unsetting states is not supported yet. " + - "See also: https://github.com/planetarium/libplanet/issues/1383" - ); - } - } - - return trie; - } } private sealed class Random : System.Random, IRandom @@ -564,33 +637,29 @@ public Random(int seed) /// Almost duplicate https://github.com/planetarium/libplanet/blob/main/Libplanet/Action/ActionEvaluator.cs#L286. /// private static IEnumerable EvaluateActions( - BlockHash? genesisHash, - ImmutableArray preEvaluationHash, + HashDigest preEvaluationHash, long blockIndex, + int blockProtocolVersion, TxId? txid, IAccountStateDelta previousStates, Address miner, Address signer, byte[] signature, IImmutableList actions, - bool rehearsal = false, - ITrie? previousBlockStatesTrie = null, - bool blockAction = false, ILogger? logger = null) { - ActionContext CreateActionContext(IAccountStateDelta prevStates, int randomSeed) + ActionContext CreateActionContext( + IAccountStateDelta prevState, + int randomSeed) { return new ActionContext( - genesisHash: genesisHash, signer: signer, txid: txid, miner: miner, blockIndex: blockIndex, - previousStates: prevStates, - randomSeed: randomSeed, - rehearsal: rehearsal, - previousBlockStatesTrie: previousBlockStatesTrie, - blockAction: blockAction); + blockProtocolVersion: blockProtocolVersion, + previousState: prevState, + randomSeed: randomSeed); } byte[] hashedSignature; @@ -599,25 +668,26 @@ ActionContext CreateActionContext(IAccountStateDelta prevStates, int randomSeed) hashedSignature = hasher.ComputeHash(signature); } - byte[] preEvaluationHashBytes = preEvaluationHash.ToBuilder().ToArray(); - int seed = ActionEvaluator.GenerateRandomSeed( - preEvaluationHashBytes, - hashedSignature, - signature, - 0); + byte[] preEvaluationHashBytes = preEvaluationHash.ToByteArray(); + int seed = ActionEvaluator.GenerateRandomSeed(preEvaluationHashBytes, hashedSignature, signature, 0); IAccountStateDelta states = previousStates; foreach (IAction action in actions) { Exception? exc = null; - ActionContext context = CreateActionContext(states, seed); - IAccountStateDelta nextStates = context.PreviousStates; + IAccountStateDelta nextStates = states; + ActionContext context = CreateActionContext(nextStates, seed); + try { - DateTimeOffset actionExecutionStarted = DateTimeOffset.Now; + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); nextStates = action.Execute(context); - TimeSpan spent = DateTimeOffset.Now - actionExecutionStarted; - logger?.Verbose($"{action} execution spent {spent.TotalMilliseconds} ms."); + logger? + .Information( + "Action {Action} took {DurationMs} ms to execute", + action, + stopwatch.ElapsedMilliseconds); } catch (OutOfMemoryException e) { @@ -626,65 +696,44 @@ ActionContext CreateActionContext(IAccountStateDelta prevStates, int randomSeed) var message = "Action {Action} of tx {TxId} of block #{BlockIndex} with " + "pre-evaluation hash {PreEvaluationHash} threw an exception " + - "during execution."; + "during execution"; logger?.Error( e, message, action, txid, blockIndex, - ByteUtil.Hex(preEvaluationHash)); + ByteUtil.Hex(preEvaluationHash.ByteArray)); throw; } catch (Exception e) { - if (rehearsal) - { - var message = - $"The action {action} threw an exception during its " + - "rehearsal. It is probably because the logic of the " + - $"action {action} is not enough generic so that it " + - "can cover every case including rehearsal mode.\n" + - "The IActionContext.Rehearsal property also might be " + - "useful to make the action can deal with the case of " + - "rehearsal mode.\n" + - "See also this exception's InnerException property."; - exc = new UnexpectedlyTerminatedActionException( - message, null, null, null, null, action, e); - } - else - { - var stateRootHash = context.PreviousStateRootHash; - var message = - "Action {Action} of tx {TxId} of block #{BlockIndex} with " + - "pre-evaluation hash {PreEvaluationHash} and previous " + - "state root hash {StateRootHash} threw an exception " + - "during execution."; - logger?.Error( - e, - message, - action, - txid, - blockIndex, - ByteUtil.Hex(preEvaluationHash), - stateRootHash); - var innerMessage = - $"The action {action} (block #{blockIndex}, " + - $"pre-evaluation hash {ByteUtil.Hex(preEvaluationHash)}, tx {txid}, " + - $"previous state root hash {stateRootHash}) threw " + - "an exception during execution. " + - "See also this exception's InnerException property."; - logger?.Error( - "{Message}\nInnerException: {ExcMessage}", innerMessage, e.Message); - exc = new UnexpectedlyTerminatedActionException( - innerMessage, - new HashDigest(preEvaluationHash), - blockIndex, - txid, - stateRootHash, - action, - e); - } + var message = + "Action {Action} of tx {TxId} of block #{BlockIndex} with " + + "pre-evaluation hash {PreEvaluationHash} threw an exception " + + "during execution"; + logger?.Error( + e, + message, + action, + txid, + blockIndex, + ByteUtil.Hex(preEvaluationHash.ByteArray)); + var innerMessage = + $"The action {action} (block #{blockIndex}, " + + $"pre-evaluation hash {ByteUtil.Hex(preEvaluationHash.ByteArray)}, " + + $"tx {txid} threw an exception during execution. " + + "See also this exception's InnerException property"; + logger?.Error( + "{Message}\nInnerException: {ExcMessage}", innerMessage, e.Message); + exc = new UnexpectedlyTerminatedActionException( + innerMessage, + preEvaluationHash, + blockIndex, + txid, + null, + action, + e); } // As IActionContext.Random is stateful, we cannot reuse @@ -694,7 +743,7 @@ ActionContext CreateActionContext(IAccountStateDelta prevStates, int randomSeed) yield return new ActionEvaluation( action: action, inputContext: equivalentContext, - outputStates: nextStates, + outputState: nextStates, exception: exc); if (exc is { }) diff --git a/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs b/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs index 97f1140b8..47e3fc2b5 100644 --- a/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs @@ -91,33 +91,19 @@ public int Tx( } // Evaluate tx. - IAccountStateDelta previousStates = new AccountStateDeltaImpl( - addresses => blockChain.GetStates( - addresses, - previousBlock.Hash), - (address, currency) => blockChain.GetBalance( - address, - currency, - previousBlock.Hash), - currency => blockChain.GetTotalSupply( - currency, - previousBlock.Hash), - () => blockChain.GetValidatorSet( - previousBlock.Hash), - tx.Signer); + IAccountState previousBlockStates = blockChain.GetBlockState(previousBlock.Hash); + IAccountStateDelta previousStates = AccountStateDelta.Create(previousBlockStates); var actions = tx.Actions.Select(a => ToAction(a)); var actionEvaluations = EvaluateActions( - genesisHash: blockChain.Genesis.Hash, - preEvaluationHash: targetBlock.PreEvaluationHash.ByteArray, - blockIndex: blockIndex, + preEvaluationHash: targetBlock.PreEvaluationHash, + blockIndex: targetBlock.Index, + blockProtocolVersion: targetBlock.ProtocolVersion, txid: tx.Id, previousStates: previousStates, miner: targetBlock.Miner, signer: tx.Signer, signature: tx.Signature, - actions: actions.Cast().ToImmutableList(), - rehearsal: false, - previousBlockStatesTrie: null + actions: actions.Cast().ToImmutableList() ); var actionNum = 1; foreach (var actionEvaluation in actionEvaluations) @@ -150,13 +136,12 @@ public int Tx( outputSw?.WriteLine(msg); } - var states = actionEvaluation.OutputStates; + var states = actionEvaluation.OutputState; var addressNum = 1; - foreach (var updatedAddress in states.UpdatedAddresses) + foreach (var (updatedAddress, updatedState) in states.Delta.States) { if (verbose) { - var updatedState = states.GetState(updatedAddress); msg = $"- action #{actionNum} updated address #{addressNum}({updatedAddress}) beginning.."; _console.Out.WriteLine(msg); outputSw?.WriteLine(msg); diff --git a/NineChronicles.Headless.Executable/Commands/StateCommand.cs b/NineChronicles.Headless.Executable/Commands/StateCommand.cs index 9fb92ec7f..8e06bf2e1 100644 --- a/NineChronicles.Headless.Executable/Commands/StateCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/StateCommand.cs @@ -23,6 +23,7 @@ using Nekoyume.Action.Loader; using NineChronicles.Headless.Executable.IO; using Serilog.Core; +using DevExUtils = Lib9c.DevExtensions.Utils; namespace NineChronicles.Headless.Executable.Commands { @@ -66,7 +67,7 @@ public void Rebuild( string? useMemoryKvStore = null ) { - using Logger logger = Utils.ConfigureLogger(verbose); + using Logger logger = DevExUtils.ConfigureLogger(verbose); CancellationToken cancellationToken = GetInterruptSignalCancellationToken(); TextWriter stderr = _console.Error; ( @@ -74,14 +75,14 @@ public void Rebuild( IStore store, IKeyValueStore stateKvStore, IStateStore stateStore - ) = Utils.GetBlockChain( + ) = DevExUtils.GetBlockChain( logger, storePath, chainId, useMemoryKvStore is string p ? new MemoryKeyValueStore(p, stderr) : null ); - Block bottom = Utils.ParseBlockOffset(chain, bottommost, 0); - Block top = Utils.ParseBlockOffset(chain, topmost); + Block bottom = DevExUtils.ParseBlockOffset(chain, bottommost, 0); + Block top = DevExUtils.ParseBlockOffset(chain, topmost); stderr.WriteLine("It will execute all actions (tx actions & block actions)"); stderr.WriteLine( @@ -221,7 +222,7 @@ public void Check( bool verbose = false ) { - using Logger logger = Utils.ConfigureLogger(verbose); + using Logger logger = DevExUtils.ConfigureLogger(verbose); CancellationToken cancellationToken = GetInterruptSignalCancellationToken(); TextWriter stderr = _console.Error; ( @@ -229,12 +230,12 @@ public void Check( IStore store, IKeyValueStore stateKvStore, IStateStore stateStore - ) = Utils.GetBlockChain( + ) = DevExUtils.GetBlockChain( logger, storePath, chainId ); - Block checkBlock = Utils.ParseBlockOffset(chain, block); + Block checkBlock = DevExUtils.ParseBlockOffset(chain, block); HashDigest stateRootHash = checkBlock.StateRootHash; ITrie stateRoot = stateStore.GetStateRoot(stateRootHash); bool exist = stateRoot.Recorded; @@ -447,14 +448,13 @@ private static ImmutableDictionary GetTotalDelta( string validatorSetKey) { IImmutableSet
stateUpdatedAddresses = actionEvaluations - .SelectMany(a => a.OutputStates.StateUpdatedAddresses) + .SelectMany(a => a.OutputState.Delta.StateUpdatedAddresses) .ToImmutableHashSet(); IImmutableSet<(Address, Currency)> updatedFungibleAssets = actionEvaluations - .SelectMany(a => a.OutputStates.UpdatedFungibleAssets - .SelectMany(kv => kv.Value.Select(c => (kv.Key, c)))) + .SelectMany(a => a.OutputState.Delta.UpdatedFungibleAssets) .ToImmutableHashSet(); IImmutableSet updatedTotalSupplies = actionEvaluations - .SelectMany(a => a.OutputStates.TotalSupplyUpdatedCurrencies) + .SelectMany(a => a.OutputState.Delta.UpdatedTotalSupplyCurrencies) .ToImmutableHashSet(); if (actionEvaluations.Count == 0) @@ -462,7 +462,7 @@ private static ImmutableDictionary GetTotalDelta( return ImmutableDictionary.Empty; } - IAccountStateDelta lastStates = actionEvaluations[actionEvaluations.Count - 1].OutputStates; + IAccountStateDelta lastStates = actionEvaluations[actionEvaluations.Count - 1].OutputState; ImmutableDictionary totalDelta = stateUpdatedAddresses.ToImmutableDictionary( diff --git a/NineChronicles.Headless.Tests/Common/Actions/EmptyAction.cs b/NineChronicles.Headless.Tests/Common/Actions/EmptyAction.cs index 95ce3cda2..36d3af788 100644 --- a/NineChronicles.Headless.Tests/Common/Actions/EmptyAction.cs +++ b/NineChronicles.Headless.Tests/Common/Actions/EmptyAction.cs @@ -13,7 +13,7 @@ public void LoadPlainValue(IValue plainValue) public IAccountStateDelta Execute(IActionContext context) { - return context.PreviousStates; + return context.PreviousState; } public void Render(IActionContext context, IAccountStateDelta nextStates) diff --git a/NineChronicles.Headless.Tests/Common/Fixtures.cs b/NineChronicles.Headless.Tests/Common/Fixtures.cs index b68b66c44..030802788 100644 --- a/NineChronicles.Headless.Tests/Common/Fixtures.cs +++ b/NineChronicles.Headless.Tests/Common/Fixtures.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Lib9c.Model.Order; using Lib9c.Tests; using Libplanet; @@ -65,6 +67,9 @@ public static ShopState ShopStateFX() return shopState; } + public static readonly List CombinationSlotStatesFx = + AvatarStateFX.combinationSlotAddresses.Select(x => new CombinationSlotState(x, 0)).ToList(); + public static ShardedShopStateV2 ShardedWeapon0ShopStateV2FX() { Address shardedWeapon0ShopStateV2Address = ShardedShopStateV2.DeriveAddress(ItemSubType.Weapon, "0"); diff --git a/NineChronicles.Headless.Tests/GraphQLTestUtils.cs b/NineChronicles.Headless.Tests/GraphQLTestUtils.cs index 3d263e6ac..f3c30f78a 100644 --- a/NineChronicles.Headless.Tests/GraphQLTestUtils.cs +++ b/NineChronicles.Headless.Tests/GraphQLTestUtils.cs @@ -14,8 +14,11 @@ using Libplanet.Store.Trie; using Libplanet.Tx; using Microsoft.Extensions.DependencyInjection; +using Nekoyume; using Nekoyume.Action; using Nekoyume.Action.Loader; +using Nekoyume.Model.State; +using NineChronicles.Headless.Utils; namespace NineChronicles.Headless.Tests { @@ -32,7 +35,7 @@ public static Task ExecuteQueryAsync( { var services = new ServiceCollection(); services.AddSingleton(typeof(TObjectGraphType)); - if (!(standaloneContext is null)) + if (standaloneContext is not null) { services.AddSingleton(standaloneContext); } @@ -69,7 +72,8 @@ public static Task ExecuteQueryAsync( } // FIXME: Passing 0 index is bad. - public static ActionBase DeserializeNCAction(IValue value) => (ActionBase)_actionLoader.LoadAction(0, value); + public static ActionBase DeserializeNCAction(IValue value) => + (ActionBase)_actionLoader.LoadAction(0, value); public static StandaloneContext CreateStandaloneContext() { @@ -89,10 +93,14 @@ public static StandaloneContext CreateStandaloneContext() stateStore, genesisBlock, actionEvaluator); + var currencyFactory = new CurrencyFactory(blockchain.GetStates); + var fungibleAssetValueFactory = new FungibleAssetValueFactory(currencyFactory); return new StandaloneContext { BlockChain = blockchain, Store = store, + CurrencyFactory = currencyFactory, + FungibleAssetValueFactory = fungibleAssetValueFactory, }; } @@ -125,10 +133,16 @@ PrivateKey minerPrivateKey stateStore, genesisBlock, actionEvaluator); + var ncg = new GoldCurrencyState((Dictionary)blockchain.GetState(Addresses.GoldCurrency)) + .Currency; + var currencyFactory = new CurrencyFactory(blockchain.GetStates, ncg); + var fungibleAssetValueFactory = new FungibleAssetValueFactory(currencyFactory); return new StandaloneContext { BlockChain = blockchain, Store = store, + CurrencyFactory = currencyFactory, + FungibleAssetValueFactory = fungibleAssetValueFactory, }; } } diff --git a/NineChronicles.Headless.Tests/GraphTypes/ActionQueryTest.cs b/NineChronicles.Headless.Tests/GraphTypes/ActionQueryTest.cs index 675fa4d8f..2ff7797f3 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/ActionQueryTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/ActionQueryTest.cs @@ -3,16 +3,19 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Bencodex; using Bencodex.Types; using GraphQL.Execution; +using Lib9c; using Libplanet; using Libplanet.Assets; using Libplanet.Crypto; using Nekoyume; using Nekoyume.Action; +using Nekoyume.Action.Garages; using Nekoyume.Helper; using Nekoyume.Model; using Nekoyume.Model.EnumType; @@ -52,8 +55,7 @@ public ActionQueryTest() activatedAccountsState: new ActivatedAccountsState(), #pragma warning disable CS0618 // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 - goldCurrencyState: - new GoldCurrencyState(Currency.Legacy("NCG", 2, minerPrivateKey.ToAddress())), + goldCurrencyState: new GoldCurrencyState(Currency.Legacy("NCG", 2, minerPrivateKey.ToAddress())), #pragma warning restore CS0618 goldDistributions: Array.Empty(), tableSheets: new Dictionary(), @@ -140,6 +142,7 @@ private class StakeFixture : IEnumerable IEnumerator IEnumerable.GetEnumerator() => _data.GetEnumerator(); } + [Theory] [InlineData("false", false)] [InlineData("true", true)] @@ -148,11 +151,13 @@ public async Task Grinding(string chargeApValue, bool chargeAp) { var avatarAddress = new PrivateKey().ToAddress(); var equipmentId = Guid.NewGuid(); - string queryArgs = $"avatarAddress: \"{avatarAddress.ToString()}\", equipmentIds: [{string.Format($"\"{equipmentId}\"")}]"; + string queryArgs = + $"avatarAddress: \"{avatarAddress.ToString()}\", equipmentIds: [{string.Format($"\"{equipmentId}\"")}]"; if (!string.IsNullOrEmpty(chargeApValue)) { queryArgs += $", chargeAp: {chargeApValue}"; } + string query = $@" {{ grinding({queryArgs}) @@ -239,6 +244,7 @@ public async Task TransferAsset(string currencyType, bool memo) { args += ", memo: \"memo\""; } + var query = $"{{ transferAsset({args}) }}"; var queryResult = await ExecuteQueryAsync(query, standaloneContext: _standaloneContext); var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; @@ -454,6 +460,7 @@ public async Task PrepareRewardAssets(bool mintersExist, int expectedCount) { assets += $", {{quantity: 100, decimalPlaces: 2, ticker: \"NCG\", minters: [\"{rewardPoolAddress}\"]}}"; } + var query = $"{{ prepareRewardAssets(rewardPoolAddress: \"{rewardPoolAddress}\", assets: [{assets}]) }}"; var queryResult = await ExecuteQueryAsync(query, standaloneContext: _standaloneContext); var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; @@ -493,17 +500,21 @@ public async Task TransferAssets(bool exc) var count = 0; while (count < Nekoyume.Action.TransferAssets.RecipientsCapacity) { - recipients += $", {{ recipient: \"{sender}\", amount: {{ quantity: 100, decimalPlaces: 18, ticker: \"CRYSTAL\" }} }}, {{ recipient: \"{sender}\", amount: {{ quantity: 100, decimalPlaces: 0, ticker: \"RUNE_FENRIR1\" }} }}"; + recipients += + $", {{ recipient: \"{sender}\", amount: {{ quantity: 100, decimalPlaces: 18, ticker: \"CRYSTAL\" }} }}, {{ recipient: \"{sender}\", amount: {{ quantity: 100, decimalPlaces: 0, ticker: \"RUNE_FENRIR1\" }} }}"; count++; } } + var query = $"{{ transferAssets(sender: \"{sender}\", recipients: [{recipients}]) }}"; var queryResult = await ExecuteQueryAsync(query, standaloneContext: _standaloneContext); if (exc) { var error = Assert.Single(queryResult.Errors!); - Assert.Contains($"recipients must be less than or equal {Nekoyume.Action.TransferAssets.RecipientsCapacity}.", error.Message); + Assert.Contains( + $"recipients must be less than or equal {Nekoyume.Action.TransferAssets.RecipientsCapacity}.", + error.Message); } else { @@ -1010,5 +1021,289 @@ public async Task CreatePledge(int? mead, int expected) Assert.Equal(MeadConfig.PatronAddress, action.PatronAddress); Assert.Equal(expected, action.Mead); } + + [Theory] + [MemberData(nameof(GetMemberDataOfLoadIntoMyGarages))] + public async Task LoadIntoMyGarages( + IEnumerable<(Address balanceAddr, FungibleAssetValue value)>? fungibleAssetValues, + Address? inventoryAddr, + IEnumerable<(HashDigest fungibleId, int count)>? fungibleIdAndCounts, + string? memo) + { + var expectedAction = new LoadIntoMyGarages( + fungibleAssetValues, + inventoryAddr, + fungibleIdAndCounts, + memo); + var sb = new StringBuilder("{ loadIntoMyGarages("); + if (fungibleAssetValues is not null) + { + sb.Append("fungibleAssetValues: ["); + sb.Append(string.Join(",", fungibleAssetValues.Select(tuple => + $"{{ balanceAddr: \"{tuple.balanceAddr.ToHex()}\", " + + $"value: {{ currencyTicker: \"{tuple.value.Currency.Ticker}\"," + + $"value: \"{tuple.value.GetQuantityString()}\" }} }}"))); + sb.Append("],"); + } + + if (inventoryAddr is not null) + { + sb.Append($"inventoryAddr: \"{inventoryAddr.Value.ToHex()}\","); + } + + if (fungibleIdAndCounts is not null) + { + sb.Append("fungibleIdAndCounts: ["); + sb.Append(string.Join(",", fungibleIdAndCounts.Select(tuple => + $"{{ fungibleId: \"{tuple.fungibleId.ToString()}\", " + + $"count: {tuple.count} }}"))); + sb.Append("],"); + } + + if (memo is not null) + { + sb.Append($"memo: \"{memo}\""); + } + + // Remove last ',' if exists. + if (sb[^1] == ',') + { + sb.Remove(sb.Length - 1, 1); + } + + sb.Append(") }"); + var queryResult = await ExecuteQueryAsync( + sb.ToString(), + standaloneContext: _standaloneContext); + Assert.Null(queryResult.Errors); + var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; + var plainValue = _codec.Decode(ByteUtil.ParseHex((string)data["loadIntoMyGarages"])); + Assert.IsType(plainValue); + var actionBase = DeserializeNCAction(plainValue); + var actualAction = Assert.IsType(actionBase); + Assert.True(expectedAction.FungibleAssetValues?.SequenceEqual(actualAction.FungibleAssetValues) ?? + actualAction.FungibleAssetValues is null); + Assert.Equal(expectedAction.InventoryAddr, actualAction.InventoryAddr); + Assert.True(expectedAction.FungibleIdAndCounts?.SequenceEqual(actualAction.FungibleIdAndCounts) ?? + actualAction.FungibleIdAndCounts is null); + Assert.Equal(expectedAction.Memo, actualAction.Memo); + } + + private static IEnumerable GetMemberDataOfLoadIntoMyGarages() + { + yield return new object[] + { + null, + null, + null, + "memo", + }; + yield return new object[] + { + new[] + { + ( + address: new PrivateKey().ToAddress(), + fungibleAssetValue: new FungibleAssetValue(Currencies.Garage, 1, 0) + ), + ( + address: new PrivateKey().ToAddress(), + fungibleAssetValue: new FungibleAssetValue(Currencies.Garage, 1, 0) + ), + }, + new PrivateKey().ToAddress(), + new[] + { + (fungibleId: new HashDigest(), count: 1), + (fungibleId: new HashDigest(), count: 1), + }, + "memo", + }; + } + + [Theory] + [MemberData(nameof(GetMemberDataOfDeliverToOthersGarages))] + public async Task DeliverToOthersGarages( + Address recipientAgentAddr, + IEnumerable? fungibleAssetValues, + IEnumerable<(HashDigest fungibleId, int count)>? fungibleIdAndCounts, + string? memo) + { + var expectedAction = new DeliverToOthersGarages( + recipientAgentAddr, + fungibleAssetValues, + fungibleIdAndCounts, + memo); + var sb = new StringBuilder("{ deliverToOthersGarages("); + sb.Append($"recipientAgentAddr: \"{recipientAgentAddr.ToHex()}\","); + if (fungibleAssetValues is not null) + { + sb.Append("fungibleAssetValues: ["); + sb.Append(string.Join(",", fungibleAssetValues.Select(tuple => + $"{{ currencyTicker: \"{tuple.Currency.Ticker}\", " + + $"value: \"{tuple.GetQuantityString()}\" }}"))); + sb.Append("],"); + } + + if (fungibleIdAndCounts is not null) + { + sb.Append("fungibleIdAndCounts: ["); + sb.Append(string.Join(",", fungibleIdAndCounts.Select(tuple => + $"{{ fungibleId: \"{tuple.fungibleId.ToString()}\", " + + $"count: {tuple.count} }}"))); + sb.Append("],"); + } + + if (memo is not null) + { + sb.Append($"memo: \"{memo}\""); + } + + // Remove last ',' if exists. + if (sb[^1] == ',') + { + sb.Remove(sb.Length - 1, 1); + } + + sb.Append(") }"); + + var queryResult = await ExecuteQueryAsync( + sb.ToString(), + standaloneContext: _standaloneContext); + Assert.Null(queryResult.Errors); + var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; + var plainValue = _codec.Decode(ByteUtil.ParseHex((string)data["deliverToOthersGarages"])); + Assert.IsType(plainValue); + var actionBase = DeserializeNCAction(plainValue); + var action = Assert.IsType(actionBase); + Assert.Equal(expectedAction.RecipientAgentAddr, action.RecipientAgentAddr); + Assert.True(expectedAction.FungibleAssetValues?.SequenceEqual(action.FungibleAssetValues) ?? + action.FungibleAssetValues is null); + Assert.True(expectedAction.FungibleIdAndCounts?.SequenceEqual(action.FungibleIdAndCounts) ?? + action.FungibleIdAndCounts is null); + Assert.Equal(expectedAction.Memo, action.Memo); + } + + private static IEnumerable GetMemberDataOfDeliverToOthersGarages() + { + yield return new object[] + { + new PrivateKey().ToAddress(), + null, + null, + null, + }; + yield return new object[] + { + new PrivateKey().ToAddress(), + new[] + { + new FungibleAssetValue(Currencies.Garage, 1, 0), + new FungibleAssetValue(Currencies.Garage, 1, 0), + }, + new[] + { + (fungibleId: new HashDigest(), count: 1), + (fungibleId: new HashDigest(), count: 1), + }, + "memo", + }; + } + + [Theory] + [MemberData(nameof(GetMemberDataOfUnloadFromMyGarages))] + public async Task UnloadFromMyGarages( + Address recipientAvatarAddr, + IEnumerable<(Address balanceAddr, FungibleAssetValue value)>? fungibleAssetValues, + IEnumerable<(HashDigest fungibleId, int count)>? fungibleIdAndCounts, + string? memo) + { + var expectedAction = new UnloadFromMyGarages( + recipientAvatarAddr, + fungibleAssetValues, + fungibleIdAndCounts, + memo); + var sb = new StringBuilder("{ unloadFromMyGarages("); + sb.Append($"recipientAvatarAddr: \"{recipientAvatarAddr.ToHex()}\","); + if (fungibleAssetValues is not null) + { + sb.Append("fungibleAssetValues: ["); + sb.Append(string.Join(",", fungibleAssetValues.Select(tuple => + $"{{ balanceAddr: \"{tuple.balanceAddr.ToHex()}\", " + + $"value: {{ currencyTicker: \"{tuple.value.Currency.Ticker}\"," + + $"value: \"{tuple.value.GetQuantityString()}\" }} }}"))); + sb.Append("],"); + } + + if (fungibleIdAndCounts is not null) + { + sb.Append("fungibleIdAndCounts: ["); + sb.Append(string.Join(",", fungibleIdAndCounts.Select(tuple => + $"{{ fungibleId: \"{tuple.fungibleId.ToString()}\", " + + $"count: {tuple.count} }}"))); + sb.Append("],"); + } + + if (memo is not null) + { + sb.Append($"memo: \"{memo}\""); + } + + // Remove last ',' if exists. + if (sb[^1] == ',') + { + sb.Remove(sb.Length - 1, 1); + } + + sb.Append(") }"); + var queryResult = await ExecuteQueryAsync( + sb.ToString(), + standaloneContext: _standaloneContext); + Assert.Null(queryResult.Errors); + + var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; + var plainValue = _codec.Decode(ByteUtil.ParseHex((string)data["unloadFromMyGarages"])); + Assert.IsType(plainValue); + var actionBase = DeserializeNCAction(plainValue); + var action = Assert.IsType(actionBase); + Assert.Equal(expectedAction.RecipientAvatarAddr, action.RecipientAvatarAddr); + Assert.True(expectedAction.FungibleAssetValues?.SequenceEqual(action.FungibleAssetValues) ?? + action.FungibleAssetValues is null); + Assert.True(expectedAction.FungibleIdAndCounts?.SequenceEqual(action.FungibleIdAndCounts) ?? + action.FungibleIdAndCounts is null); + Assert.Equal(expectedAction.Memo, action.Memo); + } + + private static IEnumerable GetMemberDataOfUnloadFromMyGarages() + { + yield return new object[] + { + new PrivateKey().ToAddress(), + null, + null, + null, + }; + yield return new object[] + { + new PrivateKey().ToAddress(), + new[] + { + ( + address: new PrivateKey().ToAddress(), + fungibleAssetValue: new FungibleAssetValue(Currencies.Garage, 1, 0) + ), + ( + address: new PrivateKey().ToAddress(), + fungibleAssetValue: new FungibleAssetValue(Currencies.Garage, 1, 0) + ), + }, + new[] + { + (fungibleId: new HashDigest(), count: 1), + (fungibleId: new HashDigest(), count: 1), + }, + "memo", + }; + } } } diff --git a/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs b/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs new file mode 100644 index 000000000..e986d1708 --- /dev/null +++ b/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Bencodex; +using Bencodex.Types; +using Google.Protobuf.WellKnownTypes; +using GraphQL; +using GraphQL.Execution; +using Lib9c; +using Libplanet; +using Libplanet.Assets; +using Libplanet.Crypto; +using Nekoyume; +using Nekoyume.Action.Garages; +using Nekoyume.Model.Elemental; +using Nekoyume.Model.Garages; +using Nekoyume.Model.Item; +using Nekoyume.Model.State; +using NineChronicles.Headless.GraphTypes; +using NineChronicles.Headless.GraphTypes.States; +using Xunit; +using static NineChronicles.Headless.Tests.GraphQLTestUtils; + +namespace NineChronicles.Headless.Tests.GraphTypes +{ + public class StateQueryTest + { + private readonly Codec _codec; + + public StateQueryTest() + { + _codec = new Codec(); + } + + [Theory] + [MemberData(nameof(GetMemberDataOfGarages))] + public async Task Garage( + Address agentAddr, + IEnumerable? currencyEnums, + IEnumerable? currencyTickers, + IEnumerable? fungibleItemIds, + IEnumerable? setToNullForFungibleItemGarages) + { + var sb = new StringBuilder("{ garages("); + sb.Append($"agentAddr: \"{agentAddr.ToString()}\""); + if (currencyEnums is not null) + { + sb.Append(", currencyEnums: ["); + sb.Append(string.Join(", ", currencyEnums)); + sb.Append("]"); + } + + if (currencyTickers is not null) + { + sb.Append(", currencyTickers: ["); + sb.Append(string.Join(", ", currencyTickers.Select(ticker => $"\"{ticker}\""))); + sb.Append("]"); + } + + if (fungibleItemIds is not null) + { + sb.Append(", fungibleItemIds: ["); + sb.Append(string.Join(", ", fungibleItemIds.Select(id => $"\"{id}\""))); + sb.Append("]"); + } + + sb.Append(") {"); + sb.Append("agentAddr"); + sb.Append(" garageBalancesAddr"); + sb.Append(" garageBalances {"); + sb.Append(" currency { ticker } sign majorUnit minorUnit quantity string"); + sb.Append(" }"); + sb.Append(" fungibleItemGarages {"); + sb.Append(" fungibleItemId addr item { fungibleItemId } count"); + sb.Append(" }"); + sb.Append("}}"); + var addrToFungibleItemIdDict = fungibleItemIds is null + ? new Dictionary() + : fungibleItemIds.ToDictionary( + fungibleItemId => Addresses.GetGarageAddress( + agentAddr, + HashDigest.FromString(fungibleItemId)), + fungibleItemId => fungibleItemId); + var queryResult = await ExecuteQueryAsync( + sb.ToString(), + source: new StateContext( + stateAddresses => + { + var arr = new IValue?[stateAddresses.Count]; + for (var i = 0; i < stateAddresses.Count; i++) + { + var stateAddr = stateAddresses[i]; + if (stateAddr.Equals(Addresses.GoldCurrency)) + { + var currency = Currency.Legacy( + "NCG", + 2, + new Address("0x47D082a115c63E7b58B1532d20E631538eaFADde")); + arr[i] = new GoldCurrencyState(currency).Serialize(); + continue; + } + + var fungibleItemId = addrToFungibleItemIdDict[stateAddr]; + var index = fungibleItemIds.ToList().IndexOf(fungibleItemId); + if (index >= 0 && setToNullForFungibleItemGarages?.ElementAt(index) == true) + { + arr[i] = null; + continue; + } + + var material = new Material(Dictionary.Empty + .SetItem("id", 400_000.Serialize()) + .SetItem("grade", 1.Serialize()) + .SetItem("item_type", ItemType.Material.Serialize()) + .SetItem("item_sub_type", ItemSubType.Hourglass.Serialize()) + .SetItem("elemental_type", ElementalType.Normal.Serialize()) + .SetItem("item_id", HashDigest.FromString(fungibleItemId).Serialize())); + var fig = new FungibleItemGarage(material, 10); + arr[i] = fig.Serialize(); + } + + return arr; + }, + (_, currency) => currency.Ticker switch + { + "NCG" => new FungibleAssetValue(currency, 99, 99), + "CRYSTAL" or "GARAGE" => new FungibleAssetValue( + currency, + 99, + 123456789012345678), + _ => new FungibleAssetValue(currency, 99, 0), + }, + 0L)); + Assert.Null(queryResult.Errors); + var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; + var garages = (Dictionary)data["garages"]; + Assert.Equal(agentAddr.ToString(), garages["agentAddr"]); + Assert.Equal(Addresses.GetGarageBalanceAddress(agentAddr).ToString(), garages["garageBalancesAddr"]); + if (currencyEnums is not null) + { + var garageBalances = ((object[])garages["garageBalances"]).OfType>(); + Assert.Equal(currencyEnums.Count(), garageBalances.Count()); + foreach (var (currencyEnum, garageBalance) in currencyEnums.Zip(garageBalances)) + { + Assert.Equal( + currencyEnum.ToString(), + ((Dictionary)garageBalance["currency"])["ticker"]); + } + } + else if (currencyTickers is not null) + { + var garageBalances = ((object[])garages["garageBalances"]).OfType>(); + Assert.Equal(currencyTickers.Count(), garageBalances.Count()); + foreach (var (currencyTicker, garageBalance) in currencyTickers.Zip(garageBalances)) + { + Assert.Equal( + currencyTicker, + ((Dictionary)garageBalance["currency"])["ticker"]); + } + } + + if (fungibleItemIds is not null) + { + var fungibleItemGarages = + ((object[])garages["fungibleItemGarages"]).OfType>(); + Assert.Equal(fungibleItemIds.Count(), fungibleItemGarages.Count()); + if (setToNullForFungibleItemGarages is null) + { + foreach (var (fungibleItemId, fungibleItemGarage) in fungibleItemIds + .Zip(fungibleItemGarages)) + { + var actualFungibleItemId = fungibleItemGarage["fungibleItemId"]; + Assert.Equal(fungibleItemId, actualFungibleItemId); + var actualAddr = fungibleItemGarage["addr"]; + Assert.Equal( + Addresses.GetGarageAddress( + agentAddr, + HashDigest.FromString(fungibleItemId)).ToString(), + actualAddr); + var actual = ((Dictionary)fungibleItemGarage["item"])["fungibleItemId"]; + Assert.Equal(fungibleItemId, actual); + } + } + else + { + foreach (var ((fungibleItemId, setToNull), fungibleItemGarage) in fungibleItemIds + .Zip(setToNullForFungibleItemGarages) + .Zip(fungibleItemGarages)) + { + var actualFungibleItemId = fungibleItemGarage["fungibleItemId"]; + Assert.Equal(fungibleItemId, actualFungibleItemId); + var actualAddr = fungibleItemGarage["addr"]; + Assert.Equal( + Addresses.GetGarageAddress( + agentAddr, + HashDigest.FromString(fungibleItemId)).ToString(), + actualAddr); + if (setToNull) + { + Assert.Null(fungibleItemGarage["item"]); + } + else + { + var actual = ((Dictionary)fungibleItemGarage["item"])["fungibleItemId"]; + Assert.Equal(fungibleItemId, actual); + } + } + } + } + } + + private static IEnumerable GetMemberDataOfGarages() + { + var agentAddr = new PrivateKey().ToAddress(); + yield return new object[] + { + agentAddr, + null, + null, + null, + null, + }; + yield return new object[] + { + agentAddr, + new[] { CurrencyEnum.NCG, CurrencyEnum.CRYSTAL, CurrencyEnum.GARAGE }, + null, + null, + null, + }; + yield return new object[] + { + agentAddr, + null, + new[] { "NCG", "CRYSTAL", "GARAGE" }, + null, + null, + }; + yield return new object[] + { + agentAddr, + null, + null, + new[] { new HashDigest().ToString() }, + null, + }; + yield return new object[] + { + agentAddr, + null, + null, + new[] { new HashDigest().ToString() }, + new[] { true }, + }; + yield return new object[] + { + agentAddr, + new[] { CurrencyEnum.NCG, CurrencyEnum.CRYSTAL, CurrencyEnum.GARAGE }, + null, + new[] { new HashDigest().ToString() }, + null, + }; + yield return new object[] + { + agentAddr, + null, + new[] { "NCG", "CRYSTAL", "GARAGE" }, + new[] { new HashDigest().ToString() }, + null, + }; + } + } +} diff --git a/NineChronicles.Headless.Tests/GraphTypes/States/Models/AvatarStateTypeTest.cs b/NineChronicles.Headless.Tests/GraphTypes/States/Models/AvatarStateTypeTest.cs index 694360930..081f7642c 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/States/Models/AvatarStateTypeTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/States/Models/AvatarStateTypeTest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Bencodex.Types; using GraphQL.Execution; @@ -53,6 +54,53 @@ public async Task Query(AvatarState avatarState, Dictionary expe Assert.Equal(expected, data); } + [Theory] + [MemberData(nameof(CombinationSlotSteteMembers))] + public async Task QueryWithCombinationSlotState(AvatarState avatarState, Dictionary expected) + { + const string query = @" + { + address + combinationSlots { + address + unlockBlockIndex + unlockStage + startBlockIndex + petId + } + } + "; + var queryResult = await ExecuteQueryAsync( + query, + source: new AvatarStateType.AvatarStateContext( + avatarState, + addresses => addresses.Select(x => + { + if (x == Fixtures.AvatarAddress) + { + return Fixtures.AvatarStateFX.Serialize(); + } + + if (x == Fixtures.UserAddress) + { + return Fixtures.AgentStateFx.Serialize(); + } + + var combinationSlotAddressIndex = + Fixtures.AvatarStateFX.combinationSlotAddresses.FindIndex(address => x == address); + if (combinationSlotAddressIndex > -1) + { + return Fixtures.CombinationSlotStatesFx[combinationSlotAddressIndex].Serialize(); + } + + return null; + }).ToList(), + (_, _) => new FungibleAssetValue(), + 0)); + var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; + Assert.Equal(expected, data); + } + public static IEnumerable Members => new List { new object[] @@ -66,5 +114,25 @@ public async Task Query(AvatarState avatarState, Dictionary expe }, }, }; + + public static IEnumerable CombinationSlotSteteMembers = new List() + { + new object[] + { + Fixtures.AvatarStateFX, + new Dictionary + { + ["address"] = Fixtures.AvatarAddress.ToString(), + ["combinationSlots"] = Fixtures.CombinationSlotStatesFx.Select(x => new Dictionary + { + ["address"] = x.address.ToString(), + ["unlockBlockIndex"] = x.UnlockBlockIndex, + ["unlockStage"] = x.UnlockStage, + ["startBlockIndex"] = x.StartBlockIndex, + ["petId"] = x.PetId + }).ToArray(), + } + } + }; } } diff --git a/NineChronicles.Headless.Tests/GraphTypes/States/Models/CombinationSlotStateTypeTest.cs b/NineChronicles.Headless.Tests/GraphTypes/States/Models/CombinationSlotStateTypeTest.cs index a4a73ea6f..a4bd3e850 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/States/Models/CombinationSlotStateTypeTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/States/Models/CombinationSlotStateTypeTest.cs @@ -29,9 +29,9 @@ public async Task Query() var expected = new Dictionary() { ["address"] = address.ToString(), - ["unlockBlockIndex"] = 0, + ["unlockBlockIndex"] = 0L, ["unlockStage"] = 1, - ["startBlockIndex"] = 0, + ["startBlockIndex"] = 0L, }; Assert.Equal(expected, data); } diff --git a/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs b/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs index 835dec679..b4f9b87c1 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs @@ -25,6 +25,7 @@ using Nekoyume.Action.Loader; using NineChronicles.Headless.GraphTypes; using NineChronicles.Headless.Tests.Common; +using NineChronicles.Headless.Utils; using Xunit; using static NineChronicles.Headless.NCActionUtils; @@ -343,11 +344,15 @@ public async Task TransactionResultIsSuccess() private Task ExecuteAsync(string query) { + var currencyFactory = new CurrencyFactory(_blockChain.GetStates); + var fungibleAssetValueFactory = new FungibleAssetValueFactory(currencyFactory); return GraphQLTestUtils.ExecuteQueryAsync(query, standaloneContext: new StandaloneContext { BlockChain = _blockChain, Store = _store, - NineChroniclesNodeService = _service + NineChroniclesNodeService = _service, + CurrencyFactory = currencyFactory, + FungibleAssetValueFactory = fungibleAssetValueFactory, }); } diff --git a/NineChronicles.Headless/ActionEvaluationPublisher.cs b/NineChronicles.Headless/ActionEvaluationPublisher.cs index 814067eb1..cdda26dff 100644 --- a/NineChronicles.Headless/ActionEvaluationPublisher.cs +++ b/NineChronicles.Headless/ActionEvaluationPublisher.cs @@ -183,10 +183,7 @@ private sealed class Client : IAsyncDisposable private readonly Address _clientAddress; private IDisposable? _blockSubscribe; - private IDisposable? _reorgSubscribe; - private IDisposable? _reorgEndSubscribe; private IDisposable? _actionEveryRenderSubscribe; - private IDisposable? _actionEveryUnrenderSubscribe; private IDisposable? _everyExceptionSubscribe; private IDisposable? _nodeStatusSubscribe; @@ -248,49 +245,6 @@ await _hub.BroadcastRenderBlockAsync( } } ); - _reorgSubscribe = blockRenderer.ReorgSubject - .SubscribeOn(NewThreadScheduler.Default) - .ObserveOn(NewThreadScheduler.Default) - .Subscribe( - async ev => - { - try - { - await _hub.ReportReorgAsync( - Codec.Encode(ev.OldTip.MarshalBlock()), - Codec.Encode(ev.NewTip.MarshalBlock()), - Codec.Encode(ev.Branchpoint.MarshalBlock()) - ); - } - catch (Exception e) - { - // FIXME add logger as property - Log.Error(e, "Skip broadcasting reorg due to the unexpected exception"); - } - } - ); - - _reorgEndSubscribe = blockRenderer.ReorgEndSubject - .SubscribeOn(NewThreadScheduler.Default) - .ObserveOn(NewThreadScheduler.Default) - .Subscribe( - async ev => - { - try - { - await _hub.ReportReorgEndAsync( - Codec.Encode(ev.OldTip.MarshalBlock()), - Codec.Encode(ev.NewTip.MarshalBlock()), - Codec.Encode(ev.Branchpoint.MarshalBlock()) - ); - } - catch (Exception e) - { - // FIXME add logger as property - Log.Error(e, "Skip broadcasting reorg end due to the unexpected exception"); - } - } - ); _actionEveryRenderSubscribe = actionRenderer.EveryRender() .Where(ContainsAddressToBroadcast) @@ -306,7 +260,7 @@ await _hub.ReportReorgEndAsync( : ev.Action; var extra = new Dictionary(); - var previousStates = ev.PreviousStates; + var previousStates = ev.PreviousState; if (pa is IBattleArenaV1 battleArena) { var enemyAvatarAddress = battleArena.EnemyAvatarAddress; @@ -350,7 +304,7 @@ await _hub.ReportReorgEndAsync( } } - var eval = new NCActionEvaluation(pa, ev.Signer, ev.BlockIndex, ev.OutputStates, ev.Exception, previousStates, ev.RandomSeed, extra); + var eval = new NCActionEvaluation(pa, ev.Signer, ev.BlockIndex, ev.OutputState, ev.Exception, previousStates, ev.RandomSeed, extra); var encoded = MessagePackSerializer.Serialize(eval); var c = new MemoryStream(); await using (var df = new DeflateStream(c, CompressionLevel.Fastest)) @@ -389,59 +343,6 @@ await _hub.ReportReorgEndAsync( } ); - _actionEveryUnrenderSubscribe = actionRenderer.EveryUnrender() - .Where(ContainsAddressToBroadcast) - .SubscribeOn(NewThreadScheduler.Default) - .ObserveOn(NewThreadScheduler.Default) - .Subscribe( - async ev => - { - ActionBase? pa = null; - if (!(ev.Action is RewardGold)) - { - pa = ev.Action; - } - try - { - var eval = new NCActionEvaluation(pa, - ev.Signer, - ev.BlockIndex, - ev.OutputStates, - ev.Exception, - ev.PreviousStates, - ev.RandomSeed, - new Dictionary() - ); - var encoded = MessagePackSerializer.Serialize(eval); - var c = new MemoryStream(); - using (var df = new DeflateStream(c, CompressionLevel.Fastest)) - { - df.Write(encoded, 0, encoded.Length); - } - - var compressed = c.ToArray(); - Log.Information( - "[{ClientAddress}] #{BlockIndex} Broadcasting unrender since the given action {Action}. eval size: {Size}", - _clientAddress, - ev.BlockIndex, - ev.Action.GetType(), - compressed.LongLength - ); - await _hub.BroadcastRenderAsync(compressed); - } - catch (SerializationException se) - { - // FIXME add logger as property - Log.Error(se, "Skip broadcasting unrender since the given action isn't serializable."); - } - catch (Exception e) - { - // FIXME add logger as property - Log.Error(e, "Skip broadcasting unrender due to the unexpected exception"); - } - } - ); - _everyExceptionSubscribe = exceptionRenderer.EveryException() .SubscribeOn(NewThreadScheduler.Default) .ObserveOn(NewThreadScheduler.Default) @@ -492,10 +393,7 @@ await _hub.ReportReorgEndAsync( public async ValueTask DisposeAsync() { _blockSubscribe?.Dispose(); - _reorgSubscribe?.Dispose(); - _reorgEndSubscribe?.Dispose(); _actionEveryRenderSubscribe?.Dispose(); - _actionEveryUnrenderSubscribe?.Dispose(); _everyExceptionSubscribe?.Dispose(); _nodeStatusSubscribe?.Dispose(); await _hub.DisposeAsync(); @@ -510,15 +408,13 @@ private bool ContainsAddressToBroadcast(ActionEvaluation ev) private bool ContainsAddressToBroadcastLocal(ActionEvaluation ev) { - var updatedAddresses = - ev.OutputStates.UpdatedAddresses.Union(ev.OutputStates.UpdatedFungibleAssets.Keys); + var updatedAddresses = ev.OutputState.Delta.UpdatedAddresses; return _context.AddressesToSubscribe.Any(updatedAddresses.Add(ev.Signer).Contains); } private bool ContainsAddressToBroadcastRemoteClient(ActionEvaluation ev) { - var updatedAddresses = - ev.OutputStates.UpdatedAddresses.Union(ev.OutputStates.UpdatedFungibleAssets.Keys); + var updatedAddresses = ev.OutputState.Delta.UpdatedAddresses; return TargetAddresses.Any(updatedAddresses.Add(ev.Signer).Contains); } } diff --git a/NineChronicles.Headless/Controllers/GraphQLController.cs b/NineChronicles.Headless/Controllers/GraphQLController.cs index af62ecce5..b15de3a3f 100644 --- a/NineChronicles.Headless/Controllers/GraphQLController.cs +++ b/NineChronicles.Headless/Controllers/GraphQLController.cs @@ -235,7 +235,7 @@ private void NotifyAction(ActionEvaluation eval) return; } Address address = StandaloneContext.NineChroniclesNodeService.MinerPrivateKey.PublicKey.ToAddress(); - if (eval.OutputStates.UpdatedAddresses.Contains(address) || eval.Signer == address) + if (eval.OutputState.Delta.UpdatedAddresses.Contains(address) || eval.Signer == address) { if (eval.Signer == address) { diff --git a/NineChronicles.Headless/GraphTypes/ActionQuery.DevEx.cs b/NineChronicles.Headless/GraphTypes/ActionQuery.DevEx.cs index 3a1335c7c..712daa8b4 100644 --- a/NineChronicles.Headless/GraphTypes/ActionQuery.DevEx.cs +++ b/NineChronicles.Headless/GraphTypes/ActionQuery.DevEx.cs @@ -66,7 +66,7 @@ private void RegisterFieldsForDevEx() }), resolve: context => { - if (standaloneContext.BlockChain is not { } chain) + if (StandaloneContext.BlockChain is not { } chain) { throw new InvalidOperationException( "BlockChain not found in the context"); diff --git a/NineChronicles.Headless/GraphTypes/ActionQuery.cs b/NineChronicles.Headless/GraphTypes/ActionQuery.cs index e4e136591..84fb2999f 100644 --- a/NineChronicles.Headless/GraphTypes/ActionQuery.cs +++ b/NineChronicles.Headless/GraphTypes/ActionQuery.cs @@ -7,7 +7,6 @@ using GraphQL; using GraphQL.Types; using Libplanet; -using Libplanet.Action; using Libplanet.Assets; using Libplanet.Explorer.GraphTypes; using Nekoyume.Action; @@ -21,12 +20,12 @@ namespace NineChronicles.Headless.GraphTypes { public partial class ActionQuery : ObjectGraphType { - private static readonly Codec Codec = new Codec(); - internal StandaloneContext standaloneContext { get; set; } + private static readonly Codec Codec = new(); + internal StandaloneContext StandaloneContext { get; set; } public ActionQuery(StandaloneContext standaloneContext) { - this.standaloneContext = standaloneContext; + StandaloneContext = standaloneContext; Field( name: "stake", @@ -188,14 +187,12 @@ public ActionQuery(StandaloneContext standaloneContext) { var sender = context.GetArgument
("sender"); var recipient = context.GetArgument
("recipient"); - Currency currency = context.GetArgument("currency") switch + var currencyEnum = context.GetArgument("currency"); + if (!standaloneContext.CurrencyFactory!.TryGetCurrency(currencyEnum, out var currency)) { - CurrencyEnum.NCG => new GoldCurrencyState( - (Dictionary)standaloneContext.BlockChain!.GetState(GoldCurrencyState.Address) - ).Currency, - CurrencyEnum.CRYSTAL => CrystalCalculator.CRYSTAL, - _ => throw new ExecutionError("Unsupported Currency type.") - }; + throw new ExecutionError($"Currency {currencyEnum} is not found."); + } + var amount = FungibleAssetValue.Parse(currency, context.GetArgument("amount")); var memo = context.GetArgument("memo"); ActionBase action = new TransferAsset(sender, recipient, amount, memo); @@ -404,6 +401,7 @@ public ActionQuery(StandaloneContext standaloneContext) ); Field>( "activateAccount", + deprecationReason: "Since NCIP-15, it doesn't care account activation.", arguments: new QueryArguments( new QueryArgument> { @@ -544,6 +542,7 @@ public ActionQuery(StandaloneContext standaloneContext) RegisterRapidCombination(); RegisterCombinationConsumable(); RegisterMead(); + RegisterGarages(); Field>( name: "craftQuery", diff --git a/NineChronicles.Headless/GraphTypes/ActionQueryFields/Garages.cs b/NineChronicles.Headless/GraphTypes/ActionQueryFields/Garages.cs new file mode 100644 index 000000000..0c6d3ff2c --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/ActionQueryFields/Garages.cs @@ -0,0 +1,202 @@ +using System.Collections.Generic; +using System.Security.Cryptography; +using GraphQL; +using GraphQL.Types; +using Libplanet; +using Libplanet.Assets; +using Libplanet.Explorer.GraphTypes; +using Nekoyume.Action; +using Nekoyume.Action.Garages; +using NineChronicles.Headless.GraphTypes.Input; + +namespace NineChronicles.Headless.GraphTypes +{ + public partial class ActionQuery + { + private void RegisterGarages() + { + Field>( + "loadIntoMyGarages", + arguments: new QueryArguments( + new QueryArgument>> + { + Name = "fungibleAssetValues", + Description = "Array of balance address and currency ticker and quantity.", + }, + new QueryArgument + { + Name = "inventoryAddr", + Description = "Inventory Address", + }, + new QueryArgument>> + { + Name = "fungibleIdAndCounts", + Description = "Array of fungible ID and count", + }, + new QueryArgument + { + Name = "memo", + Description = "Memo", + } + ), + resolve: context => + { + var balanceInputList = context.GetArgument?>("fungibleAssetValues"); + List<(Address address, FungibleAssetValue fungibleAssetValue)>? fungibleAssetValues = null; + if (balanceInputList is not null) + { + fungibleAssetValues = new List<(Address address, FungibleAssetValue fungibleAssetValue)>(); + foreach (var (balanceAddr, (currencyTicker, value)) in balanceInputList) + { + if (StandaloneContext.FungibleAssetValueFactory!.TryGetFungibleAssetValue(currencyTicker, value, out var fav)) + { + fungibleAssetValues.Add((balanceAddr, fav)); + } + else + { + throw new ExecutionError($"Invalid currency ticker: {currencyTicker}"); + } + } + } + + var inventoryAddr = context.GetArgument("inventoryAddr"); + var fungibleIdAndCounts = context.GetArgument fungibleId, + int count)>?>("fungibleIdAndCounts"); + var memo = context.GetArgument("memo"); + + ActionBase action = new LoadIntoMyGarages( + fungibleAssetValues, + inventoryAddr, + fungibleIdAndCounts, + memo); + return Encode(context, action); + } + ); + + Field>( + "deliverToOthersGarages", + arguments: new QueryArguments( + new QueryArgument> + { + Name = "recipientAgentAddr", + Description = "Recipient agent address", + }, + new QueryArgument>> + { + Name = "fungibleAssetValues", + Description = "Array of currency ticket and quantity to deliver.", + }, + new QueryArgument>> + { + Name = "fungibleIdAndCounts", + Description = "Array of Fungible ID and count to deliver.", + }, + new QueryArgument + { + Name = "memo", + Description = "Memo", + } + ), + resolve: context => + { + var recipientAgentAddr = context.GetArgument
("recipientAgentAddr"); + var fungibleAssetValueInputList = context.GetArgument?>("fungibleAssetValues"); + List? fungibleAssetValues = null; + if (fungibleAssetValueInputList is not null) + { + fungibleAssetValues = new List(); + foreach (var (currencyTicker, value) in fungibleAssetValueInputList) + { + if (StandaloneContext.FungibleAssetValueFactory!.TryGetFungibleAssetValue(currencyTicker, value, out var fav)) + { + fungibleAssetValues.Add(fav); + } + else + { + throw new ExecutionError($"Invalid currency ticker: {currencyTicker}"); + } + } + } + + var fungibleIdAndCounts = context.GetArgument fungibleId, + int count)>?>("fungibleIdAndCounts"); + var memo = context.GetArgument("memo"); + + ActionBase action = new DeliverToOthersGarages( + recipientAgentAddr, + fungibleAssetValues, + fungibleIdAndCounts, + memo); + return Encode(context, action); + } + ); + + Field>( + "unloadFromMyGarages", + arguments: new QueryArguments( + new QueryArgument> + { + Name = "recipientAvatarAddr", + Description = "Recipient avatar address", + }, + new QueryArgument>> + { + Name = "fungibleAssetValues", + Description = "Array of balance address and currency ticker and quantity to send.", + }, + new QueryArgument>> + { + Name = "fungibleIdAndCounts", + Description = "Array of fungible ID and count to send.", + }, + new QueryArgument + { + Name = "memo", + Description = "Memo", + } + ), + resolve: context => + { + var recipientAvatarAddr = context.GetArgument
("recipientAvatarAddr"); + var balanceInputList = context.GetArgument?>("fungibleAssetValues"); + List<(Address address, FungibleAssetValue fungibleAssetValue)>? fungibleAssetValues = null; + if (balanceInputList is not null) + { + fungibleAssetValues = new List<(Address address, FungibleAssetValue fungibleAssetValue)>(); + foreach (var (addr, (currencyTicker, value)) in balanceInputList) + { + if (StandaloneContext.FungibleAssetValueFactory!.TryGetFungibleAssetValue(currencyTicker, value, out var fav)) + { + fungibleAssetValues.Add((addr, fav)); + } + else + { + throw new ExecutionError($"Invalid currency ticker: {currencyTicker}"); + } + } + } + + var fungibleIdAndCounts = context.GetArgument fungibleId, + int count)>?>("fungibleIdAndCounts"); + var memo = context.GetArgument("memo"); + + ActionBase action = new UnloadFromMyGarages( + recipientAvatarAddr, + fungibleAssetValues, + fungibleIdAndCounts, + memo); + return Encode(context, action); + } + ); + } + } +} diff --git a/NineChronicles.Headless/GraphTypes/ActionQueryFields/HackAndSlash.cs b/NineChronicles.Headless/GraphTypes/ActionQueryFields/HackAndSlash.cs index 921baa906..959f0127c 100644 --- a/NineChronicles.Headless/GraphTypes/ActionQueryFields/HackAndSlash.cs +++ b/NineChronicles.Headless/GraphTypes/ActionQueryFields/HackAndSlash.cs @@ -71,7 +71,7 @@ private void RegisterHackAndSlash() context.GetArgument?>("runeSlotInfos") ?? new List(); int? stageBuffId = context.GetArgument("stageBuffId"); - if (!(standaloneContext.BlockChain is { } chain)) + if (!(StandaloneContext.BlockChain is { } chain)) { throw new InvalidOperationException("BlockChain not found in the context"); } diff --git a/NineChronicles.Headless/GraphTypes/ActionTxQuery.cs b/NineChronicles.Headless/GraphTypes/ActionTxQuery.cs index a9908021d..1acfebc2c 100644 --- a/NineChronicles.Headless/GraphTypes/ActionTxQuery.cs +++ b/NineChronicles.Headless/GraphTypes/ActionTxQuery.cs @@ -19,10 +19,10 @@ public ActionTxQuery(StandaloneContext standaloneContext) : base(standaloneConte internal override byte[] Encode(IResolveFieldContext context, ActionBase action) { var publicKey = new PublicKey(ByteUtil.ParseHex(context.Parent!.GetArgument("publicKey"))); - if (!(standaloneContext.BlockChain is BlockChain blockChain)) + if (!(StandaloneContext.BlockChain is BlockChain blockChain)) { throw new ExecutionError( - $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!"); + $"{nameof(Headless.StandaloneContext)}.{nameof(Headless.StandaloneContext.BlockChain)} was not set yet!"); } Address signer = publicKey.ToAddress(); diff --git a/NineChronicles.Headless/GraphTypes/ActivationStatusMutation.cs b/NineChronicles.Headless/GraphTypes/ActivationStatusMutation.cs index b8831a30f..83e6440ad 100644 --- a/NineChronicles.Headless/GraphTypes/ActivationStatusMutation.cs +++ b/NineChronicles.Headless/GraphTypes/ActivationStatusMutation.cs @@ -12,7 +12,10 @@ public class ActivationStatusMutation : ObjectGraphType { public ActivationStatusMutation(NineChroniclesNodeService service) { + DeprecationReason = "Since NCIP-15, it doesn't care account activation."; + Field>("activateAccount", + deprecationReason: "Since NCIP-15, it doesn't care account activation.", arguments: new QueryArguments( new QueryArgument> { diff --git a/NineChronicles.Headless/GraphTypes/ActivationStatusQuery.cs b/NineChronicles.Headless/GraphTypes/ActivationStatusQuery.cs index 27ff51632..ef2002563 100644 --- a/NineChronicles.Headless/GraphTypes/ActivationStatusQuery.cs +++ b/NineChronicles.Headless/GraphTypes/ActivationStatusQuery.cs @@ -15,8 +15,11 @@ public class ActivationStatusQuery : ObjectGraphType { public ActivationStatusQuery(StandaloneContext standaloneContext) { + DeprecationReason = "Since NCIP-15, it doesn't care account activation."; + Field>( name: "activated", + deprecationReason: "Since NCIP-15, it doesn't care account activation.", resolve: context => { var service = standaloneContext.NineChroniclesNodeService; @@ -72,6 +75,7 @@ public ActivationStatusQuery(StandaloneContext standaloneContext) Field>( name: "addressActivated", + deprecationReason: "Since NCIP-15, it doesn't care account activation.", arguments: new QueryArguments( new QueryArgument> { diff --git a/NineChronicles.Headless/GraphTypes/AddressQuery.cs b/NineChronicles.Headless/GraphTypes/AddressQuery.cs index 0fe7901a5..0336232b9 100644 --- a/NineChronicles.Headless/GraphTypes/AddressQuery.cs +++ b/NineChronicles.Headless/GraphTypes/AddressQuery.cs @@ -91,20 +91,13 @@ public AddressQuery(StandaloneContext standaloneContext) }), resolve: context => { - var currency = context.GetArgument("currency"); - switch (currency) + var currencyEnum = context.GetArgument("currency"); + if (!standaloneContext.CurrencyFactory!.TryGetCurrency(currencyEnum, out var currency)) { - case CurrencyEnum.NCG: - var blockchain = standaloneContext.BlockChain!; - var goldCurrencyStateDict = - (Dictionary)blockchain.GetState(Addresses.GoldCurrency); - var goldCurrencyState = new GoldCurrencyState(goldCurrencyStateDict); - return goldCurrencyState.Currency.Minters; - case CurrencyEnum.CRYSTAL: - return CrystalCalculator.CRYSTAL.Minters; - default: - throw new SwitchExpressionException(currency); + throw new ExecutionError($"Currency {currencyEnum} is not found."); } + + return currency.Minters; }); Field>( name: "pledgeAddress", diff --git a/NineChronicles.Headless/GraphTypes/BalanceInputType.cs b/NineChronicles.Headless/GraphTypes/BalanceInputType.cs new file mode 100644 index 000000000..fd1a292f4 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/BalanceInputType.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using GraphQL.Types; +using Libplanet; +using Libplanet.Explorer.GraphTypes; + +namespace NineChronicles.Headless.GraphTypes +{ + public class BalanceInputType : InputObjectGraphType<( + Address balanceAddr, + SimplifyFungibleAssetValueInputType valueTuple)> + { + public BalanceInputType() + { + Name = "BalanceInput"; + + Field( + name: "balanceAddr", + description: "Balance Address." + ); + + Field( + name: "value", + description: "Fungible asset value ticker and amount." + ); + } + + public override object ParseDictionary(IDictionary value) + { + var addr = (Address)value["balanceAddr"]!; + var favTuple = ((string currencyTicker, string value))value["value"]!; + return (addr, favTuple); + } + } +} diff --git a/NineChronicles.Headless/GraphTypes/CurrencyEnumType.cs b/NineChronicles.Headless/GraphTypes/CurrencyEnumType.cs index 878185771..a3f91ac4b 100644 --- a/NineChronicles.Headless/GraphTypes/CurrencyEnumType.cs +++ b/NineChronicles.Headless/GraphTypes/CurrencyEnumType.cs @@ -8,6 +8,7 @@ public enum CurrencyEnum { CRYSTAL, NCG, + GARAGE, } public class CurrencyEnumType : EnumerationGraphType diff --git a/NineChronicles.Headless/GraphTypes/FungibleIdAndCountInputType.cs b/NineChronicles.Headless/GraphTypes/FungibleIdAndCountInputType.cs new file mode 100644 index 000000000..61164c9e2 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/FungibleIdAndCountInputType.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using GraphQL.Types; +using Libplanet; +using System.Security.Cryptography; + +namespace NineChronicles.Headless.GraphTypes.Input +{ + public class FungibleIdAndCountInputType : + InputObjectGraphType<(HashDigest fungibleId, int count)> + { + public FungibleIdAndCountInputType() + { + Name = "FungibleIdAndCountInput"; + + Field>( + name: "fungibleId", + description: "Fungible ID"); + + Field>( + name: "count", + description: "Count"); + } + + public override object ParseDictionary(IDictionary value) + { + var hexDigest = (string)value["fungibleId"]!; + var fungibleId = HashDigest.FromString(hexDigest); + var count = (int)value["count"]!; + return (fungibleId, count); + } + } +} diff --git a/NineChronicles.Headless/GraphTypes/FungibleItemIdInputType.cs b/NineChronicles.Headless/GraphTypes/FungibleItemIdInputType.cs new file mode 100644 index 000000000..cc2cc97d5 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/FungibleItemIdInputType.cs @@ -0,0 +1,59 @@ +#nullable enable + +using System.Collections.Generic; +using System.Security.Cryptography; +using GraphQL; +using GraphQL.Types; +using Libplanet; +using Nekoyume.TableData; + +namespace NineChronicles.Headless.GraphTypes; + +public class FungibleItemIdInputType : InputObjectGraphType<(string? fungibleItemId, int? itemSheetId)> +{ + public static void SetFields(ComplexGraphType graphType) + { + graphType.Field( + name: "fungibleItemId", + description: "A fungible item id to be loaded."); + + graphType.Field( + name: "itemSheetId", + description: "(not recommended)A item sheet id to be loaded.\n" + + "It can be does not match with the actual fungible item id " + + "if the item sheets were patched."); + } + + public static (string? fungibleItemId, int? itemSheetId) Parse( + IDictionary value) + { + if (value.TryGetValue("fungibleItemId", out var fungibleItemId)) + { + if (value.TryGetValue("itemSheetId", out _)) + { + throw new ExecutionError("fungibleItemId and itemSheetId cannot be specified at the same time."); + } + + return ((string)fungibleItemId!, null); + } + + if (value.TryGetValue("itemSheetId", out var itemSheetId)) + { + return (null, int.Parse((string)itemSheetId!)); + } + + throw new ExecutionError("fungibleItemId or itemSheetId must be specified."); + } + + public FungibleItemIdInputType() + { + Name = "FungibleItemIdInput"; + Description = "A fungible item id to be loaded. Use either fungibleItemId or itemSheetId."; + SetFields(this); + } + + public override object ParseDictionary(IDictionary value) + { + return Parse(value); + } +} diff --git a/NineChronicles.Headless/GraphTypes/SimplifyCurrencyInputType.cs b/NineChronicles.Headless/GraphTypes/SimplifyCurrencyInputType.cs new file mode 100644 index 000000000..7f78508f5 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/SimplifyCurrencyInputType.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using GraphQL; +using GraphQL.Types; + +namespace NineChronicles.Headless.GraphTypes; + +public class SimplifyCurrencyInputType : InputObjectGraphType +{ + public static void SetFields(ComplexGraphType graphType) + { + graphType.Field( + name: "currencyEnum", + description: "A currency type to be loaded."); + + graphType.Field( + name: "currencyTicker", + description: "A currency ticker to be loaded."); + } + + public static string Parse(IDictionary value) + { + if (value.TryGetValue("currencyEnum", out var currencyEnum)) + { + if (value.ContainsKey("currencyTicker")) + { + throw new ExecutionError("currencyEnum and currencyTicker cannot be specified at the same time."); + } + + return ((CurrencyEnum)currencyEnum!).ToString(); + } + + if (value.TryGetValue("currencyTicker", out var currencyTicker)) + { + return (string)currencyTicker!; + } + + throw new ExecutionError("currencyEnum or currencyTicker must be specified."); + } + + public SimplifyCurrencyInputType() + { + Name = "SimplifyCurrencyInput"; + Description = "A currency ticker to be loaded. Use either currencyEnum or currencyTicker."; + SetFields(this); + } + + public override object ParseDictionary(IDictionary value) + { + return Parse(value); + } +} diff --git a/NineChronicles.Headless/GraphTypes/SimplifyFungibleAssetValueInputType.cs b/NineChronicles.Headless/GraphTypes/SimplifyFungibleAssetValueInputType.cs new file mode 100644 index 000000000..668fa6d03 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/SimplifyFungibleAssetValueInputType.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using GraphQL; +using GraphQL.Types; + +namespace NineChronicles.Headless.GraphTypes; + +public class SimplifyFungibleAssetValueInputType : + InputObjectGraphType<(string currencyTicker, string value)> +{ + public SimplifyFungibleAssetValueInputType() + { + Name = "SimplifyFungibleAssetValueInput"; + Description = "A fungible asset value ticker and amount." + + "You can specify either currencyEnum or currencyTicker."; + + SimplifyCurrencyInputType.SetFields(this); + Field>( + name: "value", + description: "A numeric string to parse. Can consist of digits, " + + "plus (+), minus (-), and decimal separator (.)." + + " "); + } + + public override object ParseDictionary(IDictionary value) + { + var value2 = (string)value["value"]!; + var currencyTicker = SimplifyCurrencyInputType.Parse(value); + return (currencyTicker, value: value2); + } +} diff --git a/NineChronicles.Headless/GraphTypes/StandaloneMutation.cs b/NineChronicles.Headless/GraphTypes/StandaloneMutation.cs index 6916d1a1e..1421a5e47 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneMutation.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneMutation.cs @@ -36,7 +36,8 @@ IConfiguration configuration Field( name: "activationStatus", - resolve: _ => new ActivationStatusMutation(nodeService)); + resolve: _ => new ActivationStatusMutation(nodeService), + deprecationReason: "Since NCIP-15, it doesn't care account activation."); Field( name: "action", diff --git a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs index cb98ed722..b10124de1 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs @@ -56,7 +56,7 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi blockHash switch { BlockHash bh => chain[bh].Index, - null => chain.Tip.Index, + null => chain.Tip!.Index, } ); } @@ -177,6 +177,7 @@ TransferNCGHistory ToTransferNCGHistory(TxSuccess txSuccess, string? memo) 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)) .AuthorizeWithLocalPolicyIf(useSecretToken); @@ -355,6 +356,7 @@ TransferNCGHistory ToTransferNCGHistory(TxSuccess txSuccess, string? memo) Field>( name: "activated", + deprecationReason: "Since NCIP-15, it doesn't care account activation.", arguments: new QueryArguments( new QueryArgument> { @@ -389,6 +391,7 @@ TransferNCGHistory ToTransferNCGHistory(TxSuccess txSuccess, string? memo) Field>( name: "activationKeyNonce", + deprecationReason: "Since NCIP-15, it doesn't care account activation.", arguments: new QueryArguments( new QueryArgument> { diff --git a/NineChronicles.Headless/GraphTypes/StandaloneSchema.cs b/NineChronicles.Headless/GraphTypes/StandaloneSchema.cs index e2b8631ed..4d05418df 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneSchema.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneSchema.cs @@ -1,6 +1,5 @@ using System; using GraphQL.Types; -using GraphQL.Utilities; using Microsoft.Extensions.DependencyInjection; namespace NineChronicles.Headless.GraphTypes diff --git a/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs b/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs index 25c1e27df..0016b4f5c 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs @@ -354,7 +354,7 @@ private void RenderMonsterCollectionStateSubject(ActionEvaluation eval) var agentState = new AgentState(agentDict); Address deriveAddress = MonsterCollectionState.DeriveAddress(address, agentState.MonsterCollectionRound); var subject = subjects.stateSubject; - if (eval.OutputStates.GetState(deriveAddress) is Dictionary state) + if (eval.OutputState.GetState(deriveAddress) is Dictionary state) { subject.OnNext(new MonsterCollectionState(state)); } diff --git a/NineChronicles.Headless/GraphTypes/StateQuery.cs b/NineChronicles.Headless/GraphTypes/StateQuery.cs index 787c4335b..21985c16c 100644 --- a/NineChronicles.Headless/GraphTypes/StateQuery.cs +++ b/NineChronicles.Headless/GraphTypes/StateQuery.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using Bencodex.Types; using GraphQL; using GraphQL.Types; +using Lib9c; using Lib9c.Model.Order; using Libplanet; using Libplanet.Assets; @@ -24,7 +26,7 @@ namespace NineChronicles.Headless.GraphTypes { - public class StateQuery : ObjectGraphType + public partial class StateQuery : ObjectGraphType { public StateQuery() { @@ -539,6 +541,8 @@ public StateQuery() return (address, approved, mead); } ); + + RegisterGarages(); } } } diff --git a/NineChronicles.Headless/GraphTypes/StateQueryFields/Garages.cs b/NineChronicles.Headless/GraphTypes/StateQueryFields/Garages.cs new file mode 100644 index 000000000..27297564d --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/StateQueryFields/Garages.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using Bencodex.Types; +using GraphQL; +using GraphQL.Types; +using Libplanet; +using Libplanet.Assets; +using Libplanet.Explorer.GraphTypes; +using Nekoyume; +using Nekoyume.Model.Garages; +using NineChronicles.Headless.GraphTypes.States; + +namespace NineChronicles.Headless.GraphTypes; + +public partial class StateQuery +{ + private void RegisterGarages() + { + Field( + "garages", + description: "Get balances and fungible items in garages.\n" + + "Use either `currencyEnums` or `currencyTickers` to get balances.", + arguments: new QueryArguments( + new QueryArgument> + { + Name = "agentAddr", + Description = "Agent address to get balances and fungible items in garages", + }, + new QueryArgument>> + { + Name = "currencyEnums", + Description = "List of currency enums to get balances in garages", + }, + new QueryArgument>> + { + Name = "currencyTickers", + Description = "List of currency tickers to get balances in garages", + }, + new QueryArgument>> + { + Name = "fungibleItemIds", + Description = "List of fungible item IDs to get fungible item in garages", + } + ), + resolve: context => + { + var agentAddr = context.GetArgument
("agentAddr"); + var garageBalanceAddr = Addresses.GetGarageBalanceAddress(agentAddr); + var currencyEnums = context.GetArgument("currencyEnums"); + var currencyTickers = context.GetArgument("currencyTickers"); + var garageBalances = new List(); + if (currencyEnums is not null) + { + if (currencyTickers is not null) + { + throw new ExecutionError( + "Use either `currencyEnums` or `currencyTickers` to get balances."); + } + + foreach (var currencyEnum in currencyEnums) + { + if (!context.Source.CurrencyFactory.TryGetCurrency(currencyEnum, out var currency)) + { + throw new ExecutionError($"Invalid currency enum: {currencyEnum}"); + } + + var balance = context.Source.GetBalance(garageBalanceAddr, currency); + garageBalances.Add(balance); + } + } + else if (currencyTickers is not null) + { + foreach (var currencyTicker in currencyTickers) + { + if (!context.Source.CurrencyFactory.TryGetCurrency(currencyTicker, out var currency)) + { + throw new ExecutionError($"Invalid currency ticker: {currencyTicker}"); + } + + var balance = context.Source.GetBalance(garageBalanceAddr, currency); + garageBalances.Add(balance); + } + } + + IEnumerable<(string, Address, FungibleItemGarage?)> fungibleItemGarages; + var fungibleItemIds = context.GetArgument("fungibleItemIds"); + if (fungibleItemIds is null) + { + fungibleItemGarages = Enumerable.Empty<(string, Address, FungibleItemGarage?)>(); + } + else + { + var fungibleItemGarageAddresses = fungibleItemIds + .Select(fungibleItemId => Addresses.GetGarageAddress( + agentAddr, + HashDigest.FromString(fungibleItemId))) + .ToArray(); + fungibleItemGarages = context.Source.GetStates(fungibleItemGarageAddresses) + .Select((value, i) => value is null or Null + ? (fungibleItemIds[i], fungibleItemGarageAddresses[i], null) + : (fungibleItemIds[i], fungibleItemGarageAddresses[i], new FungibleItemGarage(value))); + } + + return new GaragesType.Value( + agentAddr, + garageBalanceAddr, + garageBalances, + fungibleItemGarages); + } + ); + } +} diff --git a/NineChronicles.Headless/GraphTypes/States/AvatarStateType.cs b/NineChronicles.Headless/GraphTypes/States/AvatarStateType.cs index e8636d5fc..6476edf12 100644 --- a/NineChronicles.Headless/GraphTypes/States/AvatarStateType.cs +++ b/NineChronicles.Headless/GraphTypes/States/AvatarStateType.cs @@ -110,6 +110,16 @@ public AvatarStateType() nameof(AvatarState.combinationSlotAddresses), description: "Address list of combination slot.", resolve: context => context.Source.AvatarState.combinationSlotAddresses); + Field>>>( + "combinationSlots", + description: "Combination slots.", + resolve: context => + { + var addresses = context.Source.AvatarState.combinationSlotAddresses; + return context.Source.AccountStateGetter(addresses) + .OfType() + .Select(x => new CombinationSlotState(x)); + }); Field>( nameof(AvatarState.itemMap), description: "List of acquired item ID.", diff --git a/NineChronicles.Headless/GraphTypes/States/CombinationSlotStateType.cs b/NineChronicles.Headless/GraphTypes/States/CombinationSlotStateType.cs index fbc008836..b6742f078 100644 --- a/NineChronicles.Headless/GraphTypes/States/CombinationSlotStateType.cs +++ b/NineChronicles.Headless/GraphTypes/States/CombinationSlotStateType.cs @@ -12,7 +12,7 @@ public CombinationSlotStateType() nameof(CombinationSlotState.address), description: "Address of combination slot.", resolve: context => context.Source.address); - Field>( + Field>( nameof(CombinationSlotState.UnlockBlockIndex), description: "Block index at the combination slot can be usable.", resolve: context => context.Source.UnlockBlockIndex); @@ -20,10 +20,14 @@ public CombinationSlotStateType() nameof(CombinationSlotState.UnlockStage), description: "Stage id at the combination slot unlock.", resolve: context => context.Source.UnlockStage); - Field>( + Field>( nameof(CombinationSlotState.StartBlockIndex), description: "Block index at the combination started.", resolve: context => context.Source.StartBlockIndex); + Field( + nameof(CombinationSlotState.PetId), + description: "Pet id used in equipment", + resolve: context => context.Source.PetId); } } } diff --git a/NineChronicles.Headless/GraphTypes/States/GaragesType.cs b/NineChronicles.Headless/GraphTypes/States/GaragesType.cs new file mode 100644 index 000000000..1d78d0e38 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/States/GaragesType.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using GraphQL.Types; +using Libplanet; +using Libplanet.Assets; +using Libplanet.Explorer.GraphTypes; +using Nekoyume.Model.Garages; +using NineChronicles.Headless.GraphTypes.States.Models.Garage; + +namespace NineChronicles.Headless.GraphTypes.States; + +public class GaragesType : ObjectGraphType +{ + public struct Value + { + public readonly Address AgentAddr; + public readonly Address GarageBalancesAddr; + public readonly IEnumerable GarageBalances; + + public readonly IEnumerable<(string fungibleItemId, Address addr, FungibleItemGarage? fungibleItemGarage)> + FungibleItemGarages; + + public Value( + Address agentAddr, + Address garageBalancesAddr, + IEnumerable garageBalances, + IEnumerable<(string fungibleItemId, Address addr, FungibleItemGarage? fungibleItemGarage)> + fungibleItemGarages) + { + AgentAddr = agentAddr; + GarageBalancesAddr = garageBalancesAddr; + GarageBalances = garageBalances; + FungibleItemGarages = fungibleItemGarages; + } + } + + public GaragesType() + { + Field( + name: "agentAddr", + resolve: context => context.Source.AgentAddr); + Field( + name: "garageBalancesAddr", + resolve: context => context.Source.GarageBalancesAddr); + Field>( + name: "garageBalances", + resolve: context => context.Source.GarageBalances); + Field>( + name: "fungibleItemGarages", + resolve: context => context.Source.FungibleItemGarages); + } +} diff --git a/NineChronicles.Headless/GraphTypes/States/Models/Garage/FungibleItemGarageType.cs b/NineChronicles.Headless/GraphTypes/States/Models/Garage/FungibleItemGarageType.cs new file mode 100644 index 000000000..b7c8258c2 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/States/Models/Garage/FungibleItemGarageType.cs @@ -0,0 +1,36 @@ +using GraphQL.Types; +using Libplanet; +using Libplanet.Explorer.GraphTypes; +using Nekoyume.Model.Garages; +using NineChronicles.Headless.GraphTypes.States.Models.Item; + +namespace NineChronicles.Headless.GraphTypes.States.Models.Garage; + +public class FungibleItemGarageType : ObjectGraphType +{ + public FungibleItemGarageType() + { + Field(name: "item", resolve: context => context.Source.Item); + Field(name: "count", resolve: context => context.Source.Count); + } +} + +public class FungibleItemGarageWithAddressType : + ObjectGraphType<(string fungibleItemId, Address addr, FungibleItemGarage? fungibleItemGarage)> +{ + public FungibleItemGarageWithAddressType() + { + Field( + name: "fungibleItemId", + resolve: context => context.Source.fungibleItemId); + Field( + name: "addr", + resolve: context => context.Source.addr); + Field( + name: "item", + resolve: context => context.Source.fungibleItemGarage?.Item); + Field( + name: "count", + resolve: context => context.Source.fungibleItemGarage?.Count); + } +} diff --git a/NineChronicles.Headless/GraphTypes/States/Models/Item/FungibleItemType.cs b/NineChronicles.Headless/GraphTypes/States/Models/Item/FungibleItemType.cs new file mode 100644 index 000000000..58fce6cbb --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/States/Models/Item/FungibleItemType.cs @@ -0,0 +1,14 @@ +using GraphQL.Types; +using Nekoyume.Model.Item; + +namespace NineChronicles.Headless.GraphTypes.States.Models.Item; + +public class FungibleItemType : ItemType +{ + public FungibleItemType() + { + Field>( + "fungibleItemId", + resolve: context => context.Source?.FungibleId.ToString()); + } +} diff --git a/NineChronicles.Headless/GraphTypes/States/Models/Item/ItemType.cs b/NineChronicles.Headless/GraphTypes/States/Models/Item/ItemType.cs new file mode 100644 index 000000000..bbecd84f5 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/States/Models/Item/ItemType.cs @@ -0,0 +1,22 @@ +using GraphQL.Types; +using Nekoyume.Model.Item; +using NineChronicles.Headless.GraphTypes.States.Models.Item.Enum; + +namespace NineChronicles.Headless.GraphTypes.States.Models.Item; + +public class ItemType : ObjectGraphType where T : IItem? +{ + protected ItemType() + { + Field>( + "itemType", + description: "Item category.", + resolve: context => context.Source?.ItemType + ); + Field>( + "itemSubType", + description: "Item sub category.", + resolve: context => context.Source?.ItemSubType + ); + } +} diff --git a/NineChronicles.Headless/GraphTypes/States/StateContext.cs b/NineChronicles.Headless/GraphTypes/States/StateContext.cs index 8e1a55443..3f544c3da 100644 --- a/NineChronicles.Headless/GraphTypes/States/StateContext.cs +++ b/NineChronicles.Headless/GraphTypes/States/StateContext.cs @@ -1,25 +1,36 @@ +#nullable enable + using System.Collections.Generic; using Bencodex.Types; using Libplanet; -using Libplanet.Action; using Libplanet.Assets; using Libplanet.State; +using NineChronicles.Headless.Utils; namespace NineChronicles.Headless.GraphTypes.States { public class StateContext { - public StateContext(AccountStateGetter accountStateGetter, AccountBalanceGetter accountBalanceGetter, long blockIndex) + public StateContext( + AccountStateGetter accountStateGetter, + AccountBalanceGetter accountBalanceGetter, + long blockIndex) { AccountStateGetter = accountStateGetter; AccountBalanceGetter = accountBalanceGetter; BlockIndex = blockIndex; + CurrencyFactory = new CurrencyFactory(accountStateGetter); + FungibleAssetValueFactory = new FungibleAssetValueFactory(CurrencyFactory); } public AccountStateGetter AccountStateGetter { get; } public AccountBalanceGetter AccountBalanceGetter { get; } public long BlockIndex { get; } + public CurrencyFactory CurrencyFactory { get; } + + public FungibleAssetValueFactory FungibleAssetValueFactory { get; } + public IValue? GetState(Address address) => AccountStateGetter(new[] { address })[0]; diff --git a/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs b/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs index c972dce7f..e9fa2742c 100644 --- a/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs +++ b/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using GraphQL; using GraphQL.Types; +using Bencodex.Types; using Lib9c; using Libplanet.Blockchain; using Libplanet.Tx; @@ -168,10 +170,30 @@ public TransactionHeadlessQuery(StandaloneContext standaloneContext) Block txExecutedBlock = blockChain[txExecutedBlockHash]; return execution switch { - TxSuccess txSuccess => new TxResult(TxStatus.SUCCESS, txExecutedBlock.Index, - txExecutedBlock.Hash.ToString(), null, null, txSuccess.UpdatedStates, txSuccess.FungibleAssetsDelta, txSuccess.UpdatedFungibleAssets, txSuccess.ActionsLogsList), - TxFailure txFailure => new TxResult(TxStatus.FAILURE, txExecutedBlock.Index, - txExecutedBlock.Hash.ToString(), txFailure.ExceptionName, txFailure.ExceptionMetadata, null, null, null, null), + TxSuccess txSuccess => new TxResult( + TxStatus.SUCCESS, + txExecutedBlock.Index, + txExecutedBlock.Hash.ToString(), + null, + null, + txSuccess.UpdatedStates + .Select(kv => new KeyValuePair( + kv.Key, + kv.Value)) + .ToImmutableDictionary(), + txSuccess.FungibleAssetsDelta, + txSuccess.UpdatedFungibleAssets, + txSuccess.ActionsLogsList), + TxFailure txFailure => new TxResult( + TxStatus.FAILURE, + txExecutedBlock.Index, + txExecutedBlock.Hash.ToString(), + txFailure.ExceptionName, + txFailure.ExceptionMetadata, + null, + null, + null, + null), _ => throw new NotImplementedException( $"{nameof(execution)} is not expected concrete class.") }; diff --git a/NineChronicles.Headless/NineChroniclesNodeService.cs b/NineChronicles.Headless/NineChroniclesNodeService.cs index 9d809ff49..c799cc62b 100644 --- a/NineChronicles.Headless/NineChroniclesNodeService.cs +++ b/NineChronicles.Headless/NineChroniclesNodeService.cs @@ -26,6 +26,7 @@ using Libplanet; using Libplanet.Action; using Libplanet.Assets; +using NineChronicles.Headless.Utils; using StrictRenderer = Libplanet.Blockchain.Renderers.Debug.ValidatingActionRenderer; namespace NineChronicles.Headless @@ -280,6 +281,10 @@ internal void ConfigureContext(StandaloneContext standaloneContext) standaloneContext.BlockChain = Swarm.BlockChain; standaloneContext.Store = Store; standaloneContext.Swarm = Swarm; + standaloneContext.CurrencyFactory = + new CurrencyFactory(standaloneContext.BlockChain.GetStates); + standaloneContext.FungibleAssetValueFactory = + new FungibleAssetValueFactory(standaloneContext.CurrencyFactory); BootstrapEnded.WaitAsync().ContinueWith((task) => { standaloneContext.BootstrapEnded = true; diff --git a/NineChronicles.Headless/StandaloneContext.cs b/NineChronicles.Headless/StandaloneContext.cs index 06f7f71a1..667b51c93 100644 --- a/NineChronicles.Headless/StandaloneContext.cs +++ b/NineChronicles.Headless/StandaloneContext.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Concurrent; +using System.Numerics; using System.Reactive.Subjects; +using Bencodex.Types; +using Lib9c; using Libplanet; using Libplanet.Assets; using Libplanet.Blockchain; @@ -8,8 +11,10 @@ using Libplanet.Net; using Libplanet.Headless; using Libplanet.Store; +using Nekoyume; using Nekoyume.Model.State; using NineChronicles.Headless.GraphTypes; +using NineChronicles.Headless.Utils; namespace NineChronicles.Headless { @@ -20,21 +25,23 @@ public class StandaloneContext public bool BootstrapEnded { get; set; } public bool PreloadEnded { get; set; } public bool IsMining { get; set; } - public ReplaySubject NodeStatusSubject { get; } = new ReplaySubject(1); - public ReplaySubject PreloadStateSubject { get; } = new ReplaySubject(5); - public Subject DifferentAppProtocolVersionEncounterSubject { get; } - = new Subject(); - public Subject NotificationSubject { get; } = new Subject(); - public Subject NodeExceptionSubject { get; } = new Subject(); + public ReplaySubject NodeStatusSubject { get; } = new(1); + public ReplaySubject PreloadStateSubject { get; } = new(5); + + public Subject DifferentAppProtocolVersionEncounterSubject { get; } = + new(); + + public Subject NotificationSubject { get; } = new(); + public Subject NodeExceptionSubject { get; } = new(); public NineChroniclesNodeService? NineChroniclesNodeService { get; set; } public ConcurrentDictionary statusSubject, ReplaySubject stateSubject, ReplaySubject balanceSubject)> AgentAddresses { get; } = new ConcurrentDictionary, ReplaySubject, ReplaySubject)>(); + (ReplaySubject, ReplaySubject, ReplaySubject)>(); - public NodeStatusType NodeStatus => new NodeStatusType(this) + public NodeStatusType NodeStatus => new(this) { BootstrapEnded = BootstrapEnded, PreloadEnded = PreloadEnded, @@ -45,6 +52,10 @@ public class StandaloneContext public Swarm? Swarm { get; internal set; } + public CurrencyFactory? CurrencyFactory { get; set; } + + public FungibleAssetValueFactory? FungibleAssetValueFactory { get; set; } + internal TimeSpan DifferentAppProtocolVersionEncounterInterval { get; set; } = TimeSpan.FromSeconds(30); internal TimeSpan NotificationInterval { get; set; } = TimeSpan.FromSeconds(30); diff --git a/NineChronicles.Headless/Utils/CurrencyFactory.cs b/NineChronicles.Headless/Utils/CurrencyFactory.cs new file mode 100644 index 000000000..360caab7a --- /dev/null +++ b/NineChronicles.Headless/Utils/CurrencyFactory.cs @@ -0,0 +1,62 @@ +using Bencodex.Types; +using Lib9c; +using Libplanet.Assets; +using Libplanet.State; +using Nekoyume; +using Nekoyume.Model.State; +using NineChronicles.Headless.GraphTypes; + +namespace NineChronicles.Headless.Utils; + +public class CurrencyFactory +{ + private readonly AccountStateGetter _accountStateGetter; + private Currency? _ncg; + + public CurrencyFactory( + AccountStateGetter accountStateGetter, + Currency? ncg = null) + { + _accountStateGetter = accountStateGetter; + _ncg = ncg; + } + + public bool TryGetCurrency(CurrencyEnum currencyEnum, out Currency currency) + { + return TryGetCurrency(currencyEnum.ToString(), out currency); + } + + public bool TryGetCurrency(string ticker, out Currency currency) + { + var result = ticker switch + { + "NCG" => GetNCG(), + _ => Currencies.GetMinterlessCurrency(ticker), + }; + if (result is null) + { + currency = default; + return false; + } + + currency = result.Value; + return true; + } + + private Currency? GetNCG() + { + if (_ncg is not null) + { + return _ncg; + } + + var value = _accountStateGetter(new[] { Addresses.GoldCurrency })[0]; + if (value is Dictionary goldCurrencyDict) + { + var goldCurrency = new GoldCurrencyState(goldCurrencyDict); + _ncg = goldCurrency.Currency; + } + + return _ncg; + } +} diff --git a/NineChronicles.Headless/Utils/FungibleAssetValueFactory.cs b/NineChronicles.Headless/Utils/FungibleAssetValueFactory.cs new file mode 100644 index 000000000..e43f45483 --- /dev/null +++ b/NineChronicles.Headless/Utils/FungibleAssetValueFactory.cs @@ -0,0 +1,70 @@ +using System.Numerics; +using Libplanet.Assets; +using NineChronicles.Headless.GraphTypes; + +namespace NineChronicles.Headless.Utils; + +public class FungibleAssetValueFactory +{ + private readonly CurrencyFactory _currencyFactory; + + public FungibleAssetValueFactory(CurrencyFactory currencyFactory) + { + _currencyFactory = currencyFactory; + } + + public bool TryGetFungibleAssetValue( + CurrencyEnum currencyEnum, + BigInteger majorUnit, + BigInteger minorUnit, + out FungibleAssetValue fungibleAssetValue) + { + return TryGetFungibleAssetValue( + currencyEnum.ToString(), + majorUnit, + minorUnit, + out fungibleAssetValue); + } + + public bool TryGetFungibleAssetValue( + string ticker, + BigInteger majorUnit, + BigInteger minorUnit, + out FungibleAssetValue fungibleAssetValue) + { + if (!_currencyFactory.TryGetCurrency(ticker, out var currency)) + { + fungibleAssetValue = default; + return false; + } + + fungibleAssetValue = new FungibleAssetValue(currency, majorUnit, minorUnit); + return true; + } + + public bool TryGetFungibleAssetValue( + CurrencyEnum currencyEnum, + string value, + out FungibleAssetValue fungibleAssetValue) + { + return TryGetFungibleAssetValue( + currencyEnum.ToString(), + value, + out fungibleAssetValue); + } + + public bool TryGetFungibleAssetValue( + string ticker, + string value, + out FungibleAssetValue fungibleAssetValue) + { + if (!_currencyFactory.TryGetCurrency(ticker, out var currency)) + { + fungibleAssetValue = default; + return false; + } + + fungibleAssetValue = FungibleAssetValue.Parse(currency, value); + return true; + } +}