diff --git a/backend/app/Savor22b/Action/BuyCookingEquipmentAction.cs b/backend/app/Savor22b/Action/BuyCookingEquipmentAction.cs index 4f792790..c4c74b45 100644 --- a/backend/app/Savor22b/Action/BuyCookingEquipmentAction.cs +++ b/backend/app/Savor22b/Action/BuyCookingEquipmentAction.cs @@ -8,6 +8,7 @@ namespace Savor22b.Action; using Libplanet.Assets; using Libplanet.Headless.Extensions; using Libplanet.State; +using Savor22b.Action.Exceptions; using Savor22b.Constants; using Savor22b.Helpers; using Savor22b.Model; diff --git a/backend/app/Savor22b/Action/BuyRandomSeedItemAction.cs b/backend/app/Savor22b/Action/BuyRandomSeedItemAction.cs index 55afc5f6..d88c1efe 100644 --- a/backend/app/Savor22b/Action/BuyRandomSeedItemAction.cs +++ b/backend/app/Savor22b/Action/BuyRandomSeedItemAction.cs @@ -8,6 +8,7 @@ namespace Savor22b.Action; using Libplanet.Assets; using Libplanet.Headless.Extensions; using Libplanet.State; +using Savor22b.Action.Exceptions; using Savor22b.Constants; using Savor22b.Helpers; using Savor22b.Model; diff --git a/backend/app/Savor22b/Action/Exceptions/ActionException.cs b/backend/app/Savor22b/Action/Exceptions/ActionException.cs new file mode 100644 index 00000000..f9ea296a --- /dev/null +++ b/backend/app/Savor22b/Action/Exceptions/ActionException.cs @@ -0,0 +1,83 @@ +using System.Runtime.Serialization; +using Bencodex.Types; +using Libplanet.Action; + +[Serializable] +public class ActionException : Exception, IExtractableException, ISerializable +{ + // 새로운 예외 클래스에서 사용할 메타데이터 프로퍼티 + private readonly string ErrorType; + private readonly string ErrorMessage; + private readonly int? ErrorCode; + public IValue Metadata { get; } + + public ActionException(string message, string errorType, int? errorCode) + : base(message) + { + ErrorType = errorType; + ErrorMessage = message; + ErrorCode = errorCode; + + var metaData = new Dictionary( + new[] + { + new KeyValuePair( + (Text) "errorType", + (Text) errorType + ), + new KeyValuePair( + (Text) "errorMessage", + (Text) message + ) + } + ); + + if (errorCode is int code) + { + Metadata = metaData.SetItem( + (Text)"errorCode", + (Integer)code + ); + } + + Metadata = metaData; + + } + protected ActionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + if (info.GetString(nameof(ErrorType)) is { } errorType) + { + ErrorType = errorType; + } + + if (info.GetString(nameof(ErrorMessage)) is { } errorMessage) + { + ErrorMessage = errorMessage; + } + + if (info.GetInt32(nameof(ErrorCode)) is int errorCode) + { + ErrorCode = errorCode; + } + + if (info.GetValue(nameof(Metadata), typeof(IValue)) is IValue metadata) + { + Metadata = metadata; + } + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + info.AddValue(nameof(ErrorType), ErrorType); + info.AddValue(nameof(ErrorMessage), ErrorMessage); + info.AddValue(nameof(ErrorCode), ErrorCode); + + if (Metadata is { } metadata) + { + info.AddValue(nameof(Metadata), metadata); + } + } +} diff --git a/backend/app/Savor22b/Action/Exceptions/HouseAlreadyPlacedException.cs b/backend/app/Savor22b/Action/Exceptions/HouseAlreadyPlacedException.cs new file mode 100644 index 00000000..f9d57c3b --- /dev/null +++ b/backend/app/Savor22b/Action/Exceptions/HouseAlreadyPlacedException.cs @@ -0,0 +1,11 @@ +namespace Savor22b.Action.Exceptions; + + +[Serializable] +public class HouseAlreadyPlacedException : ActionException +{ + public HouseAlreadyPlacedException(string message, int? errorCode = null) + : base(message, "HouseAlreadyPlaced", errorCode) + { + } +} diff --git a/backend/app/Savor22b/Action/InvalidTransferSignerException.cs b/backend/app/Savor22b/Action/Exceptions/InvalidTransferSignerException.cs similarity index 97% rename from backend/app/Savor22b/Action/InvalidTransferSignerException.cs rename to backend/app/Savor22b/Action/Exceptions/InvalidTransferSignerException.cs index 14e53afb..68466a63 100644 --- a/backend/app/Savor22b/Action/InvalidTransferSignerException.cs +++ b/backend/app/Savor22b/Action/Exceptions/InvalidTransferSignerException.cs @@ -1,4 +1,4 @@ -namespace Savor22b.Action; +namespace Savor22b.Action.Exceptions; using System.Runtime.Serialization; using Libplanet; diff --git a/backend/app/Savor22b/Action/Exceptions/InvalidVillageException.cs b/backend/app/Savor22b/Action/Exceptions/InvalidVillageException.cs new file mode 100644 index 00000000..c5ba0995 --- /dev/null +++ b/backend/app/Savor22b/Action/Exceptions/InvalidVillageException.cs @@ -0,0 +1,11 @@ +namespace Savor22b.Action.Exceptions; + + +[Serializable] +public class InvalidVillageException : ActionException +{ + public InvalidVillageException(string message, int? errorCode = null) + : base(message, "InvalidVillage", errorCode) + { + } +} diff --git a/backend/app/Savor22b/Action/Exceptions/NotEnoughRawMaterialsException.cs b/backend/app/Savor22b/Action/Exceptions/NotEnoughRawMaterialsException.cs new file mode 100644 index 00000000..58a99889 --- /dev/null +++ b/backend/app/Savor22b/Action/Exceptions/NotEnoughRawMaterialsException.cs @@ -0,0 +1,13 @@ +namespace Savor22b.Action.Exceptions; + +using System.Runtime.Serialization; + + +[Serializable] +public class NotEnoughRawMaterialsException : ActionException +{ + public NotEnoughRawMaterialsException(string message, int? errorCode = null) + : base(message, "NotEnoughRawMaterials", errorCode) + { + } +} diff --git a/backend/app/Savor22b/Action/Exceptions/NotFoundTableDataException.cs b/backend/app/Savor22b/Action/Exceptions/NotFoundTableDataException.cs new file mode 100644 index 00000000..9dc70d53 --- /dev/null +++ b/backend/app/Savor22b/Action/Exceptions/NotFoundTableDataException.cs @@ -0,0 +1,11 @@ +namespace Savor22b.Action.Exceptions; + + +[Serializable] +public class NotFoundTableDataException : ActionException +{ + public NotFoundTableDataException(string message, int? errorCode = null) + : base(message, "NotFoundTableData", errorCode) + { + } +} diff --git a/backend/app/Savor22b/Action/GenerateFoodAction.cs b/backend/app/Savor22b/Action/GenerateFoodAction.cs index e972926e..a7982403 100644 --- a/backend/app/Savor22b/Action/GenerateFoodAction.cs +++ b/backend/app/Savor22b/Action/GenerateFoodAction.cs @@ -6,6 +6,7 @@ namespace Savor22b.Action; using Libplanet.Action; using Libplanet.Headless.Extensions; using Libplanet.State; +using Savor22b.Action.Exceptions; using Savor22b.Helpers; using Savor22b.Model; using Savor22b.States; diff --git a/backend/app/Savor22b/Action/NotEnoughRawMaterialsException.cs b/backend/app/Savor22b/Action/NotEnoughRawMaterialsException.cs deleted file mode 100644 index 923af20f..00000000 --- a/backend/app/Savor22b/Action/NotEnoughRawMaterialsException.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Savor22b.Action; - -using System.Runtime.Serialization; - - -[Serializable] -public class NotEnoughRawMaterialsException : Exception, ISerializable -{ - public NotEnoughRawMaterialsException() - { - } - - public NotEnoughRawMaterialsException(string? message) - : base(message) - { - } - - public NotEnoughRawMaterialsException(string? message, Exception? innerException) - : base(message, innerException) - { - } -} diff --git a/backend/app/Savor22b/Action/NotFoundTableDataException.cs b/backend/app/Savor22b/Action/NotFoundTableDataException.cs deleted file mode 100644 index 7562abc2..00000000 --- a/backend/app/Savor22b/Action/NotFoundTableDataException.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Savor22b.Action; - -using System.Runtime.Serialization; - - -[Serializable] -public class NotFoundTableDataException : Exception, ISerializable -{ - public NotFoundTableDataException() - { - } - - public NotFoundTableDataException(string? message) - : base(message) - { - } - - public NotFoundTableDataException(string? message, Exception? innerException) - : base(message, innerException) - { - } -} diff --git a/backend/app/Savor22b/Action/PlaceUserHouseAction.cs b/backend/app/Savor22b/Action/PlaceUserHouseAction.cs index b279d2ac..08938551 100644 --- a/backend/app/Savor22b/Action/PlaceUserHouseAction.cs +++ b/backend/app/Savor22b/Action/PlaceUserHouseAction.cs @@ -10,6 +10,7 @@ namespace Savor22b.Action; using Libplanet.Headless.Extensions; using Savor22b.Constants; using Libplanet; +using Savor22b.Action.Exceptions; [ActionType(nameof(PlaceUserHouseAction))] public class PlaceUserHouseAction : SVRAction @@ -84,12 +85,12 @@ public override IAccountStateDelta Execute(IActionContext ctx) if (village == null) { - throw new ArgumentException("Invalid village ID"); + throw new InvalidVillageException("Invalid village ID"); } if (!isAbleToPlaceHouse(village, TargetX, TargetY)) { - throw new ArgumentException("Invalid target position"); + throw new InvalidVillageException("Invalid target position"); } GlobalUserHouseState globalUserHouseState = @@ -101,7 +102,7 @@ public override IAccountStateDelta Execute(IActionContext ctx) if (globalUserHouseState.UserHouse.ContainsKey(userHouseKey)) { - throw new ArgumentException("House already placed"); + throw new HouseAlreadyPlacedException("House already placed"); } RootState rootState = states.GetState(ctx.Signer) is Bencodex.Types.Dictionary rootStateEncoded diff --git a/backend/app/Savor22b/Action/TransferAsset.cs b/backend/app/Savor22b/Action/TransferAsset.cs index 5009fcf0..24a133bd 100644 --- a/backend/app/Savor22b/Action/TransferAsset.cs +++ b/backend/app/Savor22b/Action/TransferAsset.cs @@ -7,6 +7,7 @@ namespace Savor22b.Action; using Libplanet.State; using Libplanet.Assets; using Libplanet.Headless.Extensions; +using Savor22b.Action.Exceptions; /// /// Basically, it's just a double of , diff --git a/backend/app/Savor22b/Action/UseRandomSeedItemAction.cs b/backend/app/Savor22b/Action/UseRandomSeedItemAction.cs index a45a1ab7..55847883 100644 --- a/backend/app/Savor22b/Action/UseRandomSeedItemAction.cs +++ b/backend/app/Savor22b/Action/UseRandomSeedItemAction.cs @@ -5,6 +5,7 @@ namespace Savor22b.Action; using Libplanet.Action; using Libplanet.Headless.Extensions; using Libplanet.State; +using Savor22b.Action.Exceptions; using Savor22b.Helpers; using Savor22b.Model; using Savor22b.States; diff --git a/backend/app/Savor22b/GraphTypes/ExceptionMetadata.cs b/backend/app/Savor22b/GraphTypes/ExceptionMetadata.cs new file mode 100644 index 00000000..fa73ea06 --- /dev/null +++ b/backend/app/Savor22b/GraphTypes/ExceptionMetadata.cs @@ -0,0 +1,40 @@ +namespace Savor22b.GraphTypes; + +using Bencodex.Types; +using Libplanet.Headless.Extensions; + +public class ExceptionMetadata +{ + public string errorType { get; set; } + public string errorMessage { get; set; } + public int? errorCode { get; set; } + + public ExceptionMetadata(string errorType, string errorMessage, int? errorCode) + { + this.errorType = errorType; + this.errorMessage = errorMessage; + this.errorCode = errorCode; + } + + public ExceptionMetadata(Bencodex.Types.IValue encoded) + { + if (encoded is Bencodex.Types.Dictionary dict) + { + // Bencodex 데이터에서 필요한 값을 꺼내서 ExceptionMetadata 객체에 매핑합니다. + if (dict.TryGetValue((Text)"errorType", out var errorTypeValue) && errorTypeValue is Text errorTypeText) + { + errorType = errorTypeText.Value; + } + + if (dict.TryGetValue((Text)"errorMessage", out var errorMessageValue) && errorMessageValue is Text errorMessageText) + { + errorMessage = errorMessageText.Value; + } + + if (dict.TryGetValue((Text)"errorCode", out var errorCodeValue) && errorCodeValue is Integer errorCodeInteger) + { + errorCode = (int)errorCodeInteger; + } + } + } +} diff --git a/backend/app/Savor22b/GraphTypes/ExceptionMetadataType.cs b/backend/app/Savor22b/GraphTypes/ExceptionMetadataType.cs new file mode 100644 index 00000000..084aa960 --- /dev/null +++ b/backend/app/Savor22b/GraphTypes/ExceptionMetadataType.cs @@ -0,0 +1,28 @@ +using Bencodex.Types; +using GraphQL.Types; +using Savor22b.GraphTypes; + + +public class ExceptionMetadataType : ObjectGraphType +{ + public ExceptionMetadataType() + { + Field>( + name: "errorType", + description: "The error type of the exception.", + resolve: context => context.Source.errorType + ); + + Field>( + name: "errorMessage", + description: "The error message of the exception.", + resolve: context => context.Source.errorMessage + ); + + Field( + name: "errorCode", + description: "The error code of the exception.", + resolve: context => context.Source.errorCode + ); + } +} diff --git a/backend/app/Savor22b/GraphTypes/Subscription.cs b/backend/app/Savor22b/GraphTypes/Subscription.cs index 1361e922..a271cd33 100644 --- a/backend/app/Savor22b/GraphTypes/Subscription.cs +++ b/backend/app/Savor22b/GraphTypes/Subscription.cs @@ -12,8 +12,10 @@ namespace Savor22b.GraphTypes; using GraphQL; using System.Reactive.Concurrency; using Libplanet.Blocks; +using Libplanet.Store; using System.Reactive.Subjects; using Libplanet.Tx; +using Libplanet.Explorer.GraphTypes; public class Subscription : ObjectGraphType { @@ -24,15 +26,19 @@ public class Subscription : ObjectGraphType private readonly BlockChain _blockChain; private readonly BlockRenderer _blockRenderer; + private readonly IStore _store; private readonly Subject _subject; + public Subscription( BlockChain blockChain, BlockRenderer blockRenderer, + IStore store, Swarm? swarm = null ) { _blockChain = blockChain; _blockRenderer = blockRenderer; + _store = store; _subject = new Subject(); AddField( @@ -67,9 +73,9 @@ public Subscription( AddField( new FieldType() { - Name = "TransactionApplied", - Type = typeof(TxAppliedGraphType), - Description = "Transaction completed", + Name = "TransactionResult", + Type = typeof(TxResultExtendType), + Description = "Transaction Result", Arguments = new QueryArguments( new QueryArgument> { @@ -77,35 +83,58 @@ public Subscription( Description = "Transaction id", } ), - Resolver = new FuncFieldResolver(context => + Resolver = new FuncFieldResolver(context => { - var strId = context.GetArgument("txId"); - var txId = new TxId(ByteUtil.ParseHex(strId)); - var tx = _blockChain.GetTransaction(txId); - bool transactionExists = _blockChain.GetTransaction(txId) != null; + if (!(_blockChain is BlockChain blockChain)) + { + throw new ExecutionError( + "blockChain was not set yet!"); + } + + if (!(_store is IStore store)) + { + throw new ExecutionError( + "store was not set yet!"); + } - return new TxApplied(transactionExists); - }), - StreamResolver = new SourceStreamResolver((context) => - { var strId = context.GetArgument("txId"); var txId = new TxId(ByteUtil.ParseHex(strId)); - return _subject - .DistinctUntilChanged() - .Select(_ => + if (!(store.GetFirstTxIdBlockHashIndex(txId) is { } txExecutedBlockHash)) + { + if (_blockChain.GetStagedTransactionIds().Contains(txId)) + { + return new TxResult(TxStatus.STAGING, null, null, null, null, null, null, null, null); + } + else { - try - { - var tx = _blockChain.GetTransaction(txId); - return new TxApplied(tx != null); - } - catch (Exception e) - { - return new TxApplied(false); - } - }); + return new TxResult(TxStatus.INVALID, null, null, null, null, null, null, null, null); + } + } + + try + { + TxExecution execution = blockChain.GetTxExecution(txExecutedBlockHash, txId); + Block txExecutedBlock = blockChain[txExecutedBlockHash]; + + return execution switch + { + TxSuccess txSuccess => new TxResult(TxStatus.SUCCESS, txExecutedBlock.Index, + txExecutedBlock.Hash.ToString(), null, null, txSuccess.UpdatedStates, txSuccess.FungibleAssetsDelta, txSuccess.UpdatedFungibleAssets, txSuccess.ActionsLogsList), + TxFailure txFailure => new TxResult(TxStatus.FAILURE, txExecutedBlock.Index, + txExecutedBlock.Hash.ToString(), txFailure.ExceptionName, txFailure.ExceptionMetadata, null, null, null, null), + _ => throw new NotImplementedException( + $"{nameof(execution)} is not expected concrete class.") + }; + } + catch (Exception) + { + return new TxResult(TxStatus.INVALID, null, null, null, null, null, null, null, null); + } }), + StreamResolver = new SourceStreamResolver>( + (context) => _subject.DistinctUntilChanged().Select(_ => _subject) + ) } ); diff --git a/backend/app/Savor22b/GraphTypes/TxResultExtendType.cs b/backend/app/Savor22b/GraphTypes/TxResultExtendType.cs new file mode 100644 index 00000000..30ec4e3a --- /dev/null +++ b/backend/app/Savor22b/GraphTypes/TxResultExtendType.cs @@ -0,0 +1,90 @@ +namespace Savor22b.GraphTypes; + +using System.Collections.Generic; +using System.Linq; +using GraphQL.Types; +using Libplanet; +using Libplanet.Assets; +using Libplanet.Explorer.GraphTypes; +using static Libplanet.Explorer.GraphTypes.TxResultType; + +public class TxResultExtendType : ObjectGraphType +{ + public TxResultExtendType() + { + + Field>( + nameof(TxResult.TxStatus), + description: "The transaction status.", + resolve: context => context.Source.TxStatus + ); + + Field( + nameof(TxResult.BlockIndex), + description: "The block index which the target transaction executed.", + resolve: context => context.Source.BlockIndex + ); + + Field( + nameof(TxResult.BlockHash), + description: "The block hash which the target transaction executed.", + resolve: context => context.Source.BlockHash + ); + + Field( + nameof(TxResult.ExceptionName), + description: "The name of exception. (when only failed)", + resolve: context => context.Source.ExceptionName + ); + + Field( + nameof(TxResult.ExceptionMetadata), + description: "The hexadecimal string of the exception metadata. (when only failed)", + resolve: context => context.Source.ExceptionMetadata != null ? new ExceptionMetadata(context.Source.ExceptionMetadata) : null + ); + + Field>>( + nameof(TxResult.UpdatedStates), + resolve: context => context.Source.UpdatedStates? + .Select(pair => new UpdatedState(pair.Key, pair.Value)) + ); + + Field>>( + nameof(TxResult.UpdatedFungibleAssets), + resolve: context => context.Source.UpdatedFungibleAssets? + .Select(pair => new FungibleAssetBalances(pair.Key, pair.Value.Values)) + ); + + Field>>( + nameof(TxResult.FungibleAssetsDelta), + resolve: context => context.Source.FungibleAssetsDelta? + .Select(pair => new FungibleAssetBalances(pair.Key, pair.Value.Values)) + ); + + Field + >> + >>( + nameof(TxResult.ActionsLogsList), + resolve: context => context.Source.ActionsLogsList + ); + } + + public record UpdatedState(Address Address, Bencodex.Types.IValue? State); + + public class UpdatedStateType : ObjectGraphType + { + public UpdatedStateType() + { + Field>( + nameof(UpdatedState.Address), + resolve: context => context.Source.Address + ); + Field( + nameof(UpdatedState.State), + resolve: context => context.Source.State + ); + } + } +}