From c12583bc1634dc6f14fed0892e166dafcde227ec Mon Sep 17 00:00:00 2001 From: Troy Spjute Date: Fri, 19 Aug 2022 17:17:19 -0600 Subject: [PATCH 1/5] Add initial TransferItem Action --- Lib9c/Action/TransferItem.cs | 142 +++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 Lib9c/Action/TransferItem.cs diff --git a/Lib9c/Action/TransferItem.cs b/Lib9c/Action/TransferItem.cs new file mode 100644 index 0000000000..3df3da2bd6 --- /dev/null +++ b/Lib9c/Action/TransferItem.cs @@ -0,0 +1,142 @@ +using Bencodex; +using Bencodex.Types; +using Libplanet; +using Libplanet.Action; +using Libplanet.Assets; +using Nekoyume.Model.State; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Nekoyume.Model; + +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_equipment")] + public class TransferItem : ActionBase, ISerializable + { + private const int MemoMaxLength = 80; + + + public TransferItem() + { + } + + public TransferItem(Address sender, Address recipient, Guid itemId, string memo = null) + { + Sender = sender; + RecipientAvatarAddress = recipient; + ItemId = itemId; + + CheckMemoLength(memo); + Memo = memo; + } + + protected TransferItem(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 RecipientAvatarAddress { get; private set; } + public Guid ItemId; + public string Memo { get; private set; } + + public override IValue PlainValue + { + get + { + IEnumerable> pairs = new[] + { + new KeyValuePair((Text) "sender", Sender.Serialize()), + new KeyValuePair((Text) "recipient", RecipientAvatarAddress.Serialize()), + new KeyValuePair((Text) "itemId", ItemId.Serialize()), + }; + + if (!(Memo is null)) + { + pairs = pairs.Append(new KeyValuePair((Text) "memo", Memo.Serialize())); + } + + return new Dictionary(pairs); + } + } + + public override IAccountStateDelta Execute(IActionContext context) + { + var state = context.PreviousStates; + if (context.Rehearsal) + { + return state; + } + + if (Sender != context.Signer) + { + throw new InvalidTransferSignerException(context.Signer, Sender, RecipientAvatarAddress); + } + + Address recipientAddress = RecipientAvatarAddress.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(RecipientAvatarAddress)) + { + throw new InvalidTransferUnactivatedRecipientException(Sender, RecipientAvatarAddress); + } + } + + //Currency currency = Amount.Currency; + //if (!(currency.Minters is null) && + // (currency.Minters.Contains(Sender) || currency.Minters.Contains(RecipientAvatarAddress))) + //{ + // throw new InvalidTransferMinterException( + // currency.Minters, + // Sender, + // RecipientAvatarAddress + // ); + //} + + return state;//.TransferAsset(Sender, RecipientAvatarAddress, Amount); + } + + public override void LoadPlainValue(IValue plainValue) + { + var asDict = (Dictionary) plainValue; + + Sender = asDict["sender"].ToAddress(); + RecipientAvatarAddress = asDict["recipient"].ToAddress(); + ItemId = asDict["itemid"].ToGuid(); + 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); + } + } + } +} From db45196e91edef3d4f97c784a5b7c80713c4eb3c Mon Sep 17 00:00:00 2001 From: Troy Spjute Date: Wed, 31 Aug 2022 00:14:34 -0600 Subject: [PATCH 2/5] item checks --- Lib9c/Action/TransferItem.cs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Lib9c/Action/TransferItem.cs b/Lib9c/Action/TransferItem.cs index 3df3da2bd6..5218321059 100644 --- a/Lib9c/Action/TransferItem.cs +++ b/Lib9c/Action/TransferItem.cs @@ -72,7 +72,7 @@ public override IValue PlainValue public override IAccountStateDelta Execute(IActionContext context) { - var state = context.PreviousStates; + var states = context.PreviousStates; if (context.Rehearsal) { return state; @@ -86,7 +86,7 @@ public override IAccountStateDelta Execute(IActionContext context) Address recipientAddress = RecipientAvatarAddress.Derive(ActivationKey.DeriveKey); // Check new type of activation first. - if (state.GetState(recipientAddress) is null && state.GetState(Addresses.ActivatedAccount) is Dictionary asDict ) + if (states.GetState(recipientAddress) is null && states.GetState(Addresses.ActivatedAccount) is Dictionary asDict ) { var activatedAccountsState = new ActivatedAccountsState(asDict); var activatedAccounts = activatedAccountsState.Accounts; @@ -97,6 +97,23 @@ public override IAccountStateDelta Execute(IActionContext context) throw new InvalidTransferUnactivatedRecipientException(Sender, RecipientAvatarAddress); } } + var recipientInventoryAddress = RecipientAvatarAddress.Derive(LegacyInventoryKey); + var recipientWorldInformationAddress = RecipientAvatarAddress.Derive(LegacyWorldInformationKey); + var recipientQuestListAddress = RecipientAvatarAddress.Derive(LegacyQuestListKey); + var addressesHex = GetSignerAndOtherAddressesHex(context, RecipientAvatarAddress); + + if (!states.TryGetAvatarStateV2(ctx.Signer, RecipientAvatarAddress, out var recipientAvatarState, out _)) + { + throw new FailedLoadStateException( + $"{addressesHex}Aborted as the avatar state of the buyer was failed to load."); + } + + if (!recipientAvatarState.worldInformation.IsStageCleared(GameConfig.RequireClearedStageLevel.ActionsInShop)) + { + recipientAvatarState.worldInformation.TryGetLastClearedStageId(out var current); + throw new NotEnoughClearedStageLevelException(addressesHex, + GameConfig.RequireClearedStageLevel.ActionsInShop, current); + } //Currency currency = Amount.Currency; //if (!(currency.Minters is null) && From 56bfdf9d5a4e122556de9699687782e1c820e36c Mon Sep 17 00:00:00 2001 From: Troy Spjute Date: Mon, 5 Sep 2022 20:37:37 -0600 Subject: [PATCH 3/5] Add TranferItem action --- Lib9c/Action/TransferItem.cs | 108 +++++++++++++++++++++++++++------- Lib9c/Model/Item/Inventory.cs | 2 +- 2 files changed, 88 insertions(+), 22 deletions(-) diff --git a/Lib9c/Action/TransferItem.cs b/Lib9c/Action/TransferItem.cs index 5218321059..280025e664 100644 --- a/Lib9c/Action/TransferItem.cs +++ b/Lib9c/Action/TransferItem.cs @@ -9,6 +9,9 @@ using System.Linq; using System.Runtime.Serialization; using Nekoyume.Model; +using static Lib9c.SerializeKeys; +using Nekoyume.Model.Item; +using Nekoyume.TableData; namespace Nekoyume.Action { @@ -21,6 +24,7 @@ namespace Nekoyume.Action public class TransferItem : ActionBase, ISerializable { private const int MemoMaxLength = 80; + private const int transferFee = 10; public TransferItem() @@ -29,7 +33,7 @@ public TransferItem() public TransferItem(Address sender, Address recipient, Guid itemId, string memo = null) { - Sender = sender; + SenderAvatarAddress = sender; RecipientAvatarAddress = recipient; ItemId = itemId; @@ -45,7 +49,7 @@ protected TransferItem(SerializationInfo info, StreamingContext context) LoadPlainValue(pv); } - public Address Sender { get; private set; } + public Address SenderAvatarAddress { get; private set; } public Address RecipientAvatarAddress { get; private set; } public Guid ItemId; public string Memo { get; private set; } @@ -56,7 +60,7 @@ public override IValue PlainValue { IEnumerable> pairs = new[] { - new KeyValuePair((Text) "sender", Sender.Serialize()), + new KeyValuePair((Text) "sender", SenderAvatarAddress.Serialize()), new KeyValuePair((Text) "recipient", RecipientAvatarAddress.Serialize()), new KeyValuePair((Text) "itemId", ItemId.Serialize()), }; @@ -75,12 +79,12 @@ public override IAccountStateDelta Execute(IActionContext context) var states = context.PreviousStates; if (context.Rehearsal) { - return state; + return states; } - if (Sender != context.Signer) + if (SenderAvatarAddress != context.Signer) { - throw new InvalidTransferSignerException(context.Signer, Sender, RecipientAvatarAddress); + throw new InvalidTransferSignerException(context.Signer, SenderAvatarAddress, RecipientAvatarAddress); } Address recipientAddress = RecipientAvatarAddress.Derive(ActivationKey.DeriveKey); @@ -94,18 +98,44 @@ public override IAccountStateDelta Execute(IActionContext context) if (activatedAccounts.Count != 0 && !activatedAccounts.Contains(RecipientAvatarAddress)) { - throw new InvalidTransferUnactivatedRecipientException(Sender, RecipientAvatarAddress); + throw new InvalidTransferUnactivatedRecipientException(SenderAvatarAddress, RecipientAvatarAddress); } } var recipientInventoryAddress = RecipientAvatarAddress.Derive(LegacyInventoryKey); var recipientWorldInformationAddress = RecipientAvatarAddress.Derive(LegacyWorldInformationKey); var recipientQuestListAddress = RecipientAvatarAddress.Derive(LegacyQuestListKey); + var senderInventoryAddress = SenderAvatarAddress.Derive(LegacyInventoryKey); var addressesHex = GetSignerAndOtherAddressesHex(context, RecipientAvatarAddress); + AvatarState senderAvatarState; + try + { + senderAvatarState = states.GetAvatarStateV2(SenderAvatarAddress); + } + // BackWard compatible. + catch (FailedLoadStateException) + { + senderAvatarState = states.GetAvatarState(SenderAvatarAddress); + } + if (senderAvatarState is null) + { + throw new FailedLoadStateException( + $"Aborted as the avatar state of the sender ({senderAvatarState}) was failed to load."); + } - if (!states.TryGetAvatarStateV2(ctx.Signer, RecipientAvatarAddress, out var recipientAvatarState, out _)) + AvatarState recipientAvatarState; + try + { + recipientAvatarState = states.GetAvatarStateV2(RecipientAvatarAddress); + } + // BackWard compatible. + catch (FailedLoadStateException) + { + recipientAvatarState = states.GetAvatarState(RecipientAvatarAddress); + } + if (recipientAvatarState is null) { throw new FailedLoadStateException( - $"{addressesHex}Aborted as the avatar state of the buyer was failed to load."); + $"Aborted as the avatar state of the sender ({recipientAvatarState}) was failed to load."); } if (!recipientAvatarState.worldInformation.IsStageCleared(GameConfig.RequireClearedStageLevel.ActionsInShop)) @@ -115,25 +145,61 @@ public override IAccountStateDelta Execute(IActionContext context) GameConfig.RequireClearedStageLevel.ActionsInShop, current); } - //Currency currency = Amount.Currency; - //if (!(currency.Minters is null) && - // (currency.Minters.Contains(Sender) || currency.Minters.Contains(RecipientAvatarAddress))) - //{ - // throw new InvalidTransferMinterException( - // currency.Minters, - // Sender, - // RecipientAvatarAddress - // ); - //} + if (!senderAvatarState.worldInformation.IsStageCleared(GameConfig.RequireClearedStageLevel.ActionsInShop)) + { + senderAvatarState.worldInformation.TryGetLastClearedStageId(out var current); + throw new NotEnoughClearedStageLevelException(addressesHex, + GameConfig.RequireClearedStageLevel.ActionsInShop, current); + } + + if(senderAvatarState.agentAddress != context.Signer) + { + throw new InvalidAddressException("Signer doesn't match sending agent address"); + } - return state;//.TransferAsset(Sender, RecipientAvatarAddress, Amount); + if(!senderAvatarState.inventory.TryGetTradableItem(ItemId,context.BlockIndex, 1, out var item)) + { + throw new InvalidAddressException("Unable to get item from inventory"); + } + if (item.Locked) + { + throw new Exception("Item is current locked, unable to send while on the market"); + } + if(item is INonFungibleItem nonFungibleItem) + { + nonFungibleItem.RequiredBlockIndex = context.BlockIndex; + senderAvatarState.inventory.RemoveNonFungibleItem(nonFungibleItem); + if (nonFungibleItem is Costume costume) + { + recipientAvatarState.UpdateFromAddCostume(costume, false); + } + else + { + recipientAvatarState.UpdateFromAddItem((ItemUsable)nonFungibleItem, false); + } + //Transfer fee + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + var feeStoreAddress = Addresses.GetShopFeeAddress(arenaData.ChampionshipId, arenaData.Round); + var goldCurrency = states.GetGoldCurrency(); + var fee = transferFee * goldCurrency; + states = states.TransferAsset(context.Signer, feeStoreAddress, fee); + } + else + { + throw new InvalidItemTypeException("Unable to load item to send"); + } + states = states + .SetState(recipientInventoryAddress, recipientAvatarState.inventory.Serialize()) + .SetState(senderInventoryAddress, senderAvatarState.inventory.Serialize()); + return states;//.TransferAsset(Sender, RecipientAvatarAddress, Amount); } public override void LoadPlainValue(IValue plainValue) { var asDict = (Dictionary) plainValue; - Sender = asDict["sender"].ToAddress(); + SenderAvatarAddress = asDict["sender"].ToAddress(); RecipientAvatarAddress = asDict["recipient"].ToAddress(); ItemId = asDict["itemid"].ToGuid(); Memo = asDict.TryGetValue((Text) "memo", out IValue memo) ? memo.ToDotnetString() : null; diff --git a/Lib9c/Model/Item/Inventory.cs b/Lib9c/Model/Item/Inventory.cs index 0119eb3821..366bdf409e 100644 --- a/Lib9c/Model/Item/Inventory.cs +++ b/Lib9c/Model/Item/Inventory.cs @@ -589,7 +589,7 @@ public bool TryGetTradableItem(Guid tradeId, long blockIndex, int count, out Ite outItem = _items.FirstOrDefault(i => i.item is ITradableItem item && item.TradableId.Equals(tradeId) && - item.RequiredBlockIndex == blockIndex && + item.RequiredBlockIndex <= blockIndex && i.count >= count ); return !(outItem is null); From 63d8a8f09b2ba8070937cc464a0b099bb7fc69de Mon Sep 17 00:00:00 2001 From: Troy Spjute Date: Mon, 12 Sep 2022 11:28:19 -0600 Subject: [PATCH 4/5] Add TransferItem tests, fix issues, use CP to calculate fee on equipment --- .Lib9c.Tests/Action/TransferItemTest.cs | 373 ++++++++++++++++++++++++ Lib9c/Action/TransferItem.cs | 110 ++++--- 2 files changed, 444 insertions(+), 39 deletions(-) create mode 100644 .Lib9c.Tests/Action/TransferItemTest.cs diff --git a/.Lib9c.Tests/Action/TransferItemTest.cs b/.Lib9c.Tests/Action/TransferItemTest.cs new file mode 100644 index 0000000000..c9c0a4d013 --- /dev/null +++ b/.Lib9c.Tests/Action/TransferItemTest.cs @@ -0,0 +1,373 @@ +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; + using Libplanet.Action; + using Libplanet.Assets; + using Libplanet.Crypto; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Serilog; + using Xunit; + using Xunit.Abstractions; + using static Lib9c.SerializeKeys; + + public class TransferItemTest + { + private const long ProductPrice = 100; + + private readonly Address _agentAddress; + private readonly Address _agent2Address; + private readonly Address _avatarAddress; + private readonly Address _avatar2Address; + private readonly Currency _currency; + private readonly AvatarState _avatarState; + private readonly AvatarState _avatar2State; + private readonly TableSheets _tableSheets; + private readonly GoldCurrencyState _goldCurrencyState; + private IAccountStateDelta _initialState; + + public TransferItemTest(ITestOutputHelper outputHelper) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(outputHelper) + .CreateLogger(); + + _initialState = new State(); + var sheets = TableSheetsImporter.ImportSheets(); + foreach (var (key, value) in sheets) + { + _initialState = _initialState + .SetState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _tableSheets = new TableSheets(sheets); + + _currency = new Currency("NCG", 2, minters: null); + _goldCurrencyState = new GoldCurrencyState(_currency); + + var shopState = new ShopState(); + + _agentAddress = new PrivateKey().ToAddress(); + var agentState = new AgentState(_agentAddress); + _avatarAddress = new PrivateKey().ToAddress(); + var rankingMapAddress = new PrivateKey().ToAddress(); + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + rankingMapAddress) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.ActionsInShop), + }; + agentState.avatarAddresses[0] = _avatarAddress; + + _agent2Address = new PrivateKey().ToAddress(); + var agent2State = new AgentState(_agent2Address); + _avatar2Address = new PrivateKey().ToAddress(); + var rankingMap2Address = new PrivateKey().ToAddress(); + _avatar2State = new AvatarState( + _avatar2Address, + _agent2Address, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + rankingMapAddress) + { + worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.ActionsInShop), + }; + agent2State.avatarAddresses[0] = _avatar2Address; + + _initialState = _initialState + .SetState(GoldCurrencyState.Address, _goldCurrencyState.Serialize()) + .SetState(Addresses.Shop, shopState.Serialize()) + .SetState(_agentAddress, agentState.Serialize()) + .SetState(_avatarAddress, _avatarState.Serialize()) + .SetState(_agent2Address, agent2State.Serialize()) + .SetState(_avatar2Address, _avatar2State.Serialize()) + .MintAsset(_agentAddress, _goldCurrencyState.Currency * 10000); + } + + [Theory] + [InlineData(ItemType.Consumable, 1, true)] + [InlineData(ItemType.Costume, 1, false)] + [InlineData(ItemType.Equipment, 1, true)] + [InlineData(ItemType.Material, 1, false)] + public void Execute( + ItemType itemType, + int itemCount, + bool backward + ) + { + var avatarState = _initialState.GetAvatarState(_avatarAddress); + var avatar2State = _initialState.GetAvatarState(_avatar2Address); + + ITradableItem tradableItem; + switch (itemType) + { + case ItemType.Consumable: + tradableItem = ItemFactory.CreateItemUsable( + _tableSheets.ConsumableItemSheet.First, + Guid.NewGuid(), + 0); + break; + case ItemType.Costume: + tradableItem = ItemFactory.CreateCostume( + _tableSheets.CostumeItemSheet.First, + Guid.NewGuid()); + break; + case ItemType.Equipment: + tradableItem = ItemFactory.CreateItemUsable( + _tableSheets.EquipmentItemSheet.First, + Guid.NewGuid(), + 0); + break; + case ItemType.Material: + var tradableMaterialRow = _tableSheets.MaterialItemSheet.OrderedList + .First(row => row.ItemSubType == ItemSubType.Hourglass); + tradableItem = ItemFactory.CreateTradableMaterial(tradableMaterialRow); + break; + default: + throw new ArgumentOutOfRangeException(nameof(itemType), itemType, null); + } + + Assert.Equal(0, tradableItem.RequiredBlockIndex); + avatarState.inventory.AddItem((ItemBase)tradableItem, itemCount); + + var previousStates = _initialState; + if (backward) + { + previousStates = previousStates.SetState(_avatarAddress, avatarState.Serialize()) + .SetState(_avatar2Address, avatar2State.Serialize()); + } + else + { + previousStates = previousStates + .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()) + .SetState(_avatar2Address.Derive(LegacyInventoryKey), avatar2State.inventory.Serialize()) + .SetState(_avatar2Address.Derive(LegacyWorldInformationKey), avatar2State.worldInformation.Serialize()) + .SetState(_avatar2Address.Derive(LegacyQuestListKey), avatar2State.questList.Serialize()) + .SetState(_avatar2Address, avatar2State.SerializeV2()); + } + + var currencyState = previousStates.GetGoldCurrency(); + var price = new FungibleAssetValue(currencyState, ProductPrice, 0); + var orderId = new Guid("6f460c1a755d48e4ad6765d5f519dbc8"); + var orderAddress = Order.DeriveAddress(orderId); + var shardedShopAddress = ShardedShopStateV2.DeriveAddress( + tradableItem.ItemSubType, + orderId); + long blockIndex = 1; + Assert.Null(previousStates.GetState(shardedShopAddress)); + + var transferItemAction = new TransferItem + { + SenderAvatarAddress = _avatarAddress, + ItemId = tradableItem.TradableId, + RecipientAvatarAddress = _avatar2Address, + }; + var nextState = transferItemAction.Execute( + new ActionContext + { + BlockIndex = blockIndex, + PreviousStates = previousStates, + Rehearsal = false, + Signer = _agentAddress, + Random = new TestRandom(), + }); + + var nextAvatarState = nextState.GetAvatarStateV2(_avatarAddress); + var nextAvatar2State = nextState.GetAvatarStateV2(_avatar2Address); + Assert.Single(nextAvatar2State.inventory.Items); + + Assert.Empty(nextAvatarState.inventory.Items); + //Assert.True(nextAvatarState.inventory.TryGetLockedItem(new OrderLock(orderId), out var inventoryItem)); + Assert.False(nextAvatarState.inventory.TryGetTradableItems(tradableItem.TradableId, blockIndex, itemCount, out _)); + Assert.False(nextAvatarState.inventory.TryGetTradableItems(tradableItem.TradableId, blockIndex, itemCount, out _)); + Assert.True(nextAvatar2State.inventory.TryGetTradableItems(tradableItem.TradableId, blockIndex, itemCount, out _)); + Assert.True(nextAvatar2State.inventory.TryGetTradableItems(tradableItem.TradableId, blockIndex, itemCount, out _)); + //ITradableItem nextTradableItem = (ITradableItem)inventoryItem.item; + //Assert.Equal(expiredBlockIndex, nextTradableItem.RequiredBlockIndex); + + // Check ShardedShopState + //var nextSerializedShardedShopState = nextState.GetState(shardedShopAddress); + //Assert.NotNull(nextSerializedShardedShopState); + //var nextShardedShopState = + // new ShardedShopStateV2((Dictionary)nextSerializedShardedShopState); + //Assert.Single(nextShardedShopState.OrderDigestList); + //var orderDigest = nextShardedShopState.OrderDigestList.First(o => o.OrderId.Equals(orderId)); + //Assert.Equal(price, orderDigest.Price); + //Assert.Equal(blockIndex, orderDigest.StartedBlockIndex); + //Assert.Equal(expiredBlockIndex, orderDigest.ExpiredBlockIndex); + //Assert.Equal(((ItemBase)tradableItem).Id, orderDigest.ItemId); + //Assert.Equal(tradableItem.TradableId, orderDigest.TradableId); + + //var serializedOrder = nextState.GetState(orderAddress); + //Assert.NotNull(serializedOrder); + //var serializedItem = nextState.GetState(Addresses.GetItemAddress(tradableItem.TradableId)); + //Assert.NotNull(serializedItem); + + //var order = OrderFactory.Deserialize((Dictionary)serializedOrder); + //ITradableItem orderItem = (ITradableItem)ItemFactory.Deserialize((Dictionary)serializedItem); + + //Assert.Equal(price, order.Price); + //Assert.Equal(orderId, order.OrderId); + //Assert.Equal(tradableItem.TradableId, order.TradableId); + //Assert.Equal(blockIndex, order.StartedBlockIndex); + //Assert.Equal(expiredBlockIndex, order.ExpiredBlockIndex); + //Assert.Equal(_agentAddress, order.SellerAgentAddress); + //Assert.Equal(_avatarAddress, order.SellerAvatarAddress); + //Assert.Equal(expiredBlockIndex, orderItem.RequiredBlockIndex); + + //var receiptDict = nextState.GetState(OrderDigestListState.DeriveAddress(_avatarAddress)); + //Assert.NotNull(receiptDict); + //var orderDigestList = new OrderDigestListState((Dictionary)receiptDict); + //Assert.Single(orderDigestList.OrderDigestList); + //OrderDigest orderDigest2 = orderDigestList.OrderDigestList.First(); + //Assert.Equal(orderDigest, orderDigest2); + } + + [Fact] + public void Execute_Throw_FailedLoadStateException() + { + var action = new TransferItem + { + SenderAvatarAddress = _avatarAddress, + ItemId = default, + RecipientAvatarAddress = _avatar2Address, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousStates = new State(), + 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 TransferItem + { + SenderAvatarAddress = _avatarAddress, + ItemId = default, + RecipientAvatarAddress = _avatar2Address, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousStates = _initialState, + Signer = _agentAddress, + })); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Execute_Throw_ItemDoesNotExistException(bool isLock) + { + var tradableId = Guid.NewGuid(); + if (isLock) + { + var tradableItem = ItemFactory.CreateItemUsable( + _tableSheets.EquipmentItemSheet.First, + tradableId, + 0); + var orderLock = new OrderLock(Guid.NewGuid()); + _avatarState.inventory.AddItem(tradableItem, 1, orderLock); + Assert.True(_avatarState.inventory.TryGetLockedItem(orderLock, out _)); + _initialState = _initialState.SetState( + _avatarAddress.Derive(LegacyInventoryKey), + _avatarState.inventory.Serialize() + ); + } + + var action = new TransferItem + { + SenderAvatarAddress = _avatarAddress, + ItemId = tradableId, + RecipientAvatarAddress = _avatar2Address, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + BlockIndex = 0, + PreviousStates = _initialState, + Signer = _agentAddress, + Random = new TestRandom(), + })); + } + + [Fact] + public void Rehearsal() + { + Guid tradableId = Guid.NewGuid(); + Guid orderId = Guid.NewGuid(); + var action = new TransferItem + { + SenderAvatarAddress = _avatarAddress, + ItemId = tradableId, + ItemCount = 1, + RecipientAvatarAddress = _avatar2Address, + }; + + var updatedAddresses = new List
() + { + _agentAddress, + _avatarAddress.Derive(LegacyInventoryKey), + _avatarAddress.Derive(LegacyWorldInformationKey), + _avatarAddress.Derive(LegacyQuestListKey), + Addresses.GetItemAddress(tradableId), + _avatar2Address.Derive(LegacyInventoryKey), + _avatar2Address.Derive(LegacyWorldInformationKey), + _avatar2Address.Derive(LegacyQuestListKey), + }; + + var state = new State(); + + var nextState = action.Execute(new ActionContext() + { + PreviousStates = state, + Signer = _agentAddress, + BlockIndex = 0, + Rehearsal = true, + }); + + Assert.Equal(updatedAddresses.ToImmutableHashSet(), nextState.UpdatedAddresses); + } + } +} diff --git a/Lib9c/Action/TransferItem.cs b/Lib9c/Action/TransferItem.cs index 280025e664..ba7e09fb0e 100644 --- a/Lib9c/Action/TransferItem.cs +++ b/Lib9c/Action/TransferItem.cs @@ -12,6 +12,7 @@ using static Lib9c.SerializeKeys; using Nekoyume.Model.Item; using Nekoyume.TableData; +using Nekoyume.Battle; namespace Nekoyume.Action { @@ -31,12 +32,12 @@ public TransferItem() { } - public TransferItem(Address sender, Address recipient, Guid itemId, string memo = null) + public TransferItem(Address sender, Address recipient, Guid itemId, int itemCount = 1, string memo = null) { SenderAvatarAddress = sender; RecipientAvatarAddress = recipient; ItemId = itemId; - + ItemCount = itemCount; CheckMemoLength(memo); Memo = memo; } @@ -49,9 +50,10 @@ protected TransferItem(SerializationInfo info, StreamingContext context) LoadPlainValue(pv); } - public Address SenderAvatarAddress { get; private set; } - public Address RecipientAvatarAddress { get; private set; } - public Guid ItemId; + public Address SenderAvatarAddress { get; set; } + public Address RecipientAvatarAddress { get; set; } + public Guid ItemId { get; set; } + public int ItemCount { get; set; } public string Memo { get; private set; } public override IValue PlainValue @@ -63,6 +65,7 @@ public override IValue PlainValue new KeyValuePair((Text) "sender", SenderAvatarAddress.Serialize()), new KeyValuePair((Text) "recipient", RecipientAvatarAddress.Serialize()), new KeyValuePair((Text) "itemId", ItemId.Serialize()), + new KeyValuePair((Text) "itemCount", ItemCount.Serialize()), }; if (!(Memo is null)) @@ -77,16 +80,26 @@ public override IValue PlainValue public override IAccountStateDelta Execute(IActionContext context) { var states = context.PreviousStates; + var recipientInventoryAddress = RecipientAvatarAddress.Derive(LegacyInventoryKey); + var recipientWorldInformationAddress = RecipientAvatarAddress.Derive(LegacyWorldInformationKey); + var recipientQuestListAddress = RecipientAvatarAddress.Derive(LegacyQuestListKey); + var senderInventoryAddress = SenderAvatarAddress.Derive(LegacyInventoryKey); + var senderWorldInformationAddress = SenderAvatarAddress.Derive(LegacyWorldInformationKey); + var senderQuestListAddress = SenderAvatarAddress.Derive(LegacyQuestListKey); if (context.Rehearsal) { - return states; - } - - if (SenderAvatarAddress != context.Signer) - { - throw new InvalidTransferSignerException(context.Signer, SenderAvatarAddress, RecipientAvatarAddress); + return states + .SetState(Addresses.GetItemAddress(ItemId), MarkChanged) + .SetState(recipientInventoryAddress, MarkChanged) + .SetState(recipientQuestListAddress, MarkChanged) + .SetState(recipientWorldInformationAddress, MarkChanged) + .SetState(senderInventoryAddress, MarkChanged) + .SetState(senderWorldInformationAddress, MarkChanged) + .SetState(senderQuestListAddress, MarkChanged) + .MarkBalanceChanged(GoldCurrencyMock, context.Signer); } + int count = ItemCount; Address recipientAddress = RecipientAvatarAddress.Derive(ActivationKey.DeriveKey); // Check new type of activation first. @@ -101,27 +114,16 @@ public override IAccountStateDelta Execute(IActionContext context) throw new InvalidTransferUnactivatedRecipientException(SenderAvatarAddress, RecipientAvatarAddress); } } - var recipientInventoryAddress = RecipientAvatarAddress.Derive(LegacyInventoryKey); - var recipientWorldInformationAddress = RecipientAvatarAddress.Derive(LegacyWorldInformationKey); - var recipientQuestListAddress = RecipientAvatarAddress.Derive(LegacyQuestListKey); - var senderInventoryAddress = SenderAvatarAddress.Derive(LegacyInventoryKey); + var addressesHex = GetSignerAndOtherAddressesHex(context, RecipientAvatarAddress); AvatarState senderAvatarState; - try - { - senderAvatarState = states.GetAvatarStateV2(SenderAvatarAddress); - } - // BackWard compatible. - catch (FailedLoadStateException) - { - senderAvatarState = states.GetAvatarState(SenderAvatarAddress); - } - if (senderAvatarState is null) + + if (!states.TryGetAvatarStateV2(context.Signer, SenderAvatarAddress, out senderAvatarState, out var senderMigrationRequired)) { throw new FailedLoadStateException( $"Aborted as the avatar state of the sender ({senderAvatarState}) was failed to load."); } - + var recipientMigrationRequired = false; AvatarState recipientAvatarState; try { @@ -131,6 +133,7 @@ public override IAccountStateDelta Execute(IActionContext context) catch (FailedLoadStateException) { recipientAvatarState = states.GetAvatarState(RecipientAvatarAddress); + recipientMigrationRequired = true; } if (recipientAvatarState is null) { @@ -157,15 +160,17 @@ public override IAccountStateDelta Execute(IActionContext context) throw new InvalidAddressException("Signer doesn't match sending agent address"); } - if(!senderAvatarState.inventory.TryGetTradableItem(ItemId,context.BlockIndex, 1, out var item)) + if(!senderAvatarState.inventory.TryGetTradableItem(ItemId,context.BlockIndex, count, out var item)) { - throw new InvalidAddressException("Unable to get item from inventory"); + throw new ItemDoesNotExistException("Unable to get item from inventory"); } if (item.Locked) { - throw new Exception("Item is current locked, unable to send while on the market"); + throw new ItemDoesNotExistException("Item is current locked, unable to send while on the market"); } - if(item is INonFungibleItem nonFungibleItem) + + int baseFee = 1000; + if(item.item is INonFungibleItem nonFungibleItem) { nonFungibleItem.RequiredBlockIndex = context.BlockIndex; senderAvatarState.inventory.RemoveNonFungibleItem(nonFungibleItem); @@ -176,22 +181,49 @@ public override IAccountStateDelta Execute(IActionContext context) else { recipientAvatarState.UpdateFromAddItem((ItemUsable)nonFungibleItem, false); + baseFee = CPHelper.GetCP((ItemUsable)nonFungibleItem)/1000; + if (baseFee == 0) baseFee = 1; } - //Transfer fee - var arenaSheet = states.GetSheet(); - var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = Addresses.GetShopFeeAddress(arenaData.ChampionshipId, arenaData.Round); - var goldCurrency = states.GetGoldCurrency(); - var fee = transferFee * goldCurrency; - states = states.TransferAsset(context.Signer, feeStoreAddress, fee); + + } + else if(item.item is ITradableFungibleItem tradable) + { + tradable.RequiredBlockIndex = context.BlockIndex; + senderAvatarState.inventory.RemoveTradableItem(tradable, count); + recipientAvatarState.UpdateFromAddItem(item.item, count, false); + baseFee = 100; } else { throw new InvalidItemTypeException("Unable to load item to send"); } + + //Transfer fee + var arenaSheet = states.GetSheet(); + var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); + var feeStoreAddress = Addresses.GetShopFeeAddress(arenaData.ChampionshipId, arenaData.Round); + var goldCurrency = states.GetGoldCurrency(); + + var fee = baseFee * goldCurrency; + states = states.TransferAsset(context.Signer, feeStoreAddress, fee); + + if (senderMigrationRequired) + { + states = states + .SetState(senderWorldInformationAddress, senderAvatarState.worldInformation.Serialize()) + .SetState(senderQuestListAddress, senderAvatarState.questList.Serialize()); + } + + if (recipientMigrationRequired) + { + states = states + .SetState(recipientWorldInformationAddress, recipientAvatarState.worldInformation.Serialize()) + .SetState(recipientQuestListAddress, recipientAvatarState.questList.Serialize()); + } + states = states - .SetState(recipientInventoryAddress, recipientAvatarState.inventory.Serialize()) - .SetState(senderInventoryAddress, senderAvatarState.inventory.Serialize()); + .SetState(senderInventoryAddress, senderAvatarState.inventory.Serialize()) + .SetState(recipientInventoryAddress, recipientAvatarState.inventory.Serialize()); return states;//.TransferAsset(Sender, RecipientAvatarAddress, Amount); } From 6d98085e71068e9b5af7a26a630de70ec9a8f658 Mon Sep 17 00:00:00 2001 From: Troy Spjute Date: Mon, 12 Sep 2022 11:58:30 -0600 Subject: [PATCH 5/5] ncg is already diviced by 100, so CP should be divided only by 10 at first --- Lib9c/Action/TransferItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib9c/Action/TransferItem.cs b/Lib9c/Action/TransferItem.cs index ba7e09fb0e..88fbb98987 100644 --- a/Lib9c/Action/TransferItem.cs +++ b/Lib9c/Action/TransferItem.cs @@ -181,7 +181,7 @@ public override IAccountStateDelta Execute(IActionContext context) else { recipientAvatarState.UpdateFromAddItem((ItemUsable)nonFungibleItem, false); - baseFee = CPHelper.GetCP((ItemUsable)nonFungibleItem)/1000; + baseFee = CPHelper.GetCP((ItemUsable)nonFungibleItem)/10; if (baseFee == 0) baseFee = 1; }