diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs b/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs index 6adcf01a2a..7c24cb4151 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs @@ -691,6 +691,35 @@ public override Int32Value GetMaxBatchApproveCount(Empty input) }; } + public override Empty ExtendSeedExpirationTime(ExtendSeedExpirationTimeInput input) + { + var tokenInfo = GetTokenInfo(input.Symbol); + if (tokenInfo == null) + { + throw new AssertionException("Seed NFT does not exist."); + } + + Assert(tokenInfo.Owner == Context.Sender, "Sender is not Seed NFT owner."); + var oldExpireTimeLong = 0L; + if (tokenInfo.ExternalInfo.Value.TryGetValue(TokenContractConstants.SeedExpireTimeExternalInfoKey, + out var oldExpireTime)) + { + long.TryParse(oldExpireTime, out oldExpireTimeLong); + } + + tokenInfo.ExternalInfo.Value[TokenContractConstants.SeedExpireTimeExternalInfoKey] = + input.ExpirationTime.ToString(); + State.TokenInfos[input.Symbol] = tokenInfo; + Context.Fire(new SeedExpirationTimeUpdated + { + ChainId = tokenInfo.IssueChainId, + Symbol = input.Symbol, + OldExpirationTime = oldExpireTimeLong, + NewExpirationTime = input.ExpirationTime + }); + return new Empty(); + } + private int GetMaxBatchApproveCount() { return State.MaxBatchApproveCount.Value == 0 diff --git a/protobuf/aelf/core.proto b/protobuf/aelf/core.proto index 4fe3a5bcf4..d2812d46f7 100644 --- a/protobuf/aelf/core.proto +++ b/protobuf/aelf/core.proto @@ -59,6 +59,8 @@ enum TransactionResultStatus { PENDING_VALIDATION = 5; // Transaction validation failed. NODE_VALIDATION_FAILED = 6; + // Transaction is expired + EXPIRED = 7; } message TransactionResult { diff --git a/protobuf/token_contract_impl.proto b/protobuf/token_contract_impl.proto index 5885914e80..3ae6ffa2c5 100644 --- a/protobuf/token_contract_impl.proto +++ b/protobuf/token_contract_impl.proto @@ -182,6 +182,9 @@ service TokenContractImpl { rpc GetMaxBatchApproveCount (google.protobuf.Empty) returns (google.protobuf.Int32Value) { } + + rpc ExtendSeedExpirationTime (ExtendSeedExpirationTimeInput) returns (google.protobuf.Empty) { + } } message AdvanceResourceTokenInput { @@ -444,4 +447,17 @@ message ModifyTokenIssuerAndOwnerInput { message SetTokenIssuerAndOwnerModificationEnabledInput{ bool enabled = 1; +} + +message ExtendSeedExpirationTimeInput { + string symbol = 1; + int64 expiration_time = 2; +} + +message SeedExpirationTimeUpdated { + option (aelf.is_event) = true; + int32 chain_id = 1; + string symbol = 2; + int64 old_expiration_time = 3; + int64 new_expiration_time = 4; } \ No newline at end of file diff --git a/src/AElf.WebApp.Application.Chain/Dto/TransactionResultDto.cs b/src/AElf.WebApp.Application.Chain/Dto/TransactionResultDto.cs index e76f92a941..115643789b 100644 --- a/src/AElf.WebApp.Application.Chain/Dto/TransactionResultDto.cs +++ b/src/AElf.WebApp.Application.Chain/Dto/TransactionResultDto.cs @@ -1,10 +1,15 @@ +using System; + namespace AElf.WebApp.Application.Chain.Dto; public class TransactionResultDto { public string TransactionId { get; set; } + [Obsolete("The Status is obsolete. Use StatusV2 instead.")] public string Status { get; set; } + + public string StatusV2 { get; set; } public LogEventDto[] Logs { get; set; } diff --git a/src/AElf.WebApp.Application.Chain/Services/TransactionResultAppService.cs b/src/AElf.WebApp.Application.Chain/Services/TransactionResultAppService.cs index 4b6220c391..8bdc750025 100644 --- a/src/AElf.WebApp.Application.Chain/Services/TransactionResultAppService.cs +++ b/src/AElf.WebApp.Application.Chain/Services/TransactionResultAppService.cs @@ -24,6 +24,8 @@ namespace AElf.WebApp.Application.Chain; public interface ITransactionResultAppService { Task GetTransactionResultAsync(string transactionId); + + Task GetTransactionResultV2Async(string transactionId); Task> GetTransactionResultsAsync(string blockHash, int offset = 0, int limit = 10); @@ -130,6 +132,72 @@ await _transactionResultProxyService.InvalidTransactionResultService.GetInvalidT } return output; } + /// + /// Get the current status of a transaction, available since V1.12.0 + /// + /// transaction id + /// + public async Task GetTransactionResultV2Async(string transactionId) + { + Hash transactionIdHash; + try + { + transactionIdHash = Hash.LoadFromHex(transactionId); + } + catch + { + throw new UserFriendlyException(Error.Message[Error.InvalidTransactionId], + Error.InvalidTransactionId.ToString()); + } + + var transactionResult = await GetTransactionResultAsync(transactionIdHash); + var output = _objectMapper.GetMapper() + .Map(transactionResult, + opt => opt.Items[TransactionProfile.ErrorTrace] = _webAppOptions.IsDebugMode); + output.StatusV2 = output.Status; + + var transaction = await _transactionManager.GetTransactionAsync(transactionResult.TransactionId); + output.Transaction = _objectMapper.Map(transaction); + output.TransactionSize = transaction?.CalculateSize() ?? 0; + + var chain = await _blockchainService.GetChainAsync(); + if (transactionResult.Status == TransactionResultStatus.Pending && + chain.BestChainHeight - output.Transaction?.RefBlockNumber > KernelConstants.ReferenceBlockValidPeriod) + { + output.StatusV2 = TransactionResultStatus.Expired.ToString().ToUpper(); + return output; + } + + if (transactionResult.Status != TransactionResultStatus.NotExisted) + { + await FormatTransactionParamsAsync(output.Transaction, transaction.Params); + return output; + } + + var validationStatus = _transactionResultStatusCacheProvider.GetTransactionResultStatus(transactionIdHash); + if (validationStatus != null) + { + output.StatusV2 = validationStatus.TransactionResultStatus.ToString().ToUpper(); + output.Error = + TransactionErrorResolver.TakeErrorMessage(validationStatus.Error, _webAppOptions.IsDebugMode); + return output; + } + + if (_transactionOptions.StoreInvalidTransactionResultEnabled) + { + var failedTransactionResult = + await _transactionResultProxyService.InvalidTransactionResultService.GetInvalidTransactionResultAsync( + transactionIdHash); + if (failedTransactionResult != null) + { + output.StatusV2 = failedTransactionResult.Status.ToString().ToUpper(); + output.Error = failedTransactionResult.Error; + return output; + } + } + + return output; + } /// /// Get multiple transaction results. diff --git a/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs b/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs index 201e56d6e6..24d06e3eec 100644 --- a/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs +++ b/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs @@ -1893,4 +1893,15 @@ public async Task TokenIssuerAndOwnerModification_Test() result.TransactionResult.Error.ShouldContain("Set token issuer and owner disabled."); } + + [Theory] + [InlineData("SEED-0", 1731927992000)] + public async Task ExtendSeedExpirationTime_Test(string symbol, long expirationTime) + { + ExtendSeedExpirationTimeInput input = new ExtendSeedExpirationTimeInput(); + input.Symbol = symbol; + input.ExpirationTime = expirationTime; + + await TokenContractStub.ExtendSeedExpirationTime.CallAsync(input); + } } \ No newline at end of file diff --git a/test/AElf.Contracts.MultiToken.Tests/MultiTokenContractTestBase.cs b/test/AElf.Contracts.MultiToken.Tests/MultiTokenContractTestBase.cs index 232a5f5b8b..62a7f23ec6 100644 --- a/test/AElf.Contracts.MultiToken.Tests/MultiTokenContractTestBase.cs +++ b/test/AElf.Contracts.MultiToken.Tests/MultiTokenContractTestBase.cs @@ -240,4 +240,7 @@ internal async Task> CreateMutiTokenWithExceptionAsync( await CreateSeedNftAsync(stub, createInput); return await stub.Create.SendWithExceptionAsync(createInput); } + + + } \ No newline at end of file diff --git a/test/AElf.WebApp.Application.Chain.Tests/BlockChainAppServiceTest.cs b/test/AElf.WebApp.Application.Chain.Tests/BlockChainAppServiceTest.cs index 67e9db0b83..c4fc96777d 100644 --- a/test/AElf.WebApp.Application.Chain.Tests/BlockChainAppServiceTest.cs +++ b/test/AElf.WebApp.Application.Chain.Tests/BlockChainAppServiceTest.cs @@ -685,6 +685,37 @@ public async Task Get_TransactionResult_Success_Test() response.BlockNumber.ShouldBe(block.Height); response.BlockHash.ShouldBe(block.GetHash().ToHex()); } + + [Fact] + public async Task Get_TransactionResultV2_Expired_Test() + { + // Generate a transaction + var transaction = await _osTestHelper.GenerateTransferTransaction(); + + // Push chain height to be ref_block_number + at least 512 + await MineSomeBlocks(520); + var transactionHex = transaction.GetHash().ToHex(); + + // Broadcast expired transaction + await _osTestHelper.BroadcastTransactions(new List { transaction }); + + // Check transaction status + var response = await GetResponseAsObjectAsync( + $"/api/blockChain/transactionResultV2?transactionId={transactionHex}"); + response.StatusV2.ShouldBe(TransactionResultStatus.Expired.ToString().ToUpper()); + } + + private async Task MineSomeBlocks(int blockNumber) + { + var heightBefore = (await _osTestHelper.GetChainContextAsync()).BlockHeight; + for (var i = 0; i < blockNumber; i++) + { + await _osTestHelper.MinedOneBlock(); + } + + var heightAfter = (await _osTestHelper.GetChainContextAsync()).BlockHeight; + heightAfter.ShouldBe(heightBefore + blockNumber); + } [Fact] public async Task Get_Failed_TransactionResult_Success_Test()