Skip to content

Commit

Permalink
Merge pull request #2148 from planetarium/feat/claim-item
Browse files Browse the repository at this point in the history
Add ClaimItems action
  • Loading branch information
longfin authored Sep 27, 2023
2 parents ccd74ec + e75a5ac commit e8313d2
Show file tree
Hide file tree
Showing 4 changed files with 341 additions and 0 deletions.
218 changes: 218 additions & 0 deletions .Lib9c.Tests/Action/ClaimItemsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
namespace Lib9c.Tests.Action
{
using System;
using System.Collections.Generic;
using System.Linq;
using Libplanet.Action.State;
using Libplanet.Crypto;
using Libplanet.Types.Assets;
using Nekoyume;
using Nekoyume.Action;
using Nekoyume.Model;
using Nekoyume.Model.State;
using Serilog;
using Xunit;
using Xunit.Abstractions;

public class ClaimItemsTest
{
private readonly IAccount _initialState;
private readonly Address _signerAddress;

private readonly TableSheets _tableSheets;
private readonly List<Currency> _currencies;
private readonly List<int> _itemIds;

public ClaimItemsTest(ITestOutputHelper outputHelper)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.TestOutput(outputHelper)
.CreateLogger();

_initialState = new MockStateDelta();

var sheets = TableSheetsImporter.ImportSheets();
foreach (var (key, value) in sheets)
{
_initialState = _initialState
.SetState(Addresses.TableSheet.Derive(key), value.Serialize());
}

_tableSheets = new TableSheets(sheets);
_itemIds = _tableSheets.CostumeItemSheet.Values.Take(3).Select(x => x.Id).ToList();
_currencies = _itemIds.Select(id => Currency.Legacy($"Item_T_{id}", 0, minters: null)).ToList();

_signerAddress = new PrivateKey().ToAddress();

var context = new ActionContext();
_initialState = _initialState
.MintAsset(context, _signerAddress, _currencies[0] * 5)
.MintAsset(context, _signerAddress, _currencies[1] * 5)
.MintAsset(context, _signerAddress, _currencies[2] * 5);
}

[Fact]
public void Serialize()
{
var states = GenerateAvatar(_initialState, out var avatarAddress1);
GenerateAvatar(states, out var avatarAddress2);

var action = new ClaimItems(new List<(Address, IReadOnlyList<FungibleAssetValue>)>
{
(avatarAddress1, new List<FungibleAssetValue> { _currencies[0] * 1, _currencies[1] * 1 }),
(avatarAddress2, new List<FungibleAssetValue> { _currencies[0] * 1 }),
});
var deserialized = new ClaimItems();
deserialized.LoadPlainValue(action.PlainValue);

var orderedClaimData = action.ClaimData.OrderBy(x => x.address).ToList();

foreach (var i in Enumerable.Range(0, 2))
{
Assert.Equal(orderedClaimData[i].address, deserialized.ClaimData[i].address);
Assert.True(orderedClaimData[i].fungibleAssetValues
.SequenceEqual(deserialized.ClaimData[i].fungibleAssetValues));
}
}

[Fact]
public void Execute_Throws_ArgumentException_TickerInvalid()
{
var state = GenerateAvatar(_initialState, out var recipientAvatarAddress);

var currency = Currencies.Crystal;
var action = new ClaimItems(new List<(Address, IReadOnlyList<FungibleAssetValue>)>
{
(recipientAvatarAddress, new List<FungibleAssetValue> { currency * 1 }),
});
Assert.Throws<ArgumentException>(() =>
action.Execute(new ActionContext
{
PreviousState = state,
Signer = _signerAddress,
BlockIndex = 100,
Random = new TestRandom(),
}));
}

[Fact]
public void Execute_Throws_WhenNotEnoughBalance()
{
var state = GenerateAvatar(_initialState, out var recipientAvatarAddress);

var currency = _currencies.First();
var action = new ClaimItems(new List<(Address, IReadOnlyList<FungibleAssetValue>)>
{
(recipientAvatarAddress, new List<FungibleAssetValue> { currency * 6 }),
});
Assert.Throws<InsufficientBalanceException>(() =>
action.Execute(new ActionContext
{
PreviousState = state,
Signer = _signerAddress,
BlockIndex = 100,
Random = new TestRandom(),
}));
}

[Fact]
public void Execute()
{
var state = GenerateAvatar(_initialState, out var recipientAvatarAddress);

var fungibleAssetValues = _currencies.Select(currency => currency * 1).ToList();
var action = new ClaimItems(new List<(Address, IReadOnlyList<FungibleAssetValue>)>
{
(recipientAvatarAddress, fungibleAssetValues),
});
var states = action.Execute(new ActionContext
{
PreviousState = state,
Signer = _signerAddress,
BlockIndex = 0,
Random = new TestRandom(),
});

var inventory = states.GetInventory(recipientAvatarAddress.Derive(SerializeKeys.LegacyInventoryKey));
foreach (var i in Enumerable.Range(0, 3))
{
Assert.Equal(_currencies[i] * 4, states.GetBalance(_signerAddress, _currencies[i]));
Assert.Equal(
1,
inventory.Items.First(x => x.item.Id == _itemIds[i]).count);
}
}

[Fact]
public void Execute_WithMultipleRecipients()
{
var state = GenerateAvatar(_initialState, out var recipientAvatarAddress1);
state = GenerateAvatar(state, out var recipientAvatarAddress2);

var recipientAvatarAddresses = new List<Address>
{
recipientAvatarAddress1, recipientAvatarAddress2,
};
var fungibleAssetValues = _currencies.Select(currency => currency * 1).ToList();

var action = new ClaimItems(new List<(Address, IReadOnlyList<FungibleAssetValue>)>
{
(recipientAvatarAddress1, fungibleAssetValues.Take(2).ToList()),
(recipientAvatarAddress2, fungibleAssetValues),
});

var states = action.Execute(new ActionContext
{
PreviousState = state,
Signer = _signerAddress,
BlockIndex = 0,
Random = new TestRandom(),
});

Assert.Equal(states.GetBalance(_signerAddress, _currencies[0]), _currencies[0] * 3);
Assert.Equal(states.GetBalance(_signerAddress, _currencies[1]), _currencies[1] * 3);
Assert.Equal(states.GetBalance(_signerAddress, _currencies[2]), _currencies[2] * 4);

var inventory1 = states.GetInventory(recipientAvatarAddress1.Derive(SerializeKeys.LegacyInventoryKey));
Assert.Equal(1, inventory1.Items.First(x => x.item.Id == _itemIds[0]).count);
Assert.Equal(1, inventory1.Items.First(x => x.item.Id == _itemIds[1]).count);

var inventory2 = states.GetInventory(recipientAvatarAddress2.Derive(SerializeKeys.LegacyInventoryKey));
Assert.Equal(1, inventory2.Items.First(x => x.item.Id == _itemIds[0]).count);
Assert.Equal(1, inventory2.Items.First(x => x.item.Id == _itemIds[1]).count);
Assert.Equal(1, inventory2.Items.First(x => x.item.Id == _itemIds[2]).count);
}

private IAccount GenerateAvatar(IAccount state, out Address avatarAddress)
{
var address = new PrivateKey().ToAddress();
var agentState = new AgentState(address);
avatarAddress = address.Derive("avatar");
var rankingMapAddress = new PrivateKey().ToAddress();
var avatarState = new AvatarState(
avatarAddress,
address,
0,
_tableSheets.GetAvatarSheets(),
new GameConfigState(),
rankingMapAddress)
{
worldInformation = new WorldInformation(
0,
_tableSheets.WorldSheet,
GameConfig.RequireClearedStageLevel.ActionsInShop),
};
agentState.avatarAddresses[0] = avatarAddress;

state = state
.SetState(address, agentState.Serialize())
.SetState(avatarAddress, avatarState.Serialize())
.SetState(
avatarAddress.Derive(SerializeKeys.LegacyInventoryKey),
avatarState.inventory.Serialize());

return state;
}
}
}
109 changes: 109 additions & 0 deletions Lib9c/Action/ClaimItems.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Bencodex.Types;
using Libplanet.Action;
using Libplanet.Action.State;
using Libplanet.Crypto;
using Libplanet.Types.Assets;
using Nekoyume.Extensions;
using Nekoyume.Model.Item;
using Nekoyume.Model.State;
using Nekoyume.TableData;
using static Lib9c.SerializeKeys;

