diff --git a/.Lib9c.Tests/Action/TransferAsset2Test.cs b/.Lib9c.Tests/Action/TransferAsset2Test.cs new file mode 100644 index 0000000000..8bf8a7c724 --- /dev/null +++ b/.Lib9c.Tests/Action/TransferAsset2Test.cs @@ -0,0 +1,348 @@ +namespace Lib9c.Tests.Action +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.IO; + using System.Linq; + using System.Runtime.Serialization.Formatters.Binary; + using Bencodex.Types; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Model; + using Nekoyume.Model.State; + using Xunit; + + public class TransferAsset2Test + { + private static readonly Address _sender = new Address( + new byte[] + { + 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + + private static readonly Address _recipient = new Address(new byte[] + { + 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + private static readonly Currency _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + + [Fact] + public void Constructor_ThrowsMemoLengthOverflowException() + { + Assert.Throws(() => + new TransferAsset2(_sender, _recipient, _currency * 100, new string(' ', 100))); + } + + [Fact] + public void Execute() + { + var prevState = new Account( + MockState.Empty + .SetState(_recipient.Derive(ActivationKey.DeriveKey), true.Serialize()) + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset2( + sender: _sender, + recipient: _recipient, + amount: _currency * 100 + ); + IAccount nextState = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + + Assert.Equal(_currency * 900, nextState.GetBalance(_sender, _currency)); + Assert.Equal(_currency * 110, nextState.GetBalance(_recipient, _currency)); + } + + [Fact] + public void ExecuteWithInvalidSigner() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset2( + sender: _sender, + recipient: _recipient, + amount: _currency * 100 + ); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + // 송금자가 직접 사인하지 않으면 실패해야 합니다. + Signer = _recipient, + BlockIndex = 1, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _recipient); + Assert.Equal(exc.TxSigner, _recipient); + } + + [Fact] + public void ExecuteWithInvalidRecipient() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000)); + // Should not allow TransferAsset2 with same sender and recipient. + var action = new TransferAsset2( + sender: _sender, + recipient: _sender, + amount: _currency * 100 + ); + + // No exception should be thrown when its index is less then 380000. + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 380001, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _sender); + } + + [Fact] + public void ExecuteWithInsufficientBalance() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset2( + sender: _sender, + recipient: _recipient, + amount: _currency * 100000 + ); + + Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + } + + [Fact] + public void ExecuteWithMinterAsSender() + { +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currencyBySender = Currency.Legacy("NCG", 2, _sender); +#pragma warning restore CS0618 + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, currencyBySender * 1000) + .SetBalance(_recipient, currencyBySender * 10)); + var action = new TransferAsset2( + sender: _sender, + recipient: _recipient, + amount: currencyBySender * 100 + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(new[] { _sender }, ex.Minters); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Fact] + public void ExecuteWithMinterAsRecipient() + { +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currencyByRecipient = Currency.Legacy("NCG", 2, _sender); +#pragma warning restore CS0618 + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, currencyByRecipient * 1000) + .SetBalance(_sender, currencyByRecipient * 10)); + var action = new TransferAsset2( + sender: _sender, + recipient: _recipient, + amount: currencyByRecipient * 100 + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(new[] { _sender }, ex.Minters); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Fact] + public void ExecuteWithUnactivatedRecipient() + { + var activatedAddress = new ActivatedAccountsState().AddAccount(new PrivateKey().Address); + var prevState = new Account( + MockState.Empty + .SetState(_sender.Derive(ActivationKey.DeriveKey), true.Serialize()) + .SetState(Addresses.ActivatedAccount, activatedAddress.Serialize()) + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset2( + sender: _sender, + recipient: _recipient, + amount: _currency * 100 + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void PlainValue(string memo) + { + var action = new TransferAsset2(_sender, _recipient, _currency * 100, memo); + Dictionary plainValue = (Dictionary)action.PlainValue; + Dictionary values = (Dictionary)plainValue["values"]; + + Assert.Equal((Text)"transfer_asset2", plainValue["type_id"]); + Assert.Equal(_sender, values["sender"].ToAddress()); + Assert.Equal(_recipient, values["recipient"].ToAddress()); + Assert.Equal(_currency * 100, values["amount"].ToFungibleAssetValue()); + if (!(memo is null)) + { + Assert.Equal(memo, values["memo"].ToDotnetString()); + } + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void LoadPlainValue(string memo) + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipient", _recipient.Serialize()), + new KeyValuePair((Text)"amount", (_currency * 100).Serialize()), + }; + if (!(memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text)"memo", memo.Serialize())); + } + + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_asset2") + .Add("values", new Dictionary(pairs)); + var action = new TransferAsset2(); + action.LoadPlainValue(plainValue); + + Assert.Equal(_sender, action.Sender); + Assert.Equal(_recipient, action.Recipient); + Assert.Equal(_currency * 100, action.Amount); + Assert.Equal(memo, action.Memo); + } + + [Fact] + public void LoadPlainValue_ThrowsMemoLengthOverflowException() + { + var action = new TransferAsset2(); + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_asset2") + .Add("values", new Dictionary(new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipient", _recipient.Serialize()), + new KeyValuePair((Text)"amount", (_currency * 100).Serialize()), + new KeyValuePair((Text)"memo", new string(' ', 81).Serialize()), + })); + + Assert.Throws(() => action.LoadPlainValue(plainValue)); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void SerializeWithDotnetAPI(string memo) + { + var formatter = new BinaryFormatter(); + var action = new TransferAsset2(_sender, _recipient, _currency * 100, memo); + + using var ms = new MemoryStream(); + formatter.Serialize(ms, action); + + ms.Seek(0, SeekOrigin.Begin); + var deserialized = (TransferAsset2)formatter.Deserialize(ms); + + Assert.Equal(_sender, deserialized.Sender); + Assert.Equal(_recipient, deserialized.Recipient); + Assert.Equal(_currency * 100, deserialized.Amount); + Assert.Equal(memo, deserialized.Memo); + } + + [Fact] + public void CheckObsolete() + { + var action = new TransferAsset2(_sender, _recipient, _currency * 1); + Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = new Account(MockState.Empty), + Signer = _sender, + BlockIndex = TransferAsset3.CrystalTransferringRestrictionStartIndex, + }); + }); + } + } +} diff --git a/.Lib9c.Tests/Action/TransferAsset3Test.cs b/.Lib9c.Tests/Action/TransferAsset3Test.cs new file mode 100644 index 0000000000..cb74a2f454 --- /dev/null +++ b/.Lib9c.Tests/Action/TransferAsset3Test.cs @@ -0,0 +1,379 @@ +namespace Lib9c.Tests.Action +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.IO; + using System.Linq; + using System.Numerics; + using System.Runtime.Serialization.Formatters.Binary; + using Bencodex.Types; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model; + using Nekoyume.Model.State; + using Xunit; + + public class TransferAsset3Test + { + private static readonly Address _sender = new Address( + new byte[] + { + 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + + private static readonly Address _recipient = new Address(new byte[] + { + 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + private static readonly Currency _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + + [Fact] + public void Constructor_ThrowsMemoLengthOverflowException() + { + Assert.Throws(() => + new TransferAsset3(_sender, _recipient, _currency * 100, new string(' ', 100))); + } + + [Theory] + // activation by derive address. + [InlineData(true, false, false)] + // activation by ActivatedAccountsState. + [InlineData(false, true, false)] + // state exist. + [InlineData(false, false, true)] + public void Execute(bool activate, bool legacyActivate, bool stateExist) + { + var mockState = MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10); + + if (activate) + { + mockState = mockState.SetState(_recipient.Derive(ActivationKey.DeriveKey), true.Serialize()); + } + + if (legacyActivate) + { + var activatedAccountState = new ActivatedAccountsState(); + activatedAccountState = activatedAccountState.AddAccount(_recipient); + mockState = mockState.SetState(activatedAccountState.address, activatedAccountState.Serialize()); + } + + if (stateExist) + { + mockState = mockState.SetState(_recipient, new AgentState(_recipient).Serialize()); + } + + var prevState = new Account(mockState); + var action = new TransferAsset3( + sender: _sender, + recipient: _recipient, + amount: _currency * 100 + ); + IAccount nextState = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + + Assert.Equal(_currency * 900, nextState.GetBalance(_sender, _currency)); + Assert.Equal(_currency * 110, nextState.GetBalance(_recipient, _currency)); + } + + [Fact] + public void ExecuteWithInvalidSigner() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset3( + sender: _sender, + recipient: _recipient, + amount: _currency * 100 + ); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + // 송금자가 직접 사인하지 않으면 실패해야 합니다. + Signer = _recipient, + BlockIndex = 1, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _recipient); + Assert.Equal(exc.TxSigner, _recipient); + } + + [Fact] + public void ExecuteWithInvalidRecipient() + { + var balance = ImmutableDictionary<(Address, Currency), FungibleAssetValue>.Empty + .Add((_sender, _currency), _currency * 1000); + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000)); + + // Should not allow TransferAsset with same sender and recipient. + var action = new TransferAsset3( + sender: _sender, + recipient: _sender, + amount: _currency * 100 + ); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _sender); + } + + [Fact] + public void ExecuteWithInsufficientBalance() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)) + .SetState(_recipient, new AgentState(_recipient).Serialize()); + var action = new TransferAsset3( + sender: _sender, + recipient: _recipient, + amount: _currency * 100000 + ); + + Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + } + + [Fact] + public void ExecuteWithMinterAsSender() + { +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currencyBySender = Currency.Legacy("NCG", 2, _sender); +#pragma warning restore CS0618 + var prevState = new Account( + MockState.Empty + .SetState(_recipient, new AgentState(_recipient).Serialize()) + .SetBalance(_sender, currencyBySender * 1000) + .SetBalance(_recipient, currencyBySender * 10)); + var action = new TransferAsset3( + sender: _sender, + recipient: _recipient, + amount: currencyBySender * 100 + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(new[] { _sender }, ex.Minters); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Fact] + public void ExecuteWithMinterAsRecipient() + { +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currencyByRecipient = Currency.Legacy("NCG", 2, _sender); +#pragma warning restore CS0618 + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, currencyByRecipient * 1000) + .SetBalance(_recipient, currencyByRecipient * 10) + .SetState(_recipient, new AgentState(_recipient).Serialize())); + var action = new TransferAsset3( + sender: _sender, + recipient: _recipient, + amount: currencyByRecipient * 100 + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(new[] { _sender }, ex.Minters); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Fact] + public void ExecuteWithUnactivatedRecipient() + { + var activatedAddress = new ActivatedAccountsState().AddAccount(new PrivateKey().Address); + var prevState = new Account( + MockState.Empty + .SetState(_sender.Derive(ActivationKey.DeriveKey), true.Serialize()) + .SetState(Addresses.ActivatedAccount, activatedAddress.Serialize()) + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset3( + sender: _sender, + recipient: _recipient, + amount: _currency * 100 + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void PlainValue(string memo) + { + var action = new TransferAsset3(_sender, _recipient, _currency * 100, memo); + Dictionary plainValue = (Dictionary)action.PlainValue; + Dictionary values = (Dictionary)plainValue["values"]; + + Assert.Equal("transfer_asset3", (Text)plainValue["type_id"]); + Assert.Equal(_sender, values["sender"].ToAddress()); + Assert.Equal(_recipient, values["recipient"].ToAddress()); + Assert.Equal(_currency * 100, values["amount"].ToFungibleAssetValue()); + if (!(memo is null)) + { + Assert.Equal(memo, values["memo"].ToDotnetString()); + } + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void LoadPlainValue(string memo) + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipient", _recipient.Serialize()), + new KeyValuePair((Text)"amount", (_currency * 100).Serialize()), + }; + if (!(memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text)"memo", memo.Serialize())); + } + + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_asset3") + .Add("values", new Dictionary(pairs)); + var action = new TransferAsset3(); + action.LoadPlainValue(plainValue); + + Assert.Equal(_sender, action.Sender); + Assert.Equal(_recipient, action.Recipient); + Assert.Equal(_currency * 100, action.Amount); + Assert.Equal(memo, action.Memo); + } + + [Fact] + public void Execute_Throw_InvalidTransferCurrencyException() + { + var crystal = CrystalCalculator.CRYSTAL; + var prevState = new Account( + MockState.Empty + .SetState(_recipient.Derive(ActivationKey.DeriveKey), true.Serialize()) + .SetBalance(_sender, crystal * 1000)); + var action = new TransferAsset3( + sender: _sender, + recipient: _recipient, + amount: 1000 * crystal + ); + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = TransferAsset3.CrystalTransferringRestrictionStartIndex, + })); + } + + [Fact] + public void LoadPlainValue_ThrowsMemoLengthOverflowException() + { + var action = new TransferAsset3(); + var values = new Dictionary(new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipient", _recipient.Serialize()), + new KeyValuePair((Text)"amount", (_currency * 100).Serialize()), + new KeyValuePair((Text)"memo", new string(' ', 81).Serialize()), + }); + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_asset3") + .Add("values", values); + + Assert.Throws(() => action.LoadPlainValue(plainValue)); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void SerializeWithDotnetAPI(string memo) + { + var formatter = new BinaryFormatter(); + var action = new TransferAsset3(_sender, _recipient, _currency * 100, memo); + + using var ms = new MemoryStream(); + formatter.Serialize(ms, action); + + ms.Seek(0, SeekOrigin.Begin); + var deserialized = (TransferAsset3)formatter.Deserialize(ms); + + Assert.Equal(_sender, deserialized.Sender); + Assert.Equal(_recipient, deserialized.Recipient); + Assert.Equal(_currency * 100, deserialized.Amount); + Assert.Equal(memo, deserialized.Memo); + } + } +} diff --git a/.Lib9c.Tests/Action/TransferAsset4Test.cs b/.Lib9c.Tests/Action/TransferAsset4Test.cs new file mode 100644 index 0000000000..1877f65a4d --- /dev/null +++ b/.Lib9c.Tests/Action/TransferAsset4Test.cs @@ -0,0 +1,301 @@ +namespace Lib9c.Tests.Action +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.IO; + using System.Linq; + using System.Runtime.Serialization.Formatters.Binary; + using Bencodex.Types; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model; + using Nekoyume.Model.State; + using Xunit; + + public class TransferAsset4Test + { + private static readonly Address _sender = new Address( + new byte[] + { + 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + + private static readonly Address _recipient = new Address(new byte[] + { + 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + private static readonly Currency _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + + [Fact] + public void Constructor_ThrowsMemoLengthOverflowException() + { + Assert.Throws(() => + new TransferAsset4(_sender, _recipient, _currency * 100, new string(' ', 100))); + } + + [Fact] + public void Execute() + { + var contractAddress = _sender.Derive(nameof(RequestPledge)); + var patronAddress = new PrivateKey().Address; + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset4( + sender: _sender, + recipient: _recipient, + amount: _currency * 100 + ); + IAccount nextState = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + + Assert.Equal(_currency * 900, nextState.GetBalance(_sender, _currency)); + Assert.Equal(_currency * 110, nextState.GetBalance(_recipient, _currency)); + } + + [Fact] + public void Execute_Throw_InvalidTransferSignerException() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10) + .SetBalance(_sender, Currencies.Mead * 1)); + var action = new TransferAsset4( + sender: _sender, + recipient: _recipient, + amount: _currency * 100 + ); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + // 송금자가 직접 사인하지 않으면 실패해야 합니다. + Signer = _recipient, + BlockIndex = 1, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _recipient); + Assert.Equal(exc.TxSigner, _recipient); + } + + [Fact] + public void Execute_Throw_InvalidTransferRecipientException() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_sender, Currencies.Mead * 1)); + // Should not allow TransferAsset with same sender and recipient. + var action = new TransferAsset4( + sender: _sender, + recipient: _sender, + amount: _currency * 100 + ); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _sender); + } + + [Fact] + public void Execute_Throw_InsufficientBalanceException() + { + var prevState = new Account( + MockState.Empty + .SetState(_recipient, new AgentState(_recipient).Serialize()) + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset4( + sender: _sender, + recipient: _recipient, + amount: _currency * 100000 + ); + + InsufficientBalanceException exc = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(_sender, exc.Address); + Assert.Equal(_currency, exc.Balance.Currency); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Execute_Throw_InvalidTransferMinterException(bool minterAsSender) + { + Address minter = minterAsSender ? _sender : _recipient; +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currencyBySender = Currency.Legacy("NCG", 2, minter); +#pragma warning restore CS0618 + var prevState = new Account( + MockState.Empty + .SetState(_recipient, new AgentState(_recipient).Serialize()) + .SetBalance(_sender, currencyBySender * 1000) + .SetBalance(_recipient, currencyBySender * 10) + .SetBalance(_sender, Currencies.Mead * 1)); + var action = new TransferAsset4( + sender: _sender, + recipient: _recipient, + amount: currencyBySender * 100 + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(new[] { minter }, ex.Minters); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void PlainValue(string memo) + { + var action = new TransferAsset4(_sender, _recipient, _currency * 100, memo); + Dictionary plainValue = (Dictionary)action.PlainValue; + Dictionary values = (Dictionary)plainValue["values"]; + + Assert.Equal((Text)"transfer_asset4", plainValue["type_id"]); + Assert.Equal(_sender, values["sender"].ToAddress()); + Assert.Equal(_recipient, values["recipient"].ToAddress()); + Assert.Equal(_currency * 100, values["amount"].ToFungibleAssetValue()); + if (!(memo is null)) + { + Assert.Equal(memo, values["memo"].ToDotnetString()); + } + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void LoadPlainValue(string memo) + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipient", _recipient.Serialize()), + new KeyValuePair((Text)"amount", (_currency * 100).Serialize()), + }; + if (!(memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text)"memo", memo.Serialize())); + } + + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_asset3") + .Add("values", new Dictionary(pairs)); + var action = new TransferAsset4(); + action.LoadPlainValue(plainValue); + + Assert.Equal(_sender, action.Sender); + Assert.Equal(_recipient, action.Recipient); + Assert.Equal(_currency * 100, action.Amount); + Assert.Equal(memo, action.Memo); + } + + [Fact] + public void Execute_Throw_InvalidTransferCurrencyException() + { + var crystal = CrystalCalculator.CRYSTAL; + var prevState = new Account( + MockState.Empty + .SetState(_recipient.Derive(ActivationKey.DeriveKey), true.Serialize()) + .SetBalance(_sender, crystal * 1000) + .SetBalance(_sender, Currencies.Mead * 1)); + var action = new TransferAsset4( + sender: _sender, + recipient: _recipient, + amount: 1000 * crystal + ); + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = TransferAsset3.CrystalTransferringRestrictionStartIndex, + })); + } + + [Fact] + public void LoadPlainValue_ThrowsMemoLengthOverflowException() + { + var action = new TransferAsset4(); + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_asset3") + .Add("values", new Dictionary(new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipient", _recipient.Serialize()), + new KeyValuePair((Text)"amount", (_currency * 100).Serialize()), + new KeyValuePair((Text)"memo", new string(' ', 81).Serialize()), + })); + + Assert.Throws(() => action.LoadPlainValue(plainValue)); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void SerializeWithDotnetAPI(string memo) + { + var formatter = new BinaryFormatter(); + var action = new TransferAsset4(_sender, _recipient, _currency * 100, memo); + + using var ms = new MemoryStream(); + formatter.Serialize(ms, action); + + ms.Seek(0, SeekOrigin.Begin); + var deserialized = (TransferAsset4)formatter.Deserialize(ms); + + Assert.Equal(_sender, deserialized.Sender); + Assert.Equal(_recipient, deserialized.Recipient); + Assert.Equal(_currency * 100, deserialized.Amount); + Assert.Equal(memo, deserialized.Memo); + } + } +} diff --git a/.Lib9c.Tests/Action/TransferAssetTest0.cs b/.Lib9c.Tests/Action/TransferAssetTest0.cs new file mode 100644 index 0000000000..46cfb33ed6 --- /dev/null +++ b/.Lib9c.Tests/Action/TransferAssetTest0.cs @@ -0,0 +1,302 @@ +namespace Lib9c.Tests.Action +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.IO; + using System.Linq; + using System.Runtime.Serialization.Formatters.Binary; + using Bencodex.Types; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Model.State; + using Xunit; + + public class TransferAssetTest0 + { + private static readonly Address _sender = new Address( + new byte[] + { + 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + + private static readonly Address _recipient = new Address(new byte[] + { + 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + private static readonly Currency _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + + [Fact] + public void Constructor_ThrowsMemoLengthOverflowException() + { + Assert.Throws(() => + new TransferAsset0(_sender, _recipient, _currency * 100, new string(' ', 100))); + } + + [Fact] + public void Execute() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset0( + sender: _sender, + recipient: _recipient, + amount: _currency * 100 + ); + IAccount nextState = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + + Assert.Equal(_currency * 900, nextState.GetBalance(_sender, _currency)); + Assert.Equal(_currency * 110, nextState.GetBalance(_recipient, _currency)); + } + + [Fact] + public void ExecuteWithInvalidSigner() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset0( + sender: _sender, + recipient: _recipient, + amount: _currency * 100 + ); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + // 송금자가 직접 사인하지 않으면 실패해야 합니다. + Signer = _recipient, + BlockIndex = 1, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _recipient); + Assert.Equal(exc.TxSigner, _recipient); + } + + [Fact] + public void ExecuteWithInvalidRecipient() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000)); + // Should not allow TransferAsset with same sender and recipient. + var action = new TransferAsset0( + sender: _sender, + recipient: _sender, + amount: _currency * 100 + ); + + // No exception should be thrown when its index is less then 380000. + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 380001, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _sender); + } + + [Fact] + public void ExecuteWithInsufficientBalance() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAsset0( + sender: _sender, + recipient: _recipient, + amount: _currency * 100000 + ); + + Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + } + + [Fact] + public void ExecuteWithMinterAsSender() + { +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currencyBySender = Currency.Legacy("NCG", 2, _sender); +#pragma warning restore CS0618 + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, currencyBySender * 1000) + .SetBalance(_recipient, currencyBySender * 10)); + var action = new TransferAsset0( + sender: _sender, + recipient: _recipient, + amount: currencyBySender * 100 + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(new[] { _sender }, ex.Minters); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Fact] + public void ExecuteWithMinterAsRecipient() + { +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currencyByRecipient = Currency.Legacy("NCG", 2, _sender); +#pragma warning restore CS0618 + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, currencyByRecipient * 1000) + .SetBalance(_recipient, currencyByRecipient * 10)); + var action = new TransferAsset0( + sender: _sender, + recipient: _recipient, + amount: currencyByRecipient * 100 + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(new[] { _sender }, ex.Minters); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void PlainValue(string memo) + { + var action = new TransferAsset0(_sender, _recipient, _currency * 100, memo); + Dictionary plainValue = (Dictionary)action.PlainValue; + Dictionary values = (Dictionary)plainValue["values"]; + + Assert.Equal(_sender, values["sender"].ToAddress()); + Assert.Equal(_recipient, values["recipient"].ToAddress()); + Assert.Equal(_currency * 100, values["amount"].ToFungibleAssetValue()); + if (!(memo is null)) + { + Assert.Equal(memo, values["memo"].ToDotnetString()); + } + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void LoadPlainValue(string memo) + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipient", _recipient.Serialize()), + new KeyValuePair((Text)"amount", (_currency * 100).Serialize()), + }; + if (!(memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text)"memo", memo.Serialize())); + } + + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_asset") + .Add("values", new Dictionary(pairs)); + var action = new TransferAsset0(); + action.LoadPlainValue(plainValue); + + Assert.Equal(_sender, action.Sender); + Assert.Equal(_recipient, action.Recipient); + Assert.Equal(_currency * 100, action.Amount); + Assert.Equal(memo, action.Memo); + } + + [Fact] + public void LoadPlainValue_ThrowsMemoLengthOverflowException() + { + var action = new TransferAsset0(); + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_asset") + .Add("values", new Dictionary(new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipient", _recipient.Serialize()), + new KeyValuePair((Text)"amount", (_currency * 100).Serialize()), + new KeyValuePair((Text)"memo", new string(' ', 81).Serialize()), + })); + + Assert.Throws(() => action.LoadPlainValue(plainValue)); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void SerializeWithDotnetAPI(string memo) + { + var formatter = new BinaryFormatter(); + var action = new TransferAsset0(_sender, _recipient, _currency * 100, memo); + + using var ms = new MemoryStream(); + formatter.Serialize(ms, action); + + ms.Seek(0, SeekOrigin.Begin); + var deserialized = (TransferAsset0)formatter.Deserialize(ms); + + Assert.Equal(_sender, deserialized.Sender); + Assert.Equal(_recipient, deserialized.Recipient); + Assert.Equal(_currency * 100, deserialized.Amount); + Assert.Equal(memo, deserialized.Memo); + } + } +} diff --git a/.Lib9c.Tests/Action/TransferAssets0Test.cs b/.Lib9c.Tests/Action/TransferAssets0Test.cs new file mode 100644 index 0000000000..c792511094 --- /dev/null +++ b/.Lib9c.Tests/Action/TransferAssets0Test.cs @@ -0,0 +1,452 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.IO; + using System.Linq; + using System.Numerics; + using System.Runtime.Serialization.Formatters.Binary; + using Bencodex.Types; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model; + using Nekoyume.Model.State; + using Xunit; + + public class TransferAssets0Test + { + private static readonly Address _sender = new Address( + new byte[] + { + 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + + private static readonly Address _recipient = new Address(new byte[] + { + 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + + private static readonly Address _recipient2 = new Address(new byte[] + { + 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + private static readonly Currency _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + + [Fact] + public void Constructor_ThrowsMemoLengthOverflowException() + { + Assert.Throws(() => + new TransferAssets0( + _sender, + new List<(Address, FungibleAssetValue)>() + { + (_recipient, _currency * 100), + }, + new string(' ', 100) + ) + ); + } + + [Theory] + // activation by derive address. + [InlineData(true, false, false)] + // activation by ActivatedAccountsState. + [InlineData(false, true, false)] + // state exist. + [InlineData(false, false, true)] + public void Execute(bool activate, bool legacyActivate, bool stateExist) + { + var mockState = MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10); + if (activate) + { + mockState = mockState + .SetState(_recipient.Derive(ActivationKey.DeriveKey), true.Serialize()) + .SetState(_recipient2.Derive(ActivationKey.DeriveKey), true.Serialize()); + } + + if (legacyActivate) + { + var activatedAccountState = new ActivatedAccountsState(); + activatedAccountState = activatedAccountState + .AddAccount(_recipient) + .AddAccount(_recipient2); + mockState = mockState.SetState(activatedAccountState.address, activatedAccountState.Serialize()); + } + + if (stateExist) + { + mockState = mockState + .SetState(_recipient, new AgentState(_recipient).Serialize()) + .SetState(_recipient2, new AgentState(_recipient2).Serialize()); + } + + var prevState = new Account(mockState); + var action = new TransferAssets0( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100), + (_recipient2, _currency * 100), + } + ); + IAccount nextState = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + + Assert.Equal(_currency * 800, nextState.GetBalance(_sender, _currency)); + Assert.Equal(_currency * 110, nextState.GetBalance(_recipient, _currency)); + Assert.Equal(_currency * 100, nextState.GetBalance(_recipient2, _currency)); + } + + [Fact] + public void ExecuteWithInvalidSigner() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAssets0( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100), + } + ); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + // 송금자가 직접 사인하지 않으면 실패해야 합니다. + Signer = _recipient, + BlockIndex = 1, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _recipient); + Assert.Equal(exc.TxSigner, _recipient); + } + + [Fact] + public void ExecuteWithInvalidRecipient() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000)); + // Should not allow TransferAsset with same sender and recipient. + var action = new TransferAssets0( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_sender, _currency * 100), + } + ); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _sender); + } + + [Fact] + public void ExecuteWithInsufficientBalance() + { + var prevState = new Account( + MockState.Empty + .SetState(_recipient, new AgentState(_recipient).Serialize()) + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAssets0( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100000), + } + ); + + Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + } + + [Fact] + public void ExecuteWithMinterAsSender() + { +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currencyBySender = Currency.Legacy("NCG", 2, _sender); +#pragma warning restore CS0618 + var prevState = new Account( + MockState.Empty + .SetState(_recipient, new AgentState(_recipient).Serialize()) + .SetBalance(_sender, currencyBySender * 1000) + .SetBalance(_recipient, currencyBySender * 10)); + var action = new TransferAssets0( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, currencyBySender * 100), + } + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(new[] { _sender }, ex.Minters); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Fact] + public void ExecuteWithMinterAsRecipient() + { +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currencyByRecipient = Currency.Legacy("NCG", 2, _sender); +#pragma warning restore CS0618 + var prevState = new Account( + MockState.Empty + .SetState(_recipient, new AgentState(_recipient).Serialize()) + .SetBalance(_sender, currencyByRecipient * 1000) + .SetBalance(_recipient, currencyByRecipient * 10)); + var action = new TransferAssets0( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, currencyByRecipient * 100), + } + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(new[] { _sender }, ex.Minters); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Fact] + public void ExecuteWithUnactivatedRecipient() + { + var activatedAddress = new ActivatedAccountsState().AddAccount(new PrivateKey().Address); + var prevState = new Account( + MockState.Empty + .SetState(_sender.Derive(ActivationKey.DeriveKey), true.Serialize()) + .SetState(Addresses.ActivatedAccount, activatedAddress.Serialize()) + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAssets0( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100), + } + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void PlainValue(string memo) + { + var action = new TransferAssets0( + _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100), + }, + memo + ); + + Dictionary plainValue = (Dictionary)action.PlainValue; + var values = (Dictionary)plainValue["values"]; + var recipients = (List)values["recipients"]; + var info = (List)recipients[0]; + Assert.Equal(_sender, values["sender"].ToAddress()); + Assert.Equal(_recipient, info[0].ToAddress()); + Assert.Equal(_currency * 100, info[1].ToFungibleAssetValue()); + if (!(memo is null)) + { + Assert.Equal(memo, values["memo"].ToDotnetString()); + } + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void LoadPlainValue(string memo) + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipients", List.Empty.Add(List.Empty.Add(_recipient.Serialize()).Add((_currency * 100).Serialize()))), + }; + if (!(memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text)"memo", memo.Serialize())); + } + + var values = new Dictionary(pairs); + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_assets") + .Add("values", values); + var action = new TransferAssets0(); + action.LoadPlainValue(plainValue); + + Assert.Equal(_sender, action.Sender); + Assert.Equal(_recipient, action.Recipients.Single().recipient); + Assert.Equal(_currency * 100, action.Recipients.Single().amount); + Assert.Equal(memo, action.Memo); + } + + [Fact] + public void LoadPlainValue_ThrowsMemoLengthOverflowException() + { + var action = new TransferAssets0(); + var values = new Dictionary(new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipients", List.Empty.Add(List.Empty.Add(_recipient.Serialize()).Add((_currency * 100).Serialize()))), + new KeyValuePair((Text)"memo", new string(' ', 81).Serialize()), + }); + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_assets") + .Add("values", values); + + Assert.Throws(() => action.LoadPlainValue(plainValue)); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void SerializeWithDotnetAPI(string memo) + { + var formatter = new BinaryFormatter(); + var action = new TransferAssets0( + _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100), + }, + memo + ); + + using var ms = new MemoryStream(); + formatter.Serialize(ms, action); + + ms.Seek(0, SeekOrigin.Begin); + var deserialized = (TransferAssets0)formatter.Deserialize(ms); + + Assert.Equal(_sender, deserialized.Sender); + Assert.Equal(_recipient, deserialized.Recipients.Single().recipient); + Assert.Equal(_currency * 100, deserialized.Recipients.Single().amount); + Assert.Equal(memo, deserialized.Memo); + } + + [Fact] + public void Execute_Throw_ArgumentOutOfRangeException() + { + var recipients = new List<(Address, FungibleAssetValue)>(); + + for (int i = 0; i < TransferAssets0.RecipientsCapacity + 1; i++) + { + recipients.Add((_recipient, _currency * 100)); + } + + var action = new TransferAssets0(_sender, recipients); + Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = new Account(MockState.Empty), + Signer = _sender, + BlockIndex = 1, + }); + }); + } + + [Fact] + public void Execute_Throw_InvalidTransferCurrencyException() + { + var crystal = CrystalCalculator.CRYSTAL; + var prevState = new Account( + MockState.Empty + .SetState(_recipient.Derive(ActivationKey.DeriveKey), true.Serialize()) + .SetBalance(_sender, crystal * 1000)); + var action = new TransferAssets0( + sender: _sender, + recipients: new List<(Address, FungibleAssetValue)> + { + (_recipient, 1000 * crystal), + (_recipient, 100 * _currency), + } + ); + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = TransferAsset3.CrystalTransferringRestrictionStartIndex, + })); + } + } +} diff --git a/.Lib9c.Tests/Action/TransferAssets2Test.cs b/.Lib9c.Tests/Action/TransferAssets2Test.cs new file mode 100644 index 0000000000..1bd683d9de --- /dev/null +++ b/.Lib9c.Tests/Action/TransferAssets2Test.cs @@ -0,0 +1,364 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.IO; + using System.Linq; + using System.Runtime.Serialization.Formatters.Binary; + using Bencodex.Types; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model; + using Nekoyume.Model.State; + using Xunit; + + public class TransferAssets2Test + { + private static readonly Address _sender = new Address( + new byte[] + { + 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + + private static readonly Address _recipient = new Address(new byte[] + { + 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + + private static readonly Address _recipient2 = new Address(new byte[] + { + 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + } + ); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + private static readonly Currency _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + + [Fact] + public void Constructor_ThrowsMemoLengthOverflowException() + { + Assert.Throws(() => + new TransferAssets2( + _sender, + new List<(Address, FungibleAssetValue)>() + { + (_recipient, _currency * 100), + }, + new string(' ', 100) + ) + ); + } + + [Fact] + public void Execute() + { + var contractAddress = _sender.Derive(nameof(RequestPledge)); + var patronAddress = new PrivateKey().Address; + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAssets2( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100), + (_recipient2, _currency * 100), + } + ); + IAccount nextState = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + + Assert.Equal(_currency * 800, nextState.GetBalance(_sender, _currency)); + Assert.Equal(_currency * 110, nextState.GetBalance(_recipient, _currency)); + Assert.Equal(_currency * 100, nextState.GetBalance(_recipient2, _currency)); + Assert.Equal(Currencies.Mead * 0, nextState.GetBalance(_sender, Currencies.Mead)); + Assert.Equal(Currencies.Mead * 0, nextState.GetBalance(patronAddress, Currencies.Mead)); + } + + [Fact] + public void Execute_Throw_InvalidTransferSignerException() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAssets2( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100), + } + ); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + // 송금자가 직접 사인하지 않으면 실패해야 합니다. + Signer = _recipient, + BlockIndex = 1, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _recipient); + Assert.Equal(exc.TxSigner, _recipient); + } + + [Fact] + public void Execute_Throw_InvalidTransferRecipientException() + { + var prevState = new Account( + MockState.Empty + .SetBalance(_sender, _currency * 1000)); + // Should not allow TransferAsset with same sender and recipient. + var action = new TransferAssets2( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_sender, _currency * 100), + } + ); + + var exc = Assert.Throws(() => + { + _ = action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(exc.Sender, _sender); + Assert.Equal(exc.Recipient, _sender); + } + + [Fact] + public void Execute_Throw_InsufficientBalanceException() + { + var prevState = new Account( + MockState.Empty + .SetState(_recipient, new AgentState(_recipient).Serialize()) + .SetBalance(_sender, _currency * 1000) + .SetBalance(_recipient, _currency * 10)); + var action = new TransferAssets2( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100000), + } + ); + + InsufficientBalanceException exc = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(_sender, exc.Address); + Assert.Equal(_currency, exc.Balance.Currency); + } + + [Fact] + public void Execute_Throw_InvalidTransferMinterException() + { +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currencyBySender = Currency.Legacy("NCG", 2, _sender); +#pragma warning restore CS0618 + var prevState = new Account( + MockState.Empty + .SetState(_recipient, new AgentState(_recipient).Serialize()) + .SetBalance(_sender, currencyBySender * 1000) + .SetBalance(_recipient, currencyBySender * 10)); + var action = new TransferAssets2( + sender: _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, currencyBySender * 100), + } + ); + var ex = Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = 1, + }); + }); + + Assert.Equal(new[] { _sender }, ex.Minters); + Assert.Equal(_sender, ex.Sender); + Assert.Equal(_recipient, ex.Recipient); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void PlainValue(string memo) + { + var action = new TransferAssets2( + _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100), + }, + memo + ); + + Dictionary plainValue = (Dictionary)action.PlainValue; + Dictionary values = (Dictionary)plainValue["values"]; + + var recipients = (List)values["recipients"]; + var info = (List)recipients[0]; + Assert.Equal((Text)"transfer_assets2", plainValue["type_id"]); + Assert.Equal(_sender, values["sender"].ToAddress()); + Assert.Equal(_recipient, info[0].ToAddress()); + Assert.Equal(_currency * 100, info[1].ToFungibleAssetValue()); + if (!(memo is null)) + { + Assert.Equal(memo, values["memo"].ToDotnetString()); + } + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void LoadPlainValue(string memo) + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipients", List.Empty.Add(List.Empty.Add(_recipient.Serialize()).Add((_currency * 100).Serialize()))), + }; + if (!(memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text)"memo", memo.Serialize())); + } + + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_assets") + .Add("values", new Dictionary(pairs)); + var action = new TransferAssets2(); + action.LoadPlainValue(plainValue); + + Assert.Equal(_sender, action.Sender); + Assert.Equal(_recipient, action.Recipients.Single().recipient); + Assert.Equal(_currency * 100, action.Recipients.Single().amount); + Assert.Equal(memo, action.Memo); + } + + [Fact] + public void LoadPlainValue_ThrowsMemoLengthOverflowException() + { + var action = new TransferAssets2(); + var plainValue = Dictionary.Empty + .Add("type_id", "transfer_assets") + .Add("values", new Dictionary(new[] + { + new KeyValuePair((Text)"sender", _sender.Serialize()), + new KeyValuePair((Text)"recipients", List.Empty.Add(List.Empty.Add(_recipient.Serialize()).Add((_currency * 100).Serialize()))), + new KeyValuePair((Text)"memo", new string(' ', 81).Serialize()), + })); + + Assert.Throws(() => action.LoadPlainValue(plainValue)); + } + + [Theory] + [InlineData(null)] + [InlineData("Nine Chronicles")] + public void SerializeWithDotnetAPI(string memo) + { + var formatter = new BinaryFormatter(); + var action = new TransferAssets2( + _sender, + new List<(Address, FungibleAssetValue)> + { + (_recipient, _currency * 100), + }, + memo + ); + + using var ms = new MemoryStream(); + formatter.Serialize(ms, action); + + ms.Seek(0, SeekOrigin.Begin); + var deserialized = (TransferAssets2)formatter.Deserialize(ms); + + Assert.Equal(_sender, deserialized.Sender); + Assert.Equal(_recipient, deserialized.Recipients.Single().recipient); + Assert.Equal(_currency * 100, deserialized.Recipients.Single().amount); + Assert.Equal(memo, deserialized.Memo); + } + + [Fact] + public void Execute_Throw_ArgumentOutOfRangeException() + { + var recipients = new List<(Address, FungibleAssetValue)>(); + + for (int i = 0; i < TransferAssets2.RecipientsCapacity + 1; i++) + { + recipients.Add((_recipient, _currency * 100)); + } + + var action = new TransferAssets2(_sender, recipients); + Assert.Throws(() => + { + action.Execute(new ActionContext() + { + PreviousState = new Account(MockState.Empty), + Signer = _sender, + BlockIndex = 1, + }); + }); + } + + [Fact] + public void Execute_Throw_InvalidTransferCurrencyException() + { + var crystal = CrystalCalculator.CRYSTAL; + var prevState = new Account( + MockState.Empty + .SetState(_recipient.Derive(ActivationKey.DeriveKey), true.Serialize()) + .SetBalance(_sender, crystal * 1000)); + var action = new TransferAssets2( + sender: _sender, + recipients: new List<(Address, FungibleAssetValue)> + { + (_recipient, 1000 * crystal), + (_recipient, 100 * _currency), + } + ); + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = prevState, + Signer = _sender, + BlockIndex = TransferAsset3.CrystalTransferringRestrictionStartIndex, + })); + } + } +} diff --git a/.Lib9c.Tests/Action/UpdateSell0Test.cs b/.Lib9c.Tests/Action/UpdateSell0Test.cs new file mode 100644 index 0000000000..3819505da5 --- /dev/null +++ b/.Lib9c.Tests/Action/UpdateSell0Test.cs @@ -0,0 +1,337 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using Bencodex.Types; + using Lib9c.Model.Order; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.State; + using Serilog; + using Xunit; + using Xunit.Abstractions; + using static Lib9c.SerializeKeys; + + public class UpdateSell0Test + { + private const long ProductPrice = 100; + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly Currency _currency; + private readonly AvatarState _avatarState; + private readonly TableSheets _tableSheets; + private readonly GoldCurrencyState _goldCurrencyState; + private IAccount _initialState; + + public UpdateSell0Test(ITestOutputHelper outputHelper) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(outputHelper) + .CreateLogger(); + + _initialState = new Account(MockState.Empty); + var sheets = TableSheetsImporter.ImportSheets(); + foreach (var (key, value) in sheets) + { + _initialState = _initialState + .SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _tableSheets = new TableSheets(sheets); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + _goldCurrencyState = new GoldCurrencyState(_currency); + + var shopState = new ShopState(); + + _agentAddress = new PrivateKey().Address; + var agentState = new AgentState(_agentAddress); + _avatarAddress = new PrivateKey().Address; + var rankingMapAddress = new PrivateKey().Address; + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + rankingMapAddress) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.ActionsInShop), + }; + agentState.avatarAddresses[0] = _avatarAddress; + + _initialState = _initialState + .SetState(GoldCurrencyState.Address, _goldCurrencyState.Serialize()) + .SetState(Addresses.Shop, shopState.Serialize()) + .SetState(_agentAddress, agentState.Serialize()) + .SetState(_avatarAddress, _avatarState.Serialize()); + } + + [Theory] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", 1, 1, 1, true, true)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", 1, 1, 1, true, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 1, 1, 1, true, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 1, 2, true, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 2, 3, true, true)] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", 1, 1, 1, false, true)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", 1, 1, 1, false, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 1, 1, 1, false, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 1, 2, false, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 2, 3, false, true)] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", 1, 1, 1, true, false)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", 1, 1, 1, true, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 1, 1, 1, true, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 1, 2, true, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 2, 3, true, false)] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", 1, 1, 1, false, false)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", 1, 1, 1, false, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 1, 1, 1, false, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 1, 2, false, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 2, 3, false, false)] + public void Execute( + ItemType itemType, + string guid, + int itemCount, + int inventoryCount, + int expectedCount, + bool fromPreviousAction, + bool legacy + ) + { + var avatarState = _initialState.GetAvatarState(_avatarAddress); + ITradableItem tradableItem; + var itemId = new Guid(guid); + var orderId = Guid.NewGuid(); + var updateSellOrderId = Guid.NewGuid(); + ItemSubType itemSubType; + const long requiredBlockIndex = Order.ExpirationInterval; + switch (itemType) + { + case ItemType.Equipment: + { + var itemUsable = ItemFactory.CreateItemUsable( + _tableSheets.EquipmentItemSheet.First, + itemId, + requiredBlockIndex); + tradableItem = (ITradableItem)itemUsable; + itemSubType = itemUsable.ItemSubType; + break; + } + + case ItemType.Costume: + { + var costume = ItemFactory.CreateCostume(_tableSheets.CostumeItemSheet.First, itemId); + costume.Update(requiredBlockIndex); + tradableItem = costume; + itemSubType = costume.ItemSubType; + break; + } + + default: + { + var material = ItemFactory.CreateTradableMaterial( + _tableSheets.MaterialItemSheet.OrderedList.First(r => r.ItemSubType == ItemSubType.Hourglass)); + itemSubType = material.ItemSubType; + material.RequiredBlockIndex = requiredBlockIndex; + tradableItem = material; + break; + } + } + + var shardedShopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, orderId); + var shopState = new ShardedShopStateV2(shardedShopAddress); + var order = OrderFactory.Create( + _agentAddress, + _avatarAddress, + orderId, + new FungibleAssetValue(_goldCurrencyState.Currency, 100, 0), + tradableItem.TradableId, + requiredBlockIndex, + itemSubType, + itemCount + ); + + var orderDigestList = new OrderDigestListState(OrderDigestListState.DeriveAddress(_avatarAddress)); + var prevState = _initialState; + + if (inventoryCount > 1) + { + for (int i = 0; i < inventoryCount; i++) + { + // Different RequiredBlockIndex for divide inventory slot. + if (tradableItem is ITradableFungibleItem tradableFungibleItem) + { + var tradable = (TradableMaterial)tradableFungibleItem.Clone(); + tradable.RequiredBlockIndex = tradableItem.RequiredBlockIndex - i; + avatarState.inventory.AddItem2(tradable, 2 - i); + } + } + } + else + { + avatarState.inventory.AddItem2((ItemBase)tradableItem, itemCount); + } + + var sellItem = legacy ? order.Sell2(avatarState) : order.Sell3(avatarState); + var orderDigest = legacy ? order.Digest2(avatarState, _tableSheets.CostumeStatSheet) + : order.Digest(avatarState, _tableSheets.CostumeStatSheet); + shopState.Add(orderDigest, requiredBlockIndex); + orderDigestList.Add(orderDigest); + + if (legacy) + { + Assert.True(avatarState.inventory.TryGetTradableItems(itemId, requiredBlockIndex * 2, itemCount, out _)); + } + else + { + Assert.True(avatarState.inventory.TryGetLockedItem(new OrderLock(orderId), out _)); + } + + Assert.Equal(inventoryCount, avatarState.inventory.Items.Count); + Assert.Equal(expectedCount, avatarState.inventory.Items.Sum(i => i.count)); + + Assert.Single(shopState.OrderDigestList); + Assert.Single(orderDigestList.OrderDigestList); + + Assert.Equal(requiredBlockIndex * 2, sellItem.RequiredBlockIndex); + + if (fromPreviousAction) + { + prevState = prevState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + prevState = prevState + .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) + .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) + .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) + .SetState(_avatarAddress, avatarState.SerializeV2()); + } + + prevState = prevState + .SetState(Addresses.GetItemAddress(itemId), sellItem.Serialize()) + .SetState(Order.DeriveAddress(order.OrderId), order.Serialize()) + .SetState(orderDigestList.Address, orderDigestList.Serialize()) + .SetState(shardedShopAddress, shopState.Serialize()); + + var currencyState = prevState.GetGoldCurrency(); + var price = new FungibleAssetValue(currencyState, ProductPrice, 0); + var action = new UpdateSell0 + { + orderId = orderId, + updateSellOrderId = updateSellOrderId, + tradableId = itemId, + sellerAvatarAddress = _avatarAddress, + itemSubType = itemSubType, + price = price, + count = itemCount, + }; + var nextState = action.Execute(new ActionContext + { + BlockIndex = 101, + PreviousState = prevState, + RandomSeed = 0, + Signer = _agentAddress, + }); + + var updateSellShopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, updateSellOrderId); + var nextShopState = new ShardedShopStateV2((Dictionary)nextState.GetState(updateSellShopAddress)); + Assert.Equal(1, nextShopState.OrderDigestList.Count); + Assert.NotEqual(orderId, nextShopState.OrderDigestList.First().OrderId); + Assert.Equal(updateSellOrderId, nextShopState.OrderDigestList.First().OrderId); + Assert.Equal(itemId, nextShopState.OrderDigestList.First().TradableId); + Assert.Equal(requiredBlockIndex + 101, nextShopState.OrderDigestList.First().ExpiredBlockIndex); + } + + [Fact] + public void Execute_Throw_FailedLoadStateException() + { + var action = new UpdateSell0 + { + orderId = default, + updateSellOrderId = default, + tradableId = default, + sellerAvatarAddress = _avatarAddress, + itemSubType = ItemSubType.Food, + price = 0 * _currency, + count = 1, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = new Account(MockState.Empty), + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_Throw_InvalidPriceException() + { + var action = new UpdateSell0 + { + orderId = default, + updateSellOrderId = default, + tradableId = default, + sellerAvatarAddress = _avatarAddress, + itemSubType = default, + price = -1 * _currency, + count = 1, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_Throw_NotEnoughClearedStageLevelException() + { + var avatarState = new AvatarState(_avatarState) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + 0 + ), + }; + + _initialState = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + + var action = new UpdateSell0 + { + updateSellOrderId = default, + orderId = default, + tradableId = default, + sellerAvatarAddress = _avatarAddress, + itemSubType = ItemSubType.Food, + price = 0 * _currency, + count = 1, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + } +} diff --git a/.Lib9c.Tests/Action/UpdateSell2Test.cs b/.Lib9c.Tests/Action/UpdateSell2Test.cs new file mode 100644 index 0000000000..e34a73d722 --- /dev/null +++ b/.Lib9c.Tests/Action/UpdateSell2Test.cs @@ -0,0 +1,318 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using Bencodex.Types; + using Lib9c.Model.Order; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.State; + using Serilog; + using Xunit; + using Xunit.Abstractions; + using static SerializeKeys; + + public class UpdateSell2Test + { + private const long ProductPrice = 100; + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly Currency _currency; + private readonly AvatarState _avatarState; + private readonly TableSheets _tableSheets; + private readonly GoldCurrencyState _goldCurrencyState; + private IAccount _initialState; + + public UpdateSell2Test(ITestOutputHelper outputHelper) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(outputHelper) + .CreateLogger(); + + _initialState = new Account(MockState.Empty); + var sheets = TableSheetsImporter.ImportSheets(); + foreach (var (key, value) in sheets) + { + _initialState = _initialState + .SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _tableSheets = new TableSheets(sheets); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + _goldCurrencyState = new GoldCurrencyState(_currency); + + var shopState = new ShopState(); + + _agentAddress = new PrivateKey().Address; + var agentState = new AgentState(_agentAddress); + _avatarAddress = new PrivateKey().Address; + var rankingMapAddress = new PrivateKey().Address; + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + rankingMapAddress) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.ActionsInShop), + }; + agentState.avatarAddresses[0] = _avatarAddress; + + _initialState = _initialState + .SetState(GoldCurrencyState.Address, _goldCurrencyState.Serialize()) + .SetState(Addresses.Shop, shopState.Serialize()) + .SetState(_agentAddress, agentState.Serialize()) + .SetState(_avatarAddress, _avatarState.Serialize()); + } + + [Theory] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", 1, 1, 1, true)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", 1, 1, 1, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 1, 1, 1, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 1, 2, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 2, 3, true)] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", 1, 1, 1, false)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", 1, 1, 1, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 1, 1, 1, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 1, 2, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 2, 3, false)] + public void Execute( + ItemType itemType, + string guid, + int itemCount, + int inventoryCount, + int expectedCount, + bool fromPreviousAction + ) + { + var avatarState = _initialState.GetAvatarState(_avatarAddress); + ITradableItem tradableItem; + var itemId = new Guid(guid); + var orderId = Guid.NewGuid(); + var updateSellOrderId = Guid.NewGuid(); + ItemSubType itemSubType; + const long requiredBlockIndex = Order.ExpirationInterval; + switch (itemType) + { + case ItemType.Equipment: + { + var itemUsable = ItemFactory.CreateItemUsable( + _tableSheets.EquipmentItemSheet.First, + itemId, + requiredBlockIndex); + tradableItem = (ITradableItem)itemUsable; + itemSubType = itemUsable.ItemSubType; + break; + } + + case ItemType.Costume: + { + var costume = ItemFactory.CreateCostume(_tableSheets.CostumeItemSheet.First, itemId); + costume.Update(requiredBlockIndex); + tradableItem = costume; + itemSubType = costume.ItemSubType; + break; + } + + default: + { + var material = ItemFactory.CreateTradableMaterial( + _tableSheets.MaterialItemSheet.OrderedList.First(r => r.ItemSubType == ItemSubType.Hourglass)); + itemSubType = material.ItemSubType; + material.RequiredBlockIndex = requiredBlockIndex; + tradableItem = material; + break; + } + } + + var shardedShopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, orderId); + var shopState = new ShardedShopStateV2(shardedShopAddress); + var order = OrderFactory.Create( + _agentAddress, + _avatarAddress, + orderId, + new FungibleAssetValue(_goldCurrencyState.Currency, 100, 0), + tradableItem.TradableId, + requiredBlockIndex, + itemSubType, + itemCount + ); + + var orderDigestList = new OrderDigestListState(OrderDigestListState.DeriveAddress(_avatarAddress)); + var prevState = _initialState; + + if (inventoryCount > 1) + { + for (int i = 0; i < inventoryCount; i++) + { + // Different RequiredBlockIndex for divide inventory slot. + if (tradableItem is ITradableFungibleItem tradableFungibleItem) + { + var tradable = (TradableMaterial)tradableFungibleItem.Clone(); + tradable.RequiredBlockIndex = tradableItem.RequiredBlockIndex - i; + avatarState.inventory.AddItem(tradable, 2 - i); + } + } + } + else + { + avatarState.inventory.AddItem((ItemBase)tradableItem, itemCount); + } + + var sellItem = order.Sell(avatarState); + var orderDigest = order.Digest(avatarState, _tableSheets.CostumeStatSheet); + shopState.Add(orderDigest, requiredBlockIndex); + orderDigestList.Add(orderDigest); + + Assert.True(avatarState.inventory.TryGetLockedItem(new OrderLock(orderId), out _)); + + Assert.Equal(inventoryCount, avatarState.inventory.Items.Count); + Assert.Equal(expectedCount, avatarState.inventory.Items.Sum(i => i.count)); + + Assert.Single(shopState.OrderDigestList); + Assert.Single(orderDigestList.OrderDigestList); + + Assert.Equal(requiredBlockIndex * 2, sellItem.RequiredBlockIndex); + + if (fromPreviousAction) + { + prevState = prevState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + prevState = prevState + .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) + .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) + .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) + .SetState(_avatarAddress, avatarState.SerializeV2()); + } + + prevState = prevState + .SetState(Addresses.GetItemAddress(itemId), sellItem.Serialize()) + .SetState(Order.DeriveAddress(order.OrderId), order.Serialize()) + .SetState(orderDigestList.Address, orderDigestList.Serialize()) + .SetState(shardedShopAddress, shopState.Serialize()); + + var currencyState = prevState.GetGoldCurrency(); + var price = new FungibleAssetValue(currencyState, ProductPrice, 0); + var action = new UpdateSell2 + { + orderId = orderId, + updateSellOrderId = updateSellOrderId, + tradableId = itemId, + sellerAvatarAddress = _avatarAddress, + itemSubType = itemSubType, + price = price, + count = itemCount, + }; + var nextState = action.Execute(new ActionContext + { + BlockIndex = 101, + PreviousState = prevState, + RandomSeed = 0, + Signer = _agentAddress, + }); + + var updateSellShopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, updateSellOrderId); + var nextShopState = new ShardedShopStateV2((Dictionary)nextState.GetState(updateSellShopAddress)); + Assert.Equal(1, nextShopState.OrderDigestList.Count); + Assert.NotEqual(orderId, nextShopState.OrderDigestList.First().OrderId); + Assert.Equal(updateSellOrderId, nextShopState.OrderDigestList.First().OrderId); + Assert.Equal(itemId, nextShopState.OrderDigestList.First().TradableId); + Assert.Equal(requiredBlockIndex + 101, nextShopState.OrderDigestList.First().ExpiredBlockIndex); + } + + [Fact] + public void Execute_Throw_FailedLoadStateException() + { + var action = new UpdateSell2 + { + orderId = default, + updateSellOrderId = default, + tradableId = default, + sellerAvatarAddress = _avatarAddress, + itemSubType = ItemSubType.Food, + price = 0 * _currency, + count = 1, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = new Account(MockState.Empty), + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_Throw_InvalidPriceException() + { + var action = new UpdateSell2 + { + orderId = default, + updateSellOrderId = default, + tradableId = default, + sellerAvatarAddress = _avatarAddress, + itemSubType = default, + price = -1 * _currency, + count = 1, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_Throw_NotEnoughClearedStageLevelException() + { + var avatarState = new AvatarState(_avatarState) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + 0 + ), + }; + + _initialState = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + + var action = new UpdateSell2 + { + updateSellOrderId = default, + orderId = default, + tradableId = default, + sellerAvatarAddress = _avatarAddress, + itemSubType = ItemSubType.Food, + price = 0 * _currency, + count = 1, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + } +} diff --git a/.Lib9c.Tests/Action/UpdateSell3Test.cs b/.Lib9c.Tests/Action/UpdateSell3Test.cs new file mode 100644 index 0000000000..91e0376e6b --- /dev/null +++ b/.Lib9c.Tests/Action/UpdateSell3Test.cs @@ -0,0 +1,389 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Bencodex.Types; + using Lib9c.Model.Order; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Battle; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.State; + using Serilog; + using Xunit; + using Xunit.Abstractions; + using static Lib9c.SerializeKeys; + + public class UpdateSell3Test + { + private const long ProductPrice = 100; + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly Currency _currency; + private readonly AvatarState _avatarState; + private readonly TableSheets _tableSheets; + private readonly GoldCurrencyState _goldCurrencyState; + private IAccount _initialState; + + public UpdateSell3Test(ITestOutputHelper outputHelper) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(outputHelper) + .CreateLogger(); + + _initialState = new Account(MockState.Empty); + var sheets = TableSheetsImporter.ImportSheets(); + foreach (var (key, value) in sheets) + { + _initialState = _initialState + .SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _tableSheets = new TableSheets(sheets); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + _goldCurrencyState = new GoldCurrencyState(_currency); + + var shopState = new ShopState(); + + _agentAddress = new PrivateKey().Address; + var agentState = new AgentState(_agentAddress); + _avatarAddress = new PrivateKey().Address; + var rankingMapAddress = new PrivateKey().Address; + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + rankingMapAddress) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.ActionsInShop), + }; + agentState.avatarAddresses[0] = _avatarAddress; + + _initialState = _initialState + .SetState(GoldCurrencyState.Address, _goldCurrencyState.Serialize()) + .SetState(Addresses.Shop, shopState.Serialize()) + .SetState(_agentAddress, agentState.Serialize()) + .SetState(_avatarAddress, _avatarState.Serialize()); + } + + [Theory] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", 1, 1, 1, true)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", 1, 1, 1, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 1, 1, 1, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 1, 2, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 2, 3, true)] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", 1, 1, 1, false)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", 1, 1, 1, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 1, 1, 1, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 1, 2, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 2, 3, false)] + public void Execute( + ItemType itemType, + string guid, + int itemCount, + int inventoryCount, + int expectedCount, + bool fromPreviousAction + ) + { + var avatarState = _initialState.GetAvatarState(_avatarAddress); + ITradableItem tradableItem; + var itemId = new Guid(guid); + var orderId = Guid.NewGuid(); + var updateSellOrderId = Guid.NewGuid(); + ItemSubType itemSubType; + const long requiredBlockIndex = Order.ExpirationInterval; + switch (itemType) + { + case ItemType.Equipment: + { + var itemUsable = ItemFactory.CreateItemUsable( + _tableSheets.EquipmentItemSheet.First, + itemId, + requiredBlockIndex); + tradableItem = (ITradableItem)itemUsable; + itemSubType = itemUsable.ItemSubType; + break; + } + + case ItemType.Costume: + { + var costume = ItemFactory.CreateCostume(_tableSheets.CostumeItemSheet.First, itemId); + costume.Update(requiredBlockIndex); + tradableItem = costume; + itemSubType = costume.ItemSubType; + break; + } + + default: + { + var material = ItemFactory.CreateTradableMaterial( + _tableSheets.MaterialItemSheet.OrderedList.First(r => r.ItemSubType == ItemSubType.Hourglass)); + itemSubType = material.ItemSubType; + material.RequiredBlockIndex = requiredBlockIndex; + tradableItem = material; + break; + } + } + + var shardedShopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, orderId); + var shopState = new ShardedShopStateV2(shardedShopAddress); + var order = OrderFactory.Create( + _agentAddress, + _avatarAddress, + orderId, + new FungibleAssetValue(_goldCurrencyState.Currency, 100, 0), + tradableItem.TradableId, + requiredBlockIndex, + itemSubType, + itemCount + ); + + var orderDigestList = new OrderDigestListState(OrderDigestListState.DeriveAddress(_avatarAddress)); + var prevState = _initialState; + + if (inventoryCount > 1) + { + for (int i = 0; i < inventoryCount; i++) + { + // Different RequiredBlockIndex for divide inventory slot. + if (tradableItem is ITradableFungibleItem tradableFungibleItem) + { + var tradable = (TradableMaterial)tradableFungibleItem.Clone(); + tradable.RequiredBlockIndex = tradableItem.RequiredBlockIndex - i; + avatarState.inventory.AddItem(tradable, 2 - i); + } + } + } + else + { + avatarState.inventory.AddItem((ItemBase)tradableItem, itemCount); + } + + var sellItem = order.Sell(avatarState); + var orderDigest = order.Digest(avatarState, _tableSheets.CostumeStatSheet); + shopState.Add(orderDigest, requiredBlockIndex); + orderDigestList.Add(orderDigest); + + Assert.True(avatarState.inventory.TryGetLockedItem(new OrderLock(orderId), out _)); + + Assert.Equal(inventoryCount, avatarState.inventory.Items.Count); + Assert.Equal(expectedCount, avatarState.inventory.Items.Sum(i => i.count)); + + Assert.Single(shopState.OrderDigestList); + Assert.Single(orderDigestList.OrderDigestList); + + Assert.Equal(requiredBlockIndex * 2, sellItem.RequiredBlockIndex); + + if (fromPreviousAction) + { + prevState = prevState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + prevState = prevState + .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) + .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) + .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) + .SetState(_avatarAddress, avatarState.SerializeV2()); + } + + prevState = prevState + .SetState(Addresses.GetItemAddress(itemId), sellItem.Serialize()) + .SetState(Order.DeriveAddress(order.OrderId), order.Serialize()) + .SetState(orderDigestList.Address, orderDigestList.Serialize()) + .SetState(shardedShopAddress, shopState.Serialize()); + + var currencyState = prevState.GetGoldCurrency(); + var price = new FungibleAssetValue(currencyState, ProductPrice, 0); + + var updateSellInfo = new UpdateSellInfo( + orderId, + updateSellOrderId, + itemId, + itemSubType, + price, + itemCount + ); + + var action = new UpdateSell3 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new[] { updateSellInfo }, + }; + + var nextState = action.Execute(new ActionContext + { + BlockIndex = 101, + PreviousState = prevState, + RandomSeed = 0, + Signer = _agentAddress, + }); + + var updateSellShopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, updateSellOrderId); + var nextShopState = new ShardedShopStateV2((Dictionary)nextState.GetState(updateSellShopAddress)); + Assert.Equal(1, nextShopState.OrderDigestList.Count); + Assert.NotEqual(orderId, nextShopState.OrderDigestList.First().OrderId); + Assert.Equal(updateSellOrderId, nextShopState.OrderDigestList.First().OrderId); + Assert.Equal(itemId, nextShopState.OrderDigestList.First().TradableId); + Assert.Equal(requiredBlockIndex + 101, nextShopState.OrderDigestList.First().ExpiredBlockIndex); + } + + [Fact] + public void Execute_Throw_ListEmptyException() + { + var action = new UpdateSell3 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new List(), + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = new Account(MockState.Empty), + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_Throw_FailedLoadStateException() + { + var updateSellInfo = new UpdateSellInfo( + default, + default, + default, + ItemSubType.Food, + 0 * _currency, + 1); + + var action = new UpdateSell3 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new[] { updateSellInfo }, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = new Account(MockState.Empty), + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_Throw_NotEnoughClearedStageLevelException() + { + var avatarState = new AvatarState(_avatarState) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + 0 + ), + }; + + _initialState = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + + var updateSellInfo = new UpdateSellInfo( + default, + default, + default, + ItemSubType.Food, + 0 * _currency, + 1); + + var action = new UpdateSell3 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new[] { updateSellInfo }, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_Throw_InvalidPriceException() + { + var avatarState = new AvatarState(_avatarState) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.ActionsInShop + ), + }; + var digestListAddress = OrderDigestListState.DeriveAddress(_avatarAddress); + var digestList = new OrderDigestListState(digestListAddress); + _initialState = _initialState + .SetState(_avatarAddress, avatarState.Serialize()) + .SetState(digestListAddress, digestList.Serialize()); + + var updateSellInfo = new UpdateSellInfo( + default, + default, + default, + default, + -1 * _currency, + 1); + + var action = new UpdateSell3 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new[] { updateSellInfo }, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_ActionObsoletedException() + { + var updateSellInfo = new UpdateSellInfo( + default, + default, + default, + default, + -1 * _currency, + 1); + + var action = new UpdateSell3 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new[] { updateSellInfo }, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = ActionObsoleteConfig.V100320ObsoleteIndex + 1, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + } +} diff --git a/.Lib9c.Tests/Action/UpdateSell4Test.cs b/.Lib9c.Tests/Action/UpdateSell4Test.cs new file mode 100644 index 0000000000..15a963f373 --- /dev/null +++ b/.Lib9c.Tests/Action/UpdateSell4Test.cs @@ -0,0 +1,408 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Bencodex.Types; + using Lib9c.Model.Order; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Battle; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.State; + using Serilog; + using Xunit; + using Xunit.Abstractions; + using static Lib9c.SerializeKeys; + + public class UpdateSell4Test + { + private const long ProductPrice = 100; + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly Currency _currency; + private readonly AvatarState _avatarState; + private readonly TableSheets _tableSheets; + private readonly GoldCurrencyState _goldCurrencyState; + private IAccount _initialState; + + public UpdateSell4Test(ITestOutputHelper outputHelper) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(outputHelper) + .CreateLogger(); + + _initialState = new Account(MockState.Empty); + var sheets = TableSheetsImporter.ImportSheets(); + foreach (var (key, value) in sheets) + { + _initialState = _initialState + .SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _tableSheets = new TableSheets(sheets); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + _goldCurrencyState = new GoldCurrencyState(_currency); + + var shopState = new ShopState(); + + _agentAddress = new PrivateKey().Address; + var agentState = new AgentState(_agentAddress); + _avatarAddress = new PrivateKey().Address; + var rankingMapAddress = new PrivateKey().Address; + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + rankingMapAddress) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.ActionsInShop), + }; + agentState.avatarAddresses[0] = _avatarAddress; + + _initialState = _initialState + .SetState(GoldCurrencyState.Address, _goldCurrencyState.Serialize()) + .SetState(Addresses.Shop, shopState.Serialize()) + .SetState(_agentAddress, agentState.Serialize()) + .SetState(_avatarAddress, _avatarState.Serialize()); + } + + [Theory] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", 1, 1, 1, true)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", 1, 1, 1, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 1, 1, 1, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 1, 2, true)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 2, 3, true)] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", 1, 1, 1, false)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", 1, 1, 1, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 1, 1, 1, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 1, 2, false)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", 2, 2, 3, false)] + public void Execute( + ItemType itemType, + string guid, + int itemCount, + int inventoryCount, + int expectedCount, + bool fromPreviousAction + ) + { + var avatarState = _initialState.GetAvatarState(_avatarAddress); + ITradableItem tradableItem; + var itemId = new Guid(guid); + var orderId = Guid.NewGuid(); + var updateSellOrderId = Guid.NewGuid(); + ItemSubType itemSubType; + const long requiredBlockIndex = Order.ExpirationInterval; + switch (itemType) + { + case ItemType.Equipment: + { + var itemUsable = ItemFactory.CreateItemUsable( + _tableSheets.EquipmentItemSheet.First, + itemId, + requiredBlockIndex); + tradableItem = (ITradableItem)itemUsable; + itemSubType = itemUsable.ItemSubType; + break; + } + + case ItemType.Costume: + { + var costume = ItemFactory.CreateCostume(_tableSheets.CostumeItemSheet.First, itemId); + costume.Update(requiredBlockIndex); + tradableItem = costume; + itemSubType = costume.ItemSubType; + break; + } + + default: + { + var material = ItemFactory.CreateTradableMaterial( + _tableSheets.MaterialItemSheet.OrderedList.First(r => r.ItemSubType == ItemSubType.Hourglass)); + itemSubType = material.ItemSubType; + material.RequiredBlockIndex = requiredBlockIndex; + tradableItem = material; + break; + } + } + + var shardedShopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, orderId); + var shopState = new ShardedShopStateV2(shardedShopAddress); + var order = OrderFactory.Create( + _agentAddress, + _avatarAddress, + orderId, + new FungibleAssetValue(_goldCurrencyState.Currency, 100, 0), + tradableItem.TradableId, + requiredBlockIndex, + itemSubType, + itemCount + ); + + var orderDigestList = new OrderDigestListState(OrderDigestListState.DeriveAddress(_avatarAddress)); + var prevState = _initialState; + + if (inventoryCount > 1) + { + for (int i = 0; i < inventoryCount; i++) + { + // Different RequiredBlockIndex for divide inventory slot. + if (tradableItem is ITradableFungibleItem tradableFungibleItem) + { + var tradable = (TradableMaterial)tradableFungibleItem.Clone(); + tradable.RequiredBlockIndex = tradableItem.RequiredBlockIndex - i; + avatarState.inventory.AddItem(tradable, 2 - i); + } + } + } + else + { + avatarState.inventory.AddItem((ItemBase)tradableItem, itemCount); + } + + var sellItem = order.Sell(avatarState); + var orderDigest = order.Digest(avatarState, _tableSheets.CostumeStatSheet); + shopState.Add(orderDigest, requiredBlockIndex); + orderDigestList.Add(orderDigest); + + Assert.True(avatarState.inventory.TryGetLockedItem(new OrderLock(orderId), out _)); + + Assert.Equal(inventoryCount, avatarState.inventory.Items.Count); + Assert.Equal(expectedCount, avatarState.inventory.Items.Sum(i => i.count)); + + Assert.Single(shopState.OrderDigestList); + Assert.Single(orderDigestList.OrderDigestList); + + Assert.Equal(requiredBlockIndex * 2, sellItem.RequiredBlockIndex); + + if (fromPreviousAction) + { + prevState = prevState.SetState(_avatarAddress, avatarState.Serialize()); + } + else + { + prevState = prevState + .SetState(_avatarAddress.Derive(LegacyInventoryKey), avatarState.inventory.Serialize()) + .SetState(_avatarAddress.Derive(LegacyWorldInformationKey), avatarState.worldInformation.Serialize()) + .SetState(_avatarAddress.Derive(LegacyQuestListKey), avatarState.questList.Serialize()) + .SetState(_avatarAddress, avatarState.SerializeV2()); + } + + prevState = prevState + .SetState(Addresses.GetItemAddress(itemId), sellItem.Serialize()) + .SetState(Order.DeriveAddress(order.OrderId), order.Serialize()) + .SetState(orderDigestList.Address, orderDigestList.Serialize()) + .SetState(shardedShopAddress, shopState.Serialize()); + + var currencyState = prevState.GetGoldCurrency(); + var price = new FungibleAssetValue(currencyState, ProductPrice, 0); + + var updateSellInfo = new UpdateSellInfo( + orderId, + updateSellOrderId, + itemId, + itemSubType, + price, + itemCount + ); + + var action = new UpdateSell4 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new[] { updateSellInfo }, + }; + + var nextState = action.Execute(new ActionContext + { + BlockIndex = 101, + PreviousState = prevState, + RandomSeed = 0, + Signer = _agentAddress, + }); + + var updateSellShopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, updateSellOrderId); + var nextShopState = new ShardedShopStateV2((Dictionary)nextState.GetState(updateSellShopAddress)); + Assert.Equal(1, nextShopState.OrderDigestList.Count); + Assert.NotEqual(orderId, nextShopState.OrderDigestList.First().OrderId); + Assert.Equal(updateSellOrderId, nextShopState.OrderDigestList.First().OrderId); + Assert.Equal(itemId, nextShopState.OrderDigestList.First().TradableId); + Assert.Equal(requiredBlockIndex + 101, nextShopState.OrderDigestList.First().ExpiredBlockIndex); + } + + [Fact] + public void Execute_Throw_ListEmptyException() + { + var action = new UpdateSell4 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new List(), + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = new Account(MockState.Empty), + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_Throw_FailedLoadStateException() + { + var updateSellInfo = new UpdateSellInfo( + default, + default, + default, + ItemSubType.Food, + 0 * _currency, + 1); + + var action = new UpdateSell4 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new[] { updateSellInfo }, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = new Account(MockState.Empty), + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_Throw_NotEnoughClearedStageLevelException() + { + var avatarState = new AvatarState(_avatarState) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + 0 + ), + }; + + _initialState = _initialState.SetState(_avatarAddress, avatarState.Serialize()); + + var updateSellInfo = new UpdateSellInfo( + default, + default, + default, + ItemSubType.Food, + 0 * _currency, + 1); + + var action = new UpdateSell4 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new[] { updateSellInfo }, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + + [Fact] + public void Execute_Throw_InvalidPriceException() + { + var avatarState = new AvatarState(_avatarState) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.ActionsInShop + ), + }; + var digestListAddress = OrderDigestListState.DeriveAddress(_avatarAddress); + var digestList = new OrderDigestListState(digestListAddress); + _initialState = _initialState + .SetState(_avatarAddress, avatarState.Serialize()) + .SetState(digestListAddress, digestList.Serialize()); + + var updateSellInfo = new UpdateSellInfo( + default, + default, + default, + default, + -1 * _currency, + 1); + + var action = new UpdateSell4 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = new[] { updateSellInfo }, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + + [Theory] + [InlineData(100, false)] + [InlineData(1, false)] + [InlineData(101, true)] + public void PurchaseInfos_Capacity(int count, bool exc) + { + var updateSellInfo = new UpdateSellInfo( + default, + default, + default, + default, + -1 * _currency, + 1); + var updateSellInfos = new List(); + for (int i = 0; i < count; i++) + { + updateSellInfos.Add(updateSellInfo); + } + + var action = new UpdateSell4 + { + sellerAvatarAddress = _avatarAddress, + updateSellInfos = updateSellInfos, + }; + if (exc) + { + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + else + { + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousState = _initialState, + Signer = _agentAddress, + })); + } + } + } +} diff --git a/Lib9c/Action/TransferAsset2.cs b/Lib9c/Action/TransferAsset2.cs new file mode 100644 index 0000000000..8ccdf99b7e --- /dev/null +++ b/Lib9c/Action/TransferAsset2.cs @@ -0,0 +1,162 @@ +using Bencodex; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Model.State; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Lib9c.Abstractions; +using Nekoyume.Model; +using Serilog; + +namespace Nekoyume.Action +{ + /// + /// Hard forked at https://github.com/planetarium/lib9c/pull/636 + /// Updated at https://github.com/planetarium/lib9c/pull/957 + /// + [Serializable] + [ActionObsolete(TransferAsset3.CrystalTransferringRestrictionStartIndex - 1)] + [ActionType("transfer_asset2")] + public class TransferAsset2 : ActionBase, ISerializable, ITransferAsset, ITransferAssetV1 + { + private const int MemoMaxLength = 80; + + public TransferAsset2() + { + } + + public TransferAsset2(Address sender, Address recipient, FungibleAssetValue amount, string memo = null) + { + Sender = sender; + Recipient = recipient; + Amount = amount; + + CheckMemoLength(memo); + Memo = memo; + } + + protected TransferAsset2(SerializationInfo info, StreamingContext context) + { + var rawBytes = (byte[])info.GetValue("serialized", typeof(byte[])); + Dictionary pv = (Dictionary) new Codec().Decode(rawBytes); + + LoadPlainValue(pv); + } + + public Address Sender { get; private set; } + public Address Recipient { get; private set; } + public FungibleAssetValue Amount { get; private set; } + public string Memo { get; private set; } + + Address ITransferAssetV1.Sender => Sender; + Address ITransferAssetV1.Recipient => Recipient; + FungibleAssetValue ITransferAssetV1.Amount => Amount; + string ITransferAssetV1.Memo => Memo; + + public override IValue PlainValue + { + get + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text) "sender", Sender.Serialize()), + new KeyValuePair((Text) "recipient", Recipient.Serialize()), + new KeyValuePair((Text) "amount", Amount.Serialize()), + }; + + if (!(Memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text) "memo", Memo.Serialize())); + } + + return Dictionary.Empty + .Add("type_id", "transfer_asset2") + .Add("values", new Dictionary(pairs)); + } + } + + public override IAccount Execute(IActionContext context) + { + context.UseGas(4); + var state = context.PreviousState; + + CheckObsolete(TransferAsset3.CrystalTransferringRestrictionStartIndex - 1, context); + var addressesHex = GetSignerAndOtherAddressesHex(context, context.Signer); + var started = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex}TransferAsset2 exec started", addressesHex); + if (Sender != context.Signer) + { + throw new InvalidTransferSignerException(context.Signer, Sender, Recipient); + } + + // This works for block after 380000. Please take a look at + // https://github.com/planetarium/libplanet/pull/1133 + if (context.BlockIndex > 380000 && Sender == Recipient) + { + throw new InvalidTransferRecipientException(Sender, Recipient); + } + + Address recipientAddress = Recipient.Derive(ActivationKey.DeriveKey); + + // Check new type of activation first. + if (state.GetState(recipientAddress) is null && state.GetState(Addresses.ActivatedAccount) is Dictionary asDict ) + { + var activatedAccountsState = new ActivatedAccountsState(asDict); + var activatedAccounts = activatedAccountsState.Accounts; + // if ActivatedAccountsState is empty, all user is activate. + if (activatedAccounts.Count != 0 + && !activatedAccounts.Contains(Recipient)) + { + throw new InvalidTransferUnactivatedRecipientException(Sender, Recipient); + } + } + + Currency currency = Amount.Currency; + if (!(currency.Minters is null) && + (currency.Minters.Contains(Sender) || currency.Minters.Contains(Recipient))) + { + throw new InvalidTransferMinterException( + currency.Minters, + Sender, + Recipient + ); + } + + var ended = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex}TransferAsset2 Total Executed Time: {Elapsed}", addressesHex, ended - started); + return state.TransferAsset(context, Sender, Recipient, Amount); + } + + public override void LoadPlainValue(IValue plainValue) + { + var asDict = (Dictionary)((Dictionary)plainValue)["values"]; + + Sender = asDict["sender"].ToAddress(); + Recipient = asDict["recipient"].ToAddress(); + Amount = asDict["amount"].ToFungibleAssetValue(); + Memo = asDict.TryGetValue((Text) "memo", out IValue memo) ? memo.ToDotnetString() : null; + + CheckMemoLength(Memo); + } + + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("serialized", new Codec().Encode(PlainValue)); + } + + private void CheckMemoLength(string memo) + { + if (memo?.Length > MemoMaxLength) + { + string msg = $"The length of the memo, {memo.Length}, " + + $"is overflowed than the max length, {MemoMaxLength}."; + throw new MemoLengthOverflowException(msg); + } + } + } +} diff --git a/Lib9c/Action/TransferAsset4.cs b/Lib9c/Action/TransferAsset4.cs new file mode 100644 index 0000000000..0f8bc34a4f --- /dev/null +++ b/Lib9c/Action/TransferAsset4.cs @@ -0,0 +1,150 @@ +using Bencodex; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Model.State; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Lib9c; +using Lib9c.Abstractions; +using Nekoyume.Helper; +using Nekoyume.Model; +using Serilog; + +namespace Nekoyume.Action +{ + /// + /// Hard forked at https://github.com/planetarium/lib9c/pull/636 + /// Updated at https://github.com/planetarium/lib9c/pull/2143 + /// + [Serializable] + [ActionType(TypeIdentifier)] + [ActionObsolete(ObsoleteBlockIndex)] + public class TransferAsset4 : ActionBase, ISerializable, ITransferAsset, ITransferAssetV1 + { + private const int MemoMaxLength = 80; + public const string TypeIdentifier = "transfer_asset4"; + public const long ObsoleteBlockIndex = ActionObsoleteConfig.V200080ObsoleteIndex; + + public TransferAsset4() + { + } + + public TransferAsset4(Address sender, Address recipient, FungibleAssetValue amount, string memo = null) + { + Sender = sender; + Recipient = recipient; + Amount = amount; + + CheckMemoLength(memo); + Memo = memo; + } + + protected TransferAsset4(SerializationInfo info, StreamingContext context) + { + var rawBytes = (byte[])info.GetValue("serialized", typeof(byte[])); + Dictionary pv = (Dictionary) new Codec().Decode(rawBytes); + + LoadPlainValue(pv); + } + + public Address Sender { get; private set; } + public Address Recipient { get; private set; } + public FungibleAssetValue Amount { get; private set; } + public string Memo { get; private set; } + + Address ITransferAssetV1.Sender => Sender; + Address ITransferAssetV1.Recipient => Recipient; + FungibleAssetValue ITransferAssetV1.Amount => Amount; + string ITransferAssetV1.Memo => Memo; + + public override IValue PlainValue + { + get + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text) "sender", Sender.Serialize()), + new KeyValuePair((Text) "recipient", Recipient.Serialize()), + new KeyValuePair((Text) "amount", Amount.Serialize()), + }; + + if (!(Memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text) "memo", Memo.Serialize())); + } + + return Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", new Dictionary(pairs)); + } + } + + public override IAccount Execute(IActionContext context) + { + context.UseGas(4); + Address signer = context.Signer; + var state = context.PreviousState; + + var addressesHex = GetSignerAndOtherAddressesHex(context, signer); + var started = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex}TransferAsset4 exec started", addressesHex); + if (Sender != signer) + { + throw new InvalidTransferSignerException(signer, Sender, Recipient); + } + + if (Sender == Recipient) + { + throw new InvalidTransferRecipientException(Sender, Recipient); + } + + Currency currency = Amount.Currency; + if (!(currency.Minters is null) && + (currency.Minters.Contains(Sender) || currency.Minters.Contains(Recipient))) + { + throw new InvalidTransferMinterException( + currency.Minters, + Sender, + Recipient + ); + } + + TransferAsset3.CheckCrystalSender(currency, context.BlockIndex, Sender); + var ended = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex}TransferAsset4 Total Executed Time: {Elapsed}", addressesHex, ended - started); + return state.TransferAsset(context, Sender, Recipient, Amount); + } + + public override void LoadPlainValue(IValue plainValue) + { + var asDict = (Dictionary)((Dictionary)plainValue)["values"]; + + Sender = asDict["sender"].ToAddress(); + Recipient = asDict["recipient"].ToAddress(); + Amount = asDict["amount"].ToFungibleAssetValue(); + Memo = asDict.TryGetValue((Text) "memo", out IValue memo) ? memo.ToDotnetString() : null; + + CheckMemoLength(Memo); + } + + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("serialized", new Codec().Encode(PlainValue)); + } + + private void CheckMemoLength(string memo) + { + if (memo?.Length > MemoMaxLength) + { + string msg = $"The length of the memo, {memo.Length}, " + + $"is overflowed than the max length, {MemoMaxLength}."; + throw new MemoLengthOverflowException(msg); + } + } + } +} diff --git a/Lib9c/Action/TransferAssets0.cs b/Lib9c/Action/TransferAssets0.cs new file mode 100644 index 0000000000..641c1070d5 --- /dev/null +++ b/Lib9c/Action/TransferAssets0.cs @@ -0,0 +1,186 @@ +using Bencodex; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Model.State; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Lib9c.Abstractions; +using Nekoyume.Model; +using Serilog; + +namespace Nekoyume.Action +{ + /// + /// Hard forked at https://github.com/planetarium/lib9c/pull/636 + /// Updated at https://github.com/planetarium/lib9c/pull/957 + /// + [Serializable] + [ActionType("transfer_assets")] + [ActionObsolete(ActionObsoleteConfig.V200030ObsoleteIndex)] + public class TransferAssets0 : ActionBase, ISerializable, ITransferAssets, ITransferAssetsV1 + { + public const int RecipientsCapacity = 100; + private const int MemoMaxLength = 80; + + public TransferAssets0() + { + } + + public TransferAssets0(Address sender, List<(Address, FungibleAssetValue)> recipients, string memo = null) + { + Sender = sender; + Recipients = recipients; + + CheckMemoLength(memo); + Memo = memo; + } + + protected TransferAssets0(SerializationInfo info, StreamingContext context) + { + var rawBytes = (byte[])info.GetValue("serialized", typeof(byte[])); + Dictionary pv = (Dictionary) new Codec().Decode(rawBytes); + + LoadPlainValue(pv); + } + + public Address Sender { get; private set; } + public List<(Address recipient, FungibleAssetValue amount)> Recipients { get; private set; } + public string Memo { get; private set; } + + Address ITransferAssetsV1.Sender => Sender; + + List<(Address recipient, FungibleAssetValue amount)> ITransferAssetsV1.Recipients => + Recipients; + string ITransferAssetsV1.Memo => Memo; + + public override IValue PlainValue + { + get + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text) "sender", Sender.Serialize()), + new KeyValuePair((Text) "recipients", Recipients.Aggregate(List.Empty, (list, t) => list.Add(List.Empty.Add(t.recipient.Serialize()).Add(t.amount.Serialize())))), + }; + + if (!(Memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text) "memo", Memo.Serialize())); + } + + return Dictionary.Empty + .Add("type_id", "transfer_assets") + .Add("values", new Dictionary(pairs)); + } + } + + public override IAccount Execute(IActionContext context) + { + context.UseGas(4); + var state = context.PreviousState; + + CheckObsolete(ActionObsoleteConfig.V200030ObsoleteIndex, context); + if (Recipients.Count > RecipientsCapacity) + { + throw new ArgumentOutOfRangeException($"{nameof(Recipients)} must be less than or equal {RecipientsCapacity}."); + } + var addressesHex = GetSignerAndOtherAddressesHex(context, context.Signer); + var started = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex}transfer_assets exec started", addressesHex); + + var activatedAccountsState = state.GetState(Addresses.ActivatedAccount) is Dictionary asDict + ? new ActivatedAccountsState(asDict) + : new ActivatedAccountsState(); + + state = Recipients.Aggregate(state, (current, t) => Transfer(context, current, context.Signer, t.recipient, t.amount, activatedAccountsState, context.BlockIndex)); + var ended = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex}transfer_assets Total Executed Time: {Elapsed}", addressesHex, ended - started); + + return state; + } + + public override void LoadPlainValue(IValue plainValue) + { + var asDict = (Dictionary)((Dictionary)plainValue)["values"]; + + Sender = asDict["sender"].ToAddress(); + var rawMap = (List)asDict["recipients"]; + Recipients = new List<(Address recipient, FungibleAssetValue amount)>(); + foreach (var iValue in rawMap) + { + var list = (List) iValue; + Recipients.Add((list[0].ToAddress(), list[1].ToFungibleAssetValue())); + } + Memo = asDict.TryGetValue((Text) "memo", out IValue memo) ? memo.ToDotnetString() : null; + + CheckMemoLength(Memo); + } + + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("serialized", new Codec().Encode(PlainValue)); + } + + private void CheckMemoLength(string memo) + { + if (memo?.Length > MemoMaxLength) + { + string msg = $"The length of the memo, {memo.Length}, " + + $"is overflowed than the max length, {MemoMaxLength}."; + throw new MemoLengthOverflowException(msg); + } + } + + private IAccount Transfer( + IActionContext context, IAccount state, Address signer, Address recipient, FungibleAssetValue amount, ActivatedAccountsState activatedAccountsState, long blockIndex) + { + if (Sender != signer) + { + throw new InvalidTransferSignerException(signer, Sender, recipient); + } + + if (Sender == recipient) + { + throw new InvalidTransferRecipientException(Sender, recipient); + } + + Address recipientAddress = recipient.Derive(ActivationKey.DeriveKey); + + // Check new type of activation first. + // If result of GetState is not null, it is assumed that it has been activated. + if ( + state.GetState(recipientAddress) is null && + state.GetState(recipient) is null + ) + { + var activatedAccounts = activatedAccountsState.Accounts; + // if ActivatedAccountsState is empty, all user is activate. + if (activatedAccounts.Count != 0 + && !activatedAccounts.Contains(recipient) + && state.GetState(recipient) is null) + { + throw new InvalidTransferUnactivatedRecipientException(Sender, recipient); + } + } + + Currency currency = amount.Currency; + if (!(currency.Minters is null) && + (currency.Minters.Contains(Sender) || currency.Minters.Contains(recipient))) + { + throw new InvalidTransferMinterException( + currency.Minters, + Sender, + recipient + ); + } + + TransferAsset3.CheckCrystalSender(currency, blockIndex, Sender); + return state.TransferAsset(context, Sender, recipient, amount); + } + } +} diff --git a/Lib9c/Action/TransferAssets2.cs b/Lib9c/Action/TransferAssets2.cs new file mode 100644 index 0000000000..b86c6a137c --- /dev/null +++ b/Lib9c/Action/TransferAssets2.cs @@ -0,0 +1,164 @@ +using Bencodex; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Model.State; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Lib9c.Abstractions; +using Nekoyume.Model; +using Serilog; + +namespace Nekoyume.Action +{ + /// + /// Hard forked at https://github.com/planetarium/lib9c/pull/636 + /// Updated at https://github.com/planetarium/lib9c/pull/957 + /// + [Serializable] + [ActionType(TypeIdentifier)] + [ActionObsolete(ActionObsoleteConfig.V200090ObsoleteIndex)] + public class TransferAssets2 : ActionBase, ISerializable, ITransferAssets, ITransferAssetsV1 + { + public const string TypeIdentifier = "transfer_assets2"; + public const int RecipientsCapacity = 100; + private const int MemoMaxLength = 80; + + public TransferAssets2() + { + } + + public TransferAssets2(Address sender, List<(Address, FungibleAssetValue)> recipients, string memo = null) + { + Sender = sender; + Recipients = recipients; + + CheckMemoLength(memo); + Memo = memo; + } + + protected TransferAssets2(SerializationInfo info, StreamingContext context) + { + var rawBytes = (byte[])info.GetValue("serialized", typeof(byte[])); + Dictionary pv = (Dictionary) new Codec().Decode(rawBytes); + + LoadPlainValue(pv); + } + + public Address Sender { get; private set; } + public List<(Address recipient, FungibleAssetValue amount)> Recipients { get; private set; } + public string Memo { get; private set; } + + Address ITransferAssetsV1.Sender => Sender; + + List<(Address recipient, FungibleAssetValue amount)> ITransferAssetsV1.Recipients => + Recipients; + string ITransferAssetsV1.Memo => Memo; + + public override IValue PlainValue + { + get + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text) "sender", Sender.Serialize()), + new KeyValuePair((Text) "recipients", Recipients.Aggregate(List.Empty, (list, t) => list.Add(List.Empty.Add(t.recipient.Serialize()).Add(t.amount.Serialize())))), + }; + + if (!(Memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text) "memo", Memo.Serialize())); + } + + return Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", new Dictionary(pairs)); + } + } + + public override IAccount Execute(IActionContext context) + { + context.UseGas(4); + var state = context.PreviousState; + CheckObsolete(ActionObsoleteConfig.V200090ObsoleteIndex, context); + + if (Recipients.Count > RecipientsCapacity) + { + throw new ArgumentOutOfRangeException($"{nameof(Recipients)} must be less than or equal {RecipientsCapacity}."); + } + var addressesHex = GetSignerAndOtherAddressesHex(context, context.Signer); + var started = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex}{ActionName} exec started", addressesHex, TypeIdentifier); + + state = Recipients.Aggregate(state, (current, t) => Transfer(context, current, context.Signer, t.recipient, t.amount, context.BlockIndex)); + var ended = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex}{ActionName} Total Executed Time: {Elapsed}", addressesHex, TypeIdentifier, ended - started); + + return state; + } + + public override void LoadPlainValue(IValue plainValue) + { + var asDict = (Dictionary)((Dictionary)plainValue)["values"]; + + Sender = asDict["sender"].ToAddress(); + var rawMap = (List)asDict["recipients"]; + Recipients = new List<(Address recipient, FungibleAssetValue amount)>(); + foreach (var iValue in rawMap) + { + var list = (List) iValue; + Recipients.Add((list[0].ToAddress(), list[1].ToFungibleAssetValue())); + } + Memo = asDict.TryGetValue((Text) "memo", out IValue memo) ? memo.ToDotnetString() : null; + + CheckMemoLength(Memo); + } + + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("serialized", new Codec().Encode(PlainValue)); + } + + private void CheckMemoLength(string memo) + { + if (memo?.Length > MemoMaxLength) + { + string msg = $"The length of the memo, {memo.Length}, " + + $"is overflowed than the max length, {MemoMaxLength}."; + throw new MemoLengthOverflowException(msg); + } + } + + private IAccount Transfer( + IActionContext context, IAccount state, Address signer, Address recipient, FungibleAssetValue amount, long blockIndex) + { + if (Sender != signer) + { + throw new InvalidTransferSignerException(signer, Sender, recipient); + } + + if (Sender == recipient) + { + throw new InvalidTransferRecipientException(Sender, recipient); + } + + Currency currency = amount.Currency; + if (!(currency.Minters is null) && + (currency.Minters.Contains(Sender) || currency.Minters.Contains(recipient))) + { + throw new InvalidTransferMinterException( + currency.Minters, + Sender, + recipient + ); + } + + TransferAsset3.CheckCrystalSender(currency, blockIndex, Sender); + return state.TransferAsset(context, Sender, recipient, amount); + } + } +} diff --git a/Lib9c/Action/UpdateSell0.cs b/Lib9c/Action/UpdateSell0.cs new file mode 100644 index 0000000000..74ecaa4fa6 --- /dev/null +++ b/Lib9c/Action/UpdateSell0.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Bencodex.Types; +using Lib9c.Abstractions; +using Lib9c.Model.Order; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Model.Item; +using Nekoyume.Model.Mail; +using Nekoyume.Model.State; +using Nekoyume.TableData; +using Serilog; +using static Lib9c.SerializeKeys; +using BxDictionary = Bencodex.Types.Dictionary; +using BxList = Bencodex.Types.List; + +namespace Nekoyume.Action +{ + [Serializable] + [ActionObsolete(ActionObsoleteConfig.V200020AccidentObsoleteIndex)] + [ActionType("update_sell")] + public class UpdateSell0 : GameAction, IUpdateSellV1 + { + public Guid orderId; + public Guid updateSellOrderId; + public Guid tradableId; + public Address sellerAvatarAddress; + public ItemSubType itemSubType; + public FungibleAssetValue price; + public int count; + + Guid IUpdateSellV1.OrderId => orderId; + Guid IUpdateSellV1.UpdateSellOrderId => updateSellOrderId; + Guid IUpdateSellV1.TradableId => tradableId; + Address IUpdateSellV1.SellerAvatarAddress => sellerAvatarAddress; + string IUpdateSellV1.ItemSubType => itemSubType.ToString(); + FungibleAssetValue IUpdateSellV1.Price => price; + int IUpdateSellV1.Count => count; + + protected override IImmutableDictionary PlainValueInternal => + new Dictionary + { + [OrderIdKey] = orderId.Serialize(), + [updateSellOrderIdKey] = updateSellOrderId.Serialize(), + [ItemIdKey] = tradableId.Serialize(), + [SellerAvatarAddressKey] = sellerAvatarAddress.Serialize(), + [ItemSubTypeKey] = itemSubType.Serialize(), + [PriceKey] = price.Serialize(), + [ItemCountKey] = count.Serialize(), + }.ToImmutableDictionary(); + + protected override void LoadPlainValueInternal(IImmutableDictionary plainValue) + { + orderId = plainValue[OrderIdKey].ToGuid(); + updateSellOrderId = plainValue[updateSellOrderIdKey].ToGuid(); + tradableId = plainValue[ItemIdKey].ToGuid(); + sellerAvatarAddress = plainValue[SellerAvatarAddressKey].ToAddress(); + itemSubType = plainValue[ItemSubTypeKey].ToEnum(); + price = plainValue[PriceKey].ToFungibleAssetValue(); + count = plainValue[ItemCountKey].ToInteger(); + } + + public override IAccount Execute(IActionContext context) + { + context.UseGas(1); + var states = context.PreviousState; + var inventoryAddress = sellerAvatarAddress.Derive(LegacyInventoryKey); + var worldInformationAddress = sellerAvatarAddress.Derive(LegacyWorldInformationKey); + var questListAddress = sellerAvatarAddress.Derive(LegacyQuestListKey); + var shopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, orderId); + var updateSellShopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, updateSellOrderId); + var updateSellOrderAddress = Order.DeriveAddress(updateSellOrderId); + var itemAddress = Addresses.GetItemAddress(tradableId); + var orderReceiptAddress = OrderDigestListState.DeriveAddress(sellerAvatarAddress); + + CheckObsolete(ActionObsoleteConfig.V100080ObsoleteIndex, context); + + // common + var addressesHex = GetSignerAndOtherAddressesHex(context, sellerAvatarAddress); + var sw = new Stopwatch(); + sw.Start(); + var started = DateTimeOffset.UtcNow; + Log.Verbose("{AddressesHex} updateSell exec started", addressesHex); + + if (price.Sign < 0) + { + throw new InvalidPriceException( + $"{addressesHex} Aborted as the price is less than zero: {price}."); + } + + if (!states.TryGetAvatarStateV2(context.Signer, sellerAvatarAddress, out var avatarState, out _)) + { + throw new FailedLoadStateException( + $"{addressesHex} Aborted as the avatar state of the signer was failed to load."); + } + sw.Stop(); + + Log.Verbose("{AddressesHex} Sell Get AgentAvatarStates: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + if (!avatarState.worldInformation.IsStageCleared( + GameConfig.RequireClearedStageLevel.ActionsInShop)) + { + avatarState.worldInformation.TryGetLastClearedStageId(out var current); + throw new NotEnoughClearedStageLevelException( + addressesHex, + GameConfig.RequireClearedStageLevel.ActionsInShop, + current); + } + sw.Stop(); + + avatarState.updatedAt = context.BlockIndex; + avatarState.blockIndex = context.BlockIndex; + + // for sell cancel + Log.Verbose("{AddressesHex} UpdateSell IsStageCleared: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + if (!states.TryGetState(shopAddress, out BxDictionary shopStateDict)) + { + throw new FailedLoadStateException($"{addressesHex}failed to load {nameof(ShardedShopStateV2)}({shopAddress})."); + } + sw.Stop(); + + Log.Verbose("{AddressesHex} UpdateSell Sell Cancel Get ShopState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + if (!states.TryGetState(Order.DeriveAddress(orderId), out Dictionary orderDict)) + { + throw new FailedLoadStateException($"{addressesHex} failed to load {nameof(Order)}({Order.DeriveAddress(orderId)})."); + } + + var orderOnSale = OrderFactory.Deserialize(orderDict); + var fromPreviousAction = false; + try + { + orderOnSale.ValidateCancelOrder(avatarState, tradableId); + } + catch (Exception) + { + orderOnSale.ValidateCancelOrder2(avatarState, tradableId); + fromPreviousAction = true; + } + + var itemOnSale = fromPreviousAction + ? orderOnSale.Cancel2(avatarState, context.BlockIndex) + : orderOnSale.Cancel(avatarState, context.BlockIndex); + if (context.BlockIndex < orderOnSale.ExpiredBlockIndex) + { + var shardedShopState = new ShardedShopStateV2(shopStateDict); + shardedShopState.Remove(orderOnSale, context.BlockIndex); + states = states.SetState(shopAddress, shardedShopState.Serialize()); + } + + if (!states.TryGetState(orderReceiptAddress, out Dictionary rawList)) + { + throw new FailedLoadStateException($"{addressesHex} failed to load {nameof(OrderDigest)}({orderReceiptAddress})."); + } + var digestList = new OrderDigestListState(rawList); + digestList.Remove(orderOnSale.OrderId); + states = states.SetState(itemAddress, itemOnSale.Serialize()) + .SetState(orderReceiptAddress, digestList.Serialize()); + sw.Stop(); + + var expirationMail = avatarState.mailBox.OfType() + .FirstOrDefault(m => m.OrderId.Equals(orderId)); + if (!(expirationMail is null)) + { + avatarState.mailBox.Remove(expirationMail); + } + + // for updateSell + var updateSellShopState = states.TryGetState(updateSellShopAddress, out Dictionary serializedState) + ? new ShardedShopStateV2(serializedState) + : new ShardedShopStateV2(updateSellShopAddress); + + Log.Verbose("{AddressesHex} UpdateSell Get ShardedShopState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + var newOrder = OrderFactory.Create(context.Signer, sellerAvatarAddress, updateSellOrderId, price, tradableId, + context.BlockIndex, itemSubType, count); + newOrder.Validate(avatarState, count); + + var tradableItem = newOrder.Sell3(avatarState); + var costumeStatSheet = states.GetSheet(); + var orderDigest = newOrder.Digest(avatarState, costumeStatSheet); + updateSellShopState.Add(orderDigest, context.BlockIndex); + + digestList.Add(orderDigest); + states = states.SetState(orderReceiptAddress, digestList.Serialize()); + states = states.SetState(inventoryAddress, avatarState.inventory.Serialize()) + .SetState(worldInformationAddress, avatarState.worldInformation.Serialize()) + .SetState(questListAddress, avatarState.questList.Serialize()) + .SetState(sellerAvatarAddress, avatarState.SerializeV2()); + sw.Stop(); + + Log.Verbose("{AddressesHex} UpdateSell Set AvatarState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + states = states + .SetState(itemAddress, tradableItem.Serialize()) + .SetState(updateSellOrderAddress, newOrder.Serialize()) + .SetState(updateSellShopAddress, updateSellShopState.Serialize()); + sw.Stop(); + + var ended = DateTimeOffset.UtcNow; + Log.Verbose("{AddressesHex} UpdateSell Set ShopState: {Elapsed}", addressesHex, sw.Elapsed); + Log.Verbose("{AddressesHex} UpdateSell Total Executed Time: {Elapsed}", addressesHex, ended - started); + + return states; + } + } +} diff --git a/Lib9c/Action/UpdateSell2.cs b/Lib9c/Action/UpdateSell2.cs new file mode 100644 index 0000000000..49e45acde4 --- /dev/null +++ b/Lib9c/Action/UpdateSell2.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Bencodex.Types; +using Lib9c.Abstractions; +using Lib9c.Model.Order; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Model.Item; +using Nekoyume.Model.Mail; +using Nekoyume.Model.State; +using Nekoyume.TableData; +using Serilog; +using static Lib9c.SerializeKeys; +using BxDictionary = Bencodex.Types.Dictionary; +using BxList = Bencodex.Types.List; + +namespace Nekoyume.Action +{ + /// + /// Hard forked at https://github.com/planetarium/lib9c/pull/602 + /// Updated at https://github.com/planetarium/lib9c/pull/1022 + /// + [Serializable] + [ActionObsolete(ActionObsoleteConfig.V200020AccidentObsoleteIndex)] + [ActionType("update_sell2")] + public class UpdateSell2 : GameAction, IUpdateSellV1 + { + public Guid orderId; + public Guid updateSellOrderId; + public Guid tradableId; + public Address sellerAvatarAddress; + public ItemSubType itemSubType; + public FungibleAssetValue price; + public int count; + + Guid IUpdateSellV1.OrderId => orderId; + Guid IUpdateSellV1.UpdateSellOrderId => updateSellOrderId; + Guid IUpdateSellV1.TradableId => tradableId; + Address IUpdateSellV1.SellerAvatarAddress => sellerAvatarAddress; + string IUpdateSellV1.ItemSubType => itemSubType.ToString(); + FungibleAssetValue IUpdateSellV1.Price => price; + int IUpdateSellV1.Count => count; + + protected override IImmutableDictionary PlainValueInternal => + new Dictionary + { + [OrderIdKey] = orderId.Serialize(), + [updateSellOrderIdKey] = updateSellOrderId.Serialize(), + [ItemIdKey] = tradableId.Serialize(), + [SellerAvatarAddressKey] = sellerAvatarAddress.Serialize(), + [ItemSubTypeKey] = itemSubType.Serialize(), + [PriceKey] = price.Serialize(), + [ItemCountKey] = count.Serialize(), + }.ToImmutableDictionary(); + + protected override void LoadPlainValueInternal(IImmutableDictionary plainValue) + { + orderId = plainValue[OrderIdKey].ToGuid(); + updateSellOrderId = plainValue[updateSellOrderIdKey].ToGuid(); + tradableId = plainValue[ItemIdKey].ToGuid(); + sellerAvatarAddress = plainValue[SellerAvatarAddressKey].ToAddress(); + itemSubType = plainValue[ItemSubTypeKey].ToEnum(); + price = plainValue[PriceKey].ToFungibleAssetValue(); + count = plainValue[ItemCountKey].ToInteger(); + } + + public override IAccount Execute(IActionContext context) + { + context.UseGas(1); + var states = context.PreviousState; + var inventoryAddress = sellerAvatarAddress.Derive(LegacyInventoryKey); + var worldInformationAddress = sellerAvatarAddress.Derive(LegacyWorldInformationKey); + var questListAddress = sellerAvatarAddress.Derive(LegacyQuestListKey); + var shopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, orderId); + var updateSellShopAddress = ShardedShopStateV2.DeriveAddress(itemSubType, updateSellOrderId); + var updateSellOrderAddress = Order.DeriveAddress(updateSellOrderId); + var itemAddress = Addresses.GetItemAddress(tradableId); + var digestListAddress = OrderDigestListState.DeriveAddress(sellerAvatarAddress); + + CheckObsolete(ActionObsoleteConfig.V100270ObsoleteIndex, context); + + // common + var addressesHex = GetSignerAndOtherAddressesHex(context, sellerAvatarAddress); + var sw = new Stopwatch(); + sw.Start(); + var started = DateTimeOffset.UtcNow; + Log.Verbose("{AddressesHex} updateSell exec started", addressesHex); + + if (price.Sign < 0) + { + throw new InvalidPriceException( + $"{addressesHex} Aborted as the price is less than zero: {price}."); + } + + if (!states.TryGetAvatarStateV2(context.Signer, sellerAvatarAddress, out var avatarState, out _)) + { + throw new FailedLoadStateException( + $"{addressesHex} Aborted as the avatar state of the signer was failed to load."); + } + sw.Stop(); + + Log.Verbose("{AddressesHex} Sell Get AgentAvatarStates: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + if (!avatarState.worldInformation.IsStageCleared( + GameConfig.RequireClearedStageLevel.ActionsInShop)) + { + avatarState.worldInformation.TryGetLastClearedStageId(out var current); + throw new NotEnoughClearedStageLevelException( + addressesHex, + GameConfig.RequireClearedStageLevel.ActionsInShop, + current); + } + sw.Stop(); + + avatarState.updatedAt = context.BlockIndex; + avatarState.blockIndex = context.BlockIndex; + + if (!states.TryGetState(digestListAddress, out Dictionary rawList)) + { + throw new FailedLoadStateException($"{addressesHex} failed to load {nameof(OrderDigest)}({digestListAddress})."); + } + var digestList = new OrderDigestListState(rawList); + + // migration method + avatarState.inventory.UnlockInvalidSlot(digestList, context.Signer, sellerAvatarAddress); + avatarState.inventory.ReconfigureFungibleItem(digestList, tradableId); + avatarState.inventory.LockByReferringToDigestList(digestList, tradableId, context.BlockIndex); + // + + // for sell cancel + Log.Verbose("{AddressesHex} UpdateSell IsStageCleared: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + if (!states.TryGetState(shopAddress, out BxDictionary shopStateDict)) + { + throw new FailedLoadStateException($"{addressesHex}failed to load {nameof(ShardedShopStateV2)}({shopAddress})."); + } + sw.Stop(); + + Log.Verbose("{AddressesHex} UpdateSell Sell Cancel Get ShopState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + if (!states.TryGetState(Order.DeriveAddress(orderId), out Dictionary orderDict)) + { + throw new FailedLoadStateException($"{addressesHex} failed to load {nameof(Order)}({Order.DeriveAddress(orderId)})."); + } + + var orderOnSale = OrderFactory.Deserialize(orderDict); + orderOnSale.ValidateCancelOrder(avatarState, tradableId); + var itemOnSale = orderOnSale.Cancel(avatarState, context.BlockIndex); + if (context.BlockIndex < orderOnSale.ExpiredBlockIndex) + { + var shardedShopState = new ShardedShopStateV2(shopStateDict); + shardedShopState.Remove(orderOnSale, context.BlockIndex); + states = states.SetState(shopAddress, shardedShopState.Serialize()); + } + + digestList.Remove(orderOnSale.OrderId); + states = states.SetState(itemAddress, itemOnSale.Serialize()) + .SetState(digestListAddress, digestList.Serialize()); + sw.Stop(); + + var expirationMail = avatarState.mailBox.OfType() + .FirstOrDefault(m => m.OrderId.Equals(orderId)); + if (!(expirationMail is null)) + { + avatarState.mailBox.Remove(expirationMail); + } + + // for updateSell + var updateSellShopState = states.TryGetState(updateSellShopAddress, out Dictionary serializedState) + ? new ShardedShopStateV2(serializedState) + : new ShardedShopStateV2(updateSellShopAddress); + + Log.Verbose("{AddressesHex} UpdateSell Get ShardedShopState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + var newOrder = OrderFactory.Create(context.Signer, sellerAvatarAddress, updateSellOrderId, price, tradableId, + context.BlockIndex, itemSubType, count); + newOrder.Validate(avatarState, count); + + var tradableItem = newOrder.Sell4(avatarState); + var costumeStatSheet = states.GetSheet(); + var orderDigest = newOrder.Digest(avatarState, costumeStatSheet); + updateSellShopState.Add(orderDigest, context.BlockIndex); + + digestList.Add(orderDigest); + states = states.SetState(digestListAddress, digestList.Serialize()); + states = states.SetState(inventoryAddress, avatarState.inventory.Serialize()) + .SetState(worldInformationAddress, avatarState.worldInformation.Serialize()) + .SetState(questListAddress, avatarState.questList.Serialize()) + .SetState(sellerAvatarAddress, avatarState.SerializeV2()); + sw.Stop(); + + Log.Verbose("{AddressesHex} UpdateSell Set AvatarState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + states = states + .SetState(itemAddress, tradableItem.Serialize()) + .SetState(updateSellOrderAddress, newOrder.Serialize()) + .SetState(updateSellShopAddress, updateSellShopState.Serialize()); + sw.Stop(); + + var ended = DateTimeOffset.UtcNow; + Log.Verbose("{AddressesHex} UpdateSell Set ShopState: {Elapsed}", addressesHex, sw.Elapsed); + Log.Verbose("{AddressesHex} UpdateSell Total Executed Time: {Elapsed}", addressesHex, ended - started); + + return states; + } + } +} diff --git a/Lib9c/Action/UpdateSell3.cs b/Lib9c/Action/UpdateSell3.cs new file mode 100644 index 0000000000..036caa0c55 --- /dev/null +++ b/Lib9c/Action/UpdateSell3.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Bencodex.Types; +using Lib9c.Abstractions; +using Lib9c.Model.Order; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Battle; +using Nekoyume.Model.Mail; +using Nekoyume.Model.State; +using Nekoyume.TableData; +using Serilog; +using static Lib9c.SerializeKeys; +using BxDictionary = Bencodex.Types.Dictionary; +using BxList = Bencodex.Types.List; + +namespace Nekoyume.Action +{ + /// + /// Hard forked at https://github.com/planetarium/lib9c/pull/1022 + /// Updated at https://github.com/planetarium/lib9c/pull/1022 + /// + [Serializable] + [ActionObsolete(ActionObsoleteConfig.V200020AccidentObsoleteIndex)] + [ActionType("update_sell3")] + public class UpdateSell3 : GameAction, IUpdateSellV2 + { + public Address sellerAvatarAddress; + public IEnumerable updateSellInfos; + + Address IUpdateSellV2.SellerAvatarAddress => sellerAvatarAddress; + IEnumerable IUpdateSellV2.UpdateSellInfos => + updateSellInfos.Select(x => x.Serialize()); + + protected override IImmutableDictionary PlainValueInternal => + new Dictionary + { + [SellerAvatarAddressKey] = sellerAvatarAddress.Serialize(), + [UpdateSellInfoKey] = updateSellInfos.Select(info => info.Serialize()).Serialize(), + }.ToImmutableDictionary(); + + protected override void LoadPlainValueInternal( + IImmutableDictionary plainValue) + { + sellerAvatarAddress = plainValue[SellerAvatarAddressKey].ToAddress(); + updateSellInfos = plainValue[UpdateSellInfoKey] + .ToEnumerable(info => new UpdateSellInfo((List)info)); + } + + public override IAccount Execute(IActionContext context) + { + context.UseGas(1); + var states = context.PreviousState; + var inventoryAddress = sellerAvatarAddress.Derive(LegacyInventoryKey); + var worldInformationAddress = sellerAvatarAddress.Derive(LegacyWorldInformationKey); + var questListAddress = sellerAvatarAddress.Derive(LegacyQuestListKey); + var digestListAddress = OrderDigestListState.DeriveAddress(sellerAvatarAddress); + + CheckObsolete(ActionObsoleteConfig.V100320ObsoleteIndex, context); + + // common + var addressesHex = GetSignerAndOtherAddressesHex(context, sellerAvatarAddress); + var sw = new Stopwatch(); + sw.Start(); + var started = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex} updateSell exec started", addressesHex); + + if (!updateSellInfos.Any()) + { + throw new ListEmptyException($"{addressesHex} List - UpdateSell infos was empty."); + } + if (!states.TryGetAvatarStateV2(context.Signer, sellerAvatarAddress, out var avatarState, out _)) + { + throw new FailedLoadStateException( + $"{addressesHex} Aborted as the avatar state of the signer was failed to load."); + } + sw.Stop(); + Log.Verbose("{AddressesHex} Sell Get AgentAvatarStates: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + if (!avatarState.worldInformation.IsStageCleared(GameConfig.RequireClearedStageLevel.ActionsInShop)) + { + avatarState.worldInformation.TryGetLastClearedStageId(out var current); + throw new NotEnoughClearedStageLevelException( + addressesHex, + GameConfig.RequireClearedStageLevel.ActionsInShop, + current); + } + sw.Stop(); + Log.Verbose("{AddressesHex} UpdateSell IsStageCleared: {Elapsed}", addressesHex, sw.Elapsed); + + avatarState.updatedAt = context.BlockIndex; + avatarState.blockIndex = context.BlockIndex; + + var costumeStatSheet = states.GetSheet(); + + if (!states.TryGetState(digestListAddress, out Dictionary rawList)) + { + throw new FailedLoadStateException( + $"{addressesHex} failed to load {nameof(OrderDigest)}({digestListAddress})."); + } + var digestList = new OrderDigestListState(rawList); + + foreach (var updateSellInfo in updateSellInfos) + { + if (updateSellInfo.price.Sign < 0) + { + throw new InvalidPriceException($"{addressesHex} Aborted as the price is less than zero: {updateSellInfo.price}."); + } + + var shopAddress = ShardedShopStateV2.DeriveAddress(updateSellInfo.itemSubType, updateSellInfo.orderId); + var updateSellShopAddress = ShardedShopStateV2.DeriveAddress(updateSellInfo.itemSubType, updateSellInfo.updateSellOrderId); + var updateSellOrderAddress = Order.DeriveAddress(updateSellInfo.updateSellOrderId); + var itemAddress = Addresses.GetItemAddress(updateSellInfo.tradableId); + + // migration method + avatarState.inventory.UnlockInvalidSlot(digestList, context.Signer, sellerAvatarAddress); + avatarState.inventory.ReconfigureFungibleItem(digestList, updateSellInfo.tradableId); + avatarState.inventory.LockByReferringToDigestList(digestList, updateSellInfo.tradableId, + context.BlockIndex); + + // for sell cancel + sw.Restart(); + if (!states.TryGetState(shopAddress, out BxDictionary shopStateDict)) + { + throw new FailedLoadStateException($"{addressesHex}failed to load {nameof(ShardedShopStateV2)}({shopAddress})."); + } + + sw.Stop(); + Log.Verbose("{AddressesHex} UpdateSell Sell Cancel Get ShopState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + if (!states.TryGetState(Order.DeriveAddress(updateSellInfo.orderId), out Dictionary orderDict)) + { + throw new FailedLoadStateException($"{addressesHex} failed to load {nameof(Order)}({Order.DeriveAddress(updateSellInfo.orderId)})."); + } + + var orderOnSale = OrderFactory.Deserialize(orderDict); + orderOnSale.ValidateCancelOrder(avatarState, updateSellInfo.tradableId); + orderOnSale.Cancel(avatarState, context.BlockIndex); + if (context.BlockIndex < orderOnSale.ExpiredBlockIndex) + { + var shardedShopState = new ShardedShopStateV2(shopStateDict); + shardedShopState.Remove(orderOnSale, context.BlockIndex); + states = states.SetState(shopAddress, shardedShopState.Serialize()); + } + + digestList.Remove(orderOnSale.OrderId); + sw.Stop(); + + var expirationMail = avatarState.mailBox.OfType() + .FirstOrDefault(m => m.OrderId.Equals(updateSellInfo.orderId)); + if (!(expirationMail is null)) + { + avatarState.mailBox.Remove(expirationMail); + } + + // for updateSell + var updateSellShopState = + states.TryGetState(updateSellShopAddress, out Dictionary serializedState) + ? new ShardedShopStateV2(serializedState) + : new ShardedShopStateV2(updateSellShopAddress); + + Log.Verbose("{AddressesHex} UpdateSell Get ShardedShopState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + var newOrder = OrderFactory.Create( + context.Signer, + sellerAvatarAddress, + updateSellInfo.updateSellOrderId, + updateSellInfo.price, + updateSellInfo.tradableId, + context.BlockIndex, + updateSellInfo.itemSubType, + updateSellInfo.count + ); + + newOrder.Validate(avatarState, updateSellInfo.count); + + var tradableItem = newOrder.Sell4(avatarState); + var orderDigest = newOrder.Digest(avatarState, costumeStatSheet); + updateSellShopState.Add(orderDigest, context.BlockIndex); + + digestList.Add(orderDigest); + + states = states + .SetState(itemAddress, tradableItem.Serialize()) + .SetState(updateSellOrderAddress, newOrder.Serialize()) + .SetState(updateSellShopAddress, updateSellShopState.Serialize()); + sw.Stop(); + Log.Verbose("{AddressesHex} UpdateSell Set ShopState: {Elapsed}", addressesHex, sw.Elapsed); + } + + sw.Restart(); + states = states.SetState(inventoryAddress, avatarState.inventory.Serialize()) + .SetState(worldInformationAddress, avatarState.worldInformation.Serialize()) + .SetState(questListAddress, avatarState.questList.Serialize()) + .SetState(sellerAvatarAddress, avatarState.SerializeV2()) + .SetState(digestListAddress, digestList.Serialize()); + sw.Stop(); + Log.Verbose("{AddressesHex} UpdateSell Set AvatarState: {Elapsed}", addressesHex, sw.Elapsed); + + var ended = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex} UpdateSell Total Executed Time: {Elapsed}", addressesHex, ended - started); + + return states; + } + } +} diff --git a/Lib9c/Action/UpdateSell4.cs b/Lib9c/Action/UpdateSell4.cs new file mode 100644 index 0000000000..1b6ccb1014 --- /dev/null +++ b/Lib9c/Action/UpdateSell4.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Bencodex.Types; +using Lib9c.Abstractions; +using Lib9c.Model.Order; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Battle; +using Nekoyume.Model.Mail; +using Nekoyume.Model.State; +using Nekoyume.TableData; +using Serilog; +using static Lib9c.SerializeKeys; +using BxDictionary = Bencodex.Types.Dictionary; +using BxList = Bencodex.Types.List; + +namespace Nekoyume.Action +{ + /// + /// Hard forked at https://github.com/planetarium/lib9c/pull/1022 + /// Updated at https://github.com/planetarium/lib9c/pull/1022 + /// + [Serializable] + [ActionType("update_sell4")] + [ActionObsolete(ActionObsoleteConfig.V200020AccidentObsoleteIndex)] + public class UpdateSell4 : GameAction, IUpdateSellV2 + { + private const int UpdateCapacity = 100; + public Address sellerAvatarAddress; + public IEnumerable updateSellInfos; + + Address IUpdateSellV2.SellerAvatarAddress => sellerAvatarAddress; + IEnumerable IUpdateSellV2.UpdateSellInfos => + updateSellInfos.Select(x => x.Serialize()); + + protected override IImmutableDictionary PlainValueInternal => + new Dictionary + { + [SellerAvatarAddressKey] = sellerAvatarAddress.Serialize(), + [UpdateSellInfoKey] = updateSellInfos.Select(info => info.Serialize()).Serialize(), + }.ToImmutableDictionary(); + + protected override void LoadPlainValueInternal( + IImmutableDictionary plainValue) + { + sellerAvatarAddress = plainValue[SellerAvatarAddressKey].ToAddress(); + updateSellInfos = plainValue[UpdateSellInfoKey] + .ToEnumerable(info => new UpdateSellInfo((List)info)); + } + + public override IAccount Execute(IActionContext context) + { + context.UseGas(1); + var states = context.PreviousState; + var inventoryAddress = sellerAvatarAddress.Derive(LegacyInventoryKey); + var worldInformationAddress = sellerAvatarAddress.Derive(LegacyWorldInformationKey); + var questListAddress = sellerAvatarAddress.Derive(LegacyQuestListKey); + var digestListAddress = OrderDigestListState.DeriveAddress(sellerAvatarAddress); + + CheckObsolete(ActionObsoleteConfig.V100351ObsoleteIndex, context); + + if (updateSellInfos.Count() > UpdateCapacity) + { + throw new ArgumentOutOfRangeException($"{nameof(updateSellInfos)} must be less than or equal 100."); + } + // common + var addressesHex = GetSignerAndOtherAddressesHex(context, sellerAvatarAddress); + var sw = new Stopwatch(); + sw.Start(); + var started = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex} updateSell exec started", addressesHex); + + if (!updateSellInfos.Any()) + { + throw new ListEmptyException($"{addressesHex} List - UpdateSell infos was empty."); + } + if (!states.TryGetAvatarStateV2(context.Signer, sellerAvatarAddress, out var avatarState, out _)) + { + throw new FailedLoadStateException( + $"{addressesHex} Aborted as the avatar state of the signer was failed to load."); + } + sw.Stop(); + Log.Verbose("{AddressesHex} Sell Get AgentAvatarStates: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + if (!avatarState.worldInformation.IsStageCleared(GameConfig.RequireClearedStageLevel.ActionsInShop)) + { + avatarState.worldInformation.TryGetLastClearedStageId(out var current); + throw new NotEnoughClearedStageLevelException( + addressesHex, + GameConfig.RequireClearedStageLevel.ActionsInShop, + current); + } + sw.Stop(); + Log.Verbose("{AddressesHex} UpdateSell IsStageCleared: {Elapsed}", addressesHex, sw.Elapsed); + + avatarState.updatedAt = context.BlockIndex; + avatarState.blockIndex = context.BlockIndex; + + var costumeStatSheet = states.GetSheet(); + + if (!states.TryGetState(digestListAddress, out Dictionary rawList)) + { + throw new FailedLoadStateException( + $"{addressesHex} failed to load {nameof(OrderDigest)}({digestListAddress})."); + } + var digestList = new OrderDigestListState(rawList); + + foreach (var updateSellInfo in updateSellInfos) + { + if (updateSellInfo.price.Sign < 0) + { + throw new InvalidPriceException($"{addressesHex} Aborted as the price is less than zero: {updateSellInfo.price}."); + } + + var shopAddress = ShardedShopStateV2.DeriveAddress(updateSellInfo.itemSubType, updateSellInfo.orderId); + var updateSellShopAddress = ShardedShopStateV2.DeriveAddress(updateSellInfo.itemSubType, updateSellInfo.updateSellOrderId); + var updateSellOrderAddress = Order.DeriveAddress(updateSellInfo.updateSellOrderId); + var itemAddress = Addresses.GetItemAddress(updateSellInfo.tradableId); + + // migration method + avatarState.inventory.UnlockInvalidSlot(digestList, context.Signer, sellerAvatarAddress); + avatarState.inventory.ReconfigureFungibleItem(digestList, updateSellInfo.tradableId); + avatarState.inventory.LockByReferringToDigestList(digestList, updateSellInfo.tradableId, + context.BlockIndex); + + // for sell cancel + sw.Restart(); + if (!states.TryGetState(shopAddress, out BxDictionary shopStateDict)) + { + throw new FailedLoadStateException($"{addressesHex}failed to load {nameof(ShardedShopStateV2)}({shopAddress})."); + } + + sw.Stop(); + Log.Verbose("{AddressesHex} UpdateSell Sell Cancel Get ShopState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + if (!states.TryGetState(Order.DeriveAddress(updateSellInfo.orderId), out Dictionary orderDict)) + { + throw new FailedLoadStateException($"{addressesHex} failed to load {nameof(Order)}({Order.DeriveAddress(updateSellInfo.orderId)})."); + } + + var orderOnSale = OrderFactory.Deserialize(orderDict); + orderOnSale.ValidateCancelOrder(avatarState, updateSellInfo.tradableId); + orderOnSale.Cancel(avatarState, context.BlockIndex); + if (context.BlockIndex < orderOnSale.ExpiredBlockIndex) + { + var shardedShopState = new ShardedShopStateV2(shopStateDict); + shardedShopState.Remove(orderOnSale, context.BlockIndex); + states = states.SetState(shopAddress, shardedShopState.Serialize()); + } + + digestList.Remove(orderOnSale.OrderId); + sw.Stop(); + + var expirationMail = avatarState.mailBox.OfType() + .FirstOrDefault(m => m.OrderId.Equals(updateSellInfo.orderId)); + if (!(expirationMail is null)) + { + avatarState.mailBox.Remove(expirationMail); + } + + // for updateSell + var updateSellShopState = + states.TryGetState(updateSellShopAddress, out Dictionary serializedState) + ? new ShardedShopStateV2(serializedState) + : new ShardedShopStateV2(updateSellShopAddress); + + Log.Verbose("{AddressesHex} UpdateSell Get ShardedShopState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + var newOrder = OrderFactory.Create( + context.Signer, + sellerAvatarAddress, + updateSellInfo.updateSellOrderId, + updateSellInfo.price, + updateSellInfo.tradableId, + context.BlockIndex, + updateSellInfo.itemSubType, + updateSellInfo.count + ); + + newOrder.Validate(avatarState, updateSellInfo.count); + + var tradableItem = newOrder.Sell4(avatarState); + var orderDigest = newOrder.Digest(avatarState, costumeStatSheet); + updateSellShopState.Add(orderDigest, context.BlockIndex); + + digestList.Add(orderDigest); + + states = states + .SetState(itemAddress, tradableItem.Serialize()) + .SetState(updateSellOrderAddress, newOrder.Serialize()) + .SetState(updateSellShopAddress, updateSellShopState.Serialize()); + sw.Stop(); + Log.Verbose("{AddressesHex} UpdateSell Set ShopState: {Elapsed}", addressesHex, sw.Elapsed); + } + + sw.Restart(); + states = states.SetState(inventoryAddress, avatarState.inventory.Serialize()) + .SetState(worldInformationAddress, avatarState.worldInformation.Serialize()) + .SetState(questListAddress, avatarState.questList.Serialize()) + .SetState(sellerAvatarAddress, avatarState.SerializeV2()) + .SetState(digestListAddress, digestList.Serialize()); + sw.Stop(); + Log.Verbose("{AddressesHex} UpdateSell Set AvatarState: {Elapsed}", addressesHex, sw.Elapsed); + + var ended = DateTimeOffset.UtcNow; + Log.Debug("{AddressesHex} UpdateSell Total Executed Time: {Elapsed}", addressesHex, ended - started); + + return states; + } + } +}