diff --git a/backend/app/Savor22b.Tests/Action/BuyTradeGoodTests.cs b/backend/app/Savor22b.Tests/Action/BuyTradeGoodTests.cs new file mode 100644 index 00000000..3c474c79 --- /dev/null +++ b/backend/app/Savor22b.Tests/Action/BuyTradeGoodTests.cs @@ -0,0 +1,109 @@ +namespace Savor22b.Tests.Action; + +using System; +using System.Collections.Immutable; +using Libplanet; +using Libplanet.Assets; +using Libplanet.State; +using Savor22b.Action; +using Savor22b.States; +using Savor22b.States.Trade; +using Xunit; + +public class BuyTradeGoodTests : ActionTests +{ + public BuyTradeGoodTests() { } + + [Fact] + public void Execute_Success() + { + var (stateDelta, productId) = CreatePresetStateDelta(); + stateDelta = stateDelta.MintAsset( + SignerAddress(), + FungibleAssetValue.Parse(Currencies.KeyCurrency, "10") + ); + + var action = new BuyTradeGoodAction( + productId + ); + + stateDelta = action.Execute( + new DummyActionContext + { + PreviousStates = stateDelta, + Signer = SignerAddress(), + Random = random, + Rehearsal = false, + BlockIndex = 1, + } + ); + + var afterTradeInventoryState = DeriveTradeInventoryStateDelta(stateDelta); + var afterRootState = DeriveRootStateFromAccountStateDelta(stateDelta); + + Assert.Empty(afterTradeInventoryState.TradeGoods); + Assert.True(afterRootState.InventoryState.RefrigeratorStateList.Count == 1); + } + + [Fact] + public void Execute_Fail_InsufficientBalance() + { + var (stateDelta, productId) = CreatePresetStateDelta(); + + var action = new BuyTradeGoodAction( + productId + ); + + Assert.Throws(() => + { + action.Execute( + new DummyActionContext + { + PreviousStates = stateDelta, + Signer = SignerAddress(), + Random = random, + Rehearsal = false, + BlockIndex = 1, + } + ); + }); + } + + private (IAccountStateDelta, Guid) CreatePresetStateDelta() + { + IAccountStateDelta state = new DummyState(); + Address signerAddress = SignerAddress(); + + var rootStateEncoded = state.GetState(signerAddress); + RootState rootState = rootStateEncoded is Bencodex.Types.Dictionary bdict + ? new RootState(bdict) + : new RootState(); + TradeInventoryState tradeInventoryState = state.GetState(TradeInventoryState.StateAddress) is Bencodex.Types.Dictionary tradeInventoryStateEncoded + ? new TradeInventoryState(tradeInventoryStateEncoded) + : new TradeInventoryState(); + + var foodProductId = Guid.NewGuid(); + + var food = RefrigeratorState.CreateFood( + Guid.NewGuid(), + 1, + "D", + 1, + 1, + 1, + 1, + 1, + ImmutableList.Empty + ); + + tradeInventoryState = tradeInventoryState.RegisterGood( + new FoodGoodState( + signerAddress, + foodProductId, + FungibleAssetValue.Parse(Currencies.KeyCurrency, "10"), + food) + ); + state = state.SetState(TradeInventoryState.StateAddress, tradeInventoryState.Serialize()); + return (state.SetState(signerAddress, rootState.Serialize()), foodProductId); + } +} diff --git a/backend/app/Savor22b/Action/BuyTradeGoodAction.cs b/backend/app/Savor22b/Action/BuyTradeGoodAction.cs new file mode 100644 index 00000000..6519d1af --- /dev/null +++ b/backend/app/Savor22b/Action/BuyTradeGoodAction.cs @@ -0,0 +1,85 @@ +namespace Savor22b.Action; + +using System; +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Headless.Extensions; +using Libplanet.State; +using Savor22b.States; +using Savor22b.States.Trade; +using Savor22b.Action.Exceptions; + +[ActionType(nameof(BuyTradeGoodAction))] +public class BuyTradeGoodAction : SVRAction +{ + public BuyTradeGoodAction() { } + + public BuyTradeGoodAction(Guid productId) + { + ProductId = productId; + } + + public Guid ProductId; + + protected override IImmutableDictionary PlainValueInternal => + new Dictionary() + { + [nameof(ProductId)] = ProductId.Serialize(), + }.ToImmutableDictionary(); + + protected override void LoadPlainValueInternal(IImmutableDictionary plainValue) + { + ProductId = plainValue[nameof(ProductId)].ToGuid(); + } + + public override IAccountStateDelta Execute(IActionContext ctx) + { + if (ctx.Rehearsal) + { + return ctx.PreviousStates; + } + + IAccountStateDelta states = ctx.PreviousStates; + + RootState rootState = states.GetState(ctx.Signer) is Dictionary rootStateEncoded + ? new RootState(rootStateEncoded) + : new RootState(); + TradeInventoryState tradeInventoryState = states.GetState(TradeInventoryState.StateAddress) is Dictionary tradeInventoryStateEncoded + ? new TradeInventoryState(tradeInventoryStateEncoded) + : new TradeInventoryState(); + + var inventoryState = rootState.InventoryState; + + var good = tradeInventoryState.TradeGoods.First(g => g.Key == ProductId); + + states = states.TransferAsset( + ctx.Signer, + good.Value.SellerAddress, + good.Value.Price, + allowNegativeBalance: false + ); + + tradeInventoryState.TradeGoods.Remove(good.Key); + + switch (good.Value) + { + case FoodGoodState foodGoodState: + inventoryState = inventoryState.AddRefrigeratorItem(foodGoodState.Food); + break; + case ItemsGoodState itemsGoodState: + foreach (var itemState in itemsGoodState.Items) + { + inventoryState = inventoryState.AddItem(itemState); + } + + break; + default: + throw new InvalidValueException("Food or Items required"); + } + + rootState.SetInventoryState(inventoryState); + states = states.SetState(TradeInventoryState.StateAddress, tradeInventoryState.Serialize()); + return states.SetState(ctx.Signer, rootState.Serialize()); + } +} diff --git a/backend/app/Savor22b/GraphTypes/Query/Query.cs b/backend/app/Savor22b/GraphTypes/Query/Query.cs index ff34b1fd..756e512e 100644 --- a/backend/app/Savor22b/GraphTypes/Query/Query.cs +++ b/backend/app/Savor22b/GraphTypes/Query/Query.cs @@ -577,6 +577,38 @@ swarm is null } ); + Field>( + "createAction_BuyTradeGoodAction", + description: "무역상점 구매", + arguments: new QueryArguments( + new QueryArgument> + { + Name = "publicKey", + Description = "The base64-encoded public key for Transaction.", + }, + new QueryArgument> + { + Name = "productId", + Description = "상품 고유 Id", + } + ), + resolve: context => + { + var publicKey = new PublicKey( + ByteUtil.ParseHex(context.GetArgument("publicKey")) + ); + + var action = new BuyTradeGoodAction(context.GetArgument("productId")); + + return new GetUnsignedTransactionHex( + action, + publicKey, + _blockChain, + _swarm + ).UnsignedTransactionHex; + } + ); + Field>( "createAction_RegisterTradeGoodAction", description: "Register Trade Good to Trade Store",