namespace Nekoyume.Action
{
[ActionType(ActionTypeText)]
public class ClaimItems : GameAction, IClaimItems
{
private const string ActionTypeText = "claim_items";

public IReadOnlyList<(Address address, IReadOnlyList<FungibleAssetValue> fungibleAssetValues)> ClaimData { get; private set; }

public ClaimItems()
{
}

public ClaimItems(IReadOnlyList<(Address, IReadOnlyList<FungibleAssetValue>)> claimData)
{
ClaimData = claimData;
}

protected override IImmutableDictionary<string, IValue> PlainValueInternal =>
ImmutableDictionary<string, IValue>.Empty
.Add(ClaimDataKey, ClaimData.Select(tuple =>
{
var serializedFungibleAssetValues = tuple.fungibleAssetValues.Select(x => x.Serialize()).Serialize();

return (tuple.address, serialized: serializedFungibleAssetValues);
}).Serialize());

protected override void LoadPlainValueInternal(
IImmutableDictionary<string, IValue> plainValue)
{
ClaimData = plainValue[ClaimDataKey].ToStateList()
.Select((tuple =>
{
return (
tuple.Item1,
tuple.Item2.ToList((x => x.ToFungibleAssetValue())) as IReadOnlyList<FungibleAssetValue>);
})).ToList();
}

public override IAccount Execute(IActionContext context)
{
context.UseGas(1);

var states = context.PreviousState;
var itemSheet = states.GetSheets(containItemSheet: true).GetItemSheet();

foreach (var (avatarAddress, fungibleAssetValues) in ClaimData)
{
var inventoryAddress = avatarAddress.Derive(LegacyInventoryKey);
var inventory = states.GetInventory(inventoryAddress)
?? throw new FailedLoadStateException(
ActionTypeText,
GetSignerAndOtherAddressesHex(context, inventoryAddress),
typeof(Inventory),
inventoryAddress);

foreach (var fungibleAssetValue in fungibleAssetValues)
{
if (fungibleAssetValue.Currency.DecimalPlaces != 0)
{
throw new ArgumentException(
$"DecimalPlaces of fungibleAssetValue for claimItems are not 0: {fungibleAssetValue.Currency.Ticker}");
}

var parsedTicker = fungibleAssetValue.Currency.Ticker.Split("_");
if (parsedTicker.Length != 3
|| parsedTicker[0] != "Item"
|| (parsedTicker[1] != "NT" && parsedTicker[1] != "T")
|| !int.TryParse(parsedTicker[2], out var itemId))
{
throw new ArgumentException(
$"Format of Amount currency's ticker is invalid");
}

states = states.BurnAsset(context, context.Signer, fungibleAssetValue);

var item = itemSheet[itemId] switch
{
MaterialItemSheet.Row materialRow => parsedTicker[1] == "T"
? ItemFactory.CreateTradableMaterial(materialRow)
: ItemFactory.CreateMaterial(materialRow),
var itemRow => ItemFactory.CreateItem(itemRow, context.Random)
};

inventory.AddItem(item, (int)fungibleAssetValue.RawValue);
}

states = states.SetState(inventoryAddress, inventory.Serialize());
}

return states;
}
}
}
11 changes: 11 additions & 0 deletions Lib9c/Action/IClaimItems.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using Libplanet.Crypto;
using Libplanet.Types.Assets;

namespace Nekoyume.Action
{
public interface IClaimItems
{
public IReadOnlyList<(Address address, IReadOnlyList<FungibleAssetValue> fungibleAssetValues)> ClaimData { get; }
}
}
3 changes: 3 additions & 0 deletions Lib9c/SerializeKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,8 @@ public static class SerializeKeys

// Grand Finale
public const string GrandFinaleIdKey = "gfi";

// ClaimItems
public const string ClaimDataKey = "cd";
}
}

0 comments on commit e8313d2

Please sign in to comment